diff --git a/.agentignore b/.agentignore new file mode 100644 index 0000000..00930df --- /dev/null +++ b/.agentignore @@ -0,0 +1 @@ +.scratch/ diff --git a/.agents/skills/_shared/annotation-ownership.md b/.agents/skills/_shared/annotation-ownership.md deleted file mode 100644 index 1ab7a69..0000000 --- a/.agents/skills/_shared/annotation-ownership.md +++ /dev/null @@ -1,95 +0,0 @@ -# Annotation Ownership (canonical reference) - -Shared reference for which `@architect-*` tags live on feature files -versus on code stubs / production TypeScript. Linked from -`architect-design-session`, `architect-implement-spec`, and -`architect-review-implementation`. - -## Split-ownership principle - -Feature files own _what_ and _when_ (planning). Code stubs and -production TypeScript own _how_ and _with what_ (implementation). -Neither duplicates the other. - -This split is what lets the kernel state, definitively: - -- A pattern is **identified** by its feature file. -- Production code **realizes** the pattern via - `@architect-implements:` (a relation, not an identity claim). -- Production-TS `@architect-*` annotations are **additive enrichment**, - not mandatory completion criteria. - -## Feature files own (planning) - -| Tag | Purpose | -| ----------------------------- | ---------------------------------------------------------- | -| `@architect-pattern` | Pattern identity (canonical) | -| `@architect-status` | FSM state (`roadmap`, `active`, `completed`, `deferred`) | -| `@architect-bounded-context` | Canonical structural grouping | -| `@architect-uses` | Declared dependency edges for spec, ADR, and test patterns | -| `@architect-implements` | Realization edge (test feature → production pattern) | -| `@architect-executable-specs` | Forward link from design spec to executable feature | -| `@architect-unlock-reason` | Audit-trail for unusual FSM transitions | - -## Code stubs / production TS own (implementation) - -| Tag | Purpose | -| --------------------- | -------------------------------------------------- | -| `@architect-usecase` | When/how to use | -| `@architect-target` | Stub's forward pointer to eventual production path | -| `@architect-decision` | ADR / DD reference (additive) | -| `@architect-role` | Closed implementation-role enum | - -## Code-originated patterns - -Some patterns have no feature file because their canonical definition is the code itself, codecs (renderers), contracts (Zod schemas), and certain utilities. These patterns identify themselves on `.ts` source via `@architect-pattern:` and carry `@architect-role:codec | contract | utility | ...` on the same file. When source owns identity, the same source file also owns identity-coupled metadata such as `@architect-bounded-context` and any `@architect-uses` edges for that pattern. The PatternGraph extractor accepts production-TS identity for these roles. - -Practical rule: `@architect-bounded-context` belongs on the surface that owns canonical identity. For planned behavior patterns, that surface is the feature file. For code-originated patterns, that surface is the `.ts` file carrying `@architect-pattern`. Do not duplicate the tag across both surfaces for the same pattern unless the second copy is an intentionally additive annotation with a different scope. - -## When to use a feature file vs the source for identity - -Use a feature file when the pattern represents planned behaviour, business intent, or a UI/integration outcome — anything where the Gherkin scenarios are part of the pattern's definition. - -Use a `.ts` file when the pattern is purely structural — a contract surface, a serialization codec, a barrel, or a narrow utility — and a feature file would carry no scenarios beyond "the type compiles." - -## Critical: do not duplicate identity - -If a feature file owns identity, do NOT also author `@architect-pattern` on the realising production code. Keep `@architect-bounded-context` and feature-level `@architect-uses` on that owning feature, then use `@architect-implements:` (relation, not identity) on the production file. The feature still owns identity. - -## Production-TS annotations are additive, not mandatory - -A pattern can be `@architect-status:completed` with **zero** -`@architect-*` JSDoc on the production source, provided the executable -feature carries the full surface (pattern identity, status, dependencies, -invariants, scenarios). - -Implications: - -- Value transfer (see [`./value-transfer.md`](./value-transfer.md)) - does NOT require production-TS JSDoc to exist as a precondition for - deletion-readiness — it only requires the executable feature carry - the rule content. -- Annotations enrich discoverability for code-first navigation; they - do not gate completion. -- A reviewer flagging "no annotations on ``" as a value-transfer - blocker is mistaken — refer them here. - -## Sibling references - -- [`./value-transfer.md`](./value-transfer.md) — how this policy feeds - the deletion gate. -- [`./spec-pattern-relationships.md`](./spec-pattern-relationships.md) - — bipartite production↔test pattern graph + the - `@architect-implements` realization edge. -- [`./canonical-references.md`](./canonical-references.md) — - self-containment and anti-anecdote rules. - -## Provenance (informational) - -The split-ownership policy was originally codified in the architect -package's methodology doctrine and is now formalized in -`formal-spec/03-tag-system.md`; the canonical statement for plugin-internal -use lives here in the kernel. Tag definitions and required/repeatable -flags are derived live via `pnpm architect:query taxonomy --format json` -— re-verify against the CLI output rather than against any generated -`.md` if the two ever diverge. diff --git a/.agents/skills/_shared/canonical-references.md b/.agents/skills/_shared/canonical-references.md deleted file mode 100644 index 5e0fbaa..0000000 --- a/.agents/skills/_shared/canonical-references.md +++ /dev/null @@ -1,82 +0,0 @@ -# Self-Contained Kernel (anti-anecdote rule) - -The `_shared/` doctrine kernel is **self-contained**. Every load-bearing -claim lives inside the kernel and is restated, not linked. External -sources may be cited as **provenance** (where the rule was originally -derived from, with a verification date) but never as **authority** — -canonical doctrine lives here, in plain Markdown, in this folder. - -This file anchors two rules: the **anti-anecdote rule** below, and the -**self-containment rule** captured by the structure of every other -`_shared/*.md` file. - -## Anti-anecdote rule - -When you encounter a sample-derived finding (an ad-hoc -session-handoff note, a snapshot folder with a SHA suffix, an -n=2 "we tried this twice and it worked" worklog, or any similar -narrow-sample artifact) that appears to contradict a kernel rule: - -1. **Treat the kernel as correct.** The rule was paraphrased - intentionally; if the sample disagrees, the sample is anecdote. -2. **Treat the sample as anecdote** — useful for understanding why the - rule exists, but not authoritative for what the rule is. -3. If the kernel is silent on a question that the sample addresses, - the sample's finding is **provisional** — flag it for the next - kernel revision rather than encoding it inline. - -This rule keeps doctrine drift bounded. Skill bodies may evolve faster -than the kernel; both may evolve faster than the underlying CLI/MCP and -the architect package's own `docs/` tree. Pin authority to the kernel -and you have one thing to keep right. - -## Self-containment rule - -Every `_shared/*.md` file: - -1. **States its rules inline.** No "see X for the full rule." If a rule - is load-bearing, it lives in the kernel in full. -2. **Cites siblings via relative links** when one kernel doc builds on - another (e.g. `value-transfer.md` builds on `annotation-ownership.md`). -3. **Records provenance, not authority.** A `## Provenance` footer (when - useful) names the external doc the rule was derived from, with a - verification note. The footer is informational; the kernel content - does not depend on the external doc continuing to exist. -4. **Does not paraphrase external docs verbatim** — paraphrase carefully - and adapt to plugin-internal context. Verbatim copies create - review-time false-positive churn when the upstream doc evolves. - -The earlier draft of this file inverted point 4 ("avoid paraphrasing the -canonical source verbatim") in a way that contradicted the -self-containment goal. The corrected rule is above. - -## Provenance (informational, verified at commit time) - -The kernel's content was originally derived from the following sources. -The kernel does not depend on any of them remaining unchanged or even -remaining present. - -- **Tag taxonomy** — derived live via `architect taxonomy --format json` - (CLI output, not a doc). Re-verify with - `pnpm architect:query taxonomy --format json | jq '.root.tags | length'`. -- **Process-Guard FSM transitions** — fully inlined in - [`./fsm-transitions.md`](./fsm-transitions.md). The kernel is the - single source of truth; no external doc dependency. -- **Annotation ownership policy** — derived from the methodology doctrine - practiced across the package family, inlined in - [`./annotation-ownership.md`](./annotation-ownership.md). The kernel - is now the single source of truth for the policy. -- **Rule-block template** — the 4-field convention (`Rule:` / - `**Invariant:**` / `**Rationale:**` / `**Verified by:**`) is fully - inlined in [`./rule-block-template.md`](./rule-block-template.md). -- **Four-tier ladder** — fully inlined in - [`./four-tier-ladder.md`](./four-tier-ladder.md). -- **Refactoring carve-out** — the rule "when backfilling coverage for - code that already exists, skip directly to design or executable tier; - never via plan-level" is inlined in - [`./spec-pattern-relationships.md`](./spec-pattern-relationships.md) - with a parenthetical `formal-spec/08-spec-evolution.md` provenance note. - -If the kernel ever needs deeper background that does not fit a kernel -file, link the external source at the **point of use** with a -verification date — never as a stand-in for inlining the rule itself. diff --git a/.agents/skills/_shared/four-tier-ladder.md b/.agents/skills/_shared/four-tier-ladder.md deleted file mode 100644 index 51e3688..0000000 --- a/.agents/skills/_shared/four-tier-ladder.md +++ /dev/null @@ -1,129 +0,0 @@ -# Four-Tier Ladder (canonical reference) - -Shared reference for every Architect session-typed skill. The ladder is -discriminated by **authored status**, **file location**, and the tier's -required content. `@architect-maturity` is an effective/derived concept and -must not be authored on source. Skills link here instead of inlining the tier -table; that keeps tier rules in one place and prevents the three-skill drift -that prompted this consolidation. - -**Terminology.** "Idea inbox" is the colloquial name for `architect/specs/ideas/` -— the folder that holds idea-tier specs awaiting promotion. "Idea tier" and -"idea inbox" are used interchangeably across the skills and route to the same -planning intent. - -## Tiers - -| Tier | Authored status / location | Folder | Line budget | What this tier adds vs the one above | -| --------- | ------------------------------------------------------------------ | ----------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Idea | `@architect-status:candidate`; idea-tier shape | `architect/specs/ideas/` | **≤30 lines (warn-only)** | User story + 1-3 invariant-only rules. Five authored tags total; structural-variant carve-outs (epic / slice) may add `**Members:**` and `**Usage:**` blocks — see "Epic and slice variants" below. Both still respect the ≤30 budget. Otherwise no `Background:`, no scenarios, no rationale, no verified-by. | -| Candidate | `@architect-status:candidate`; candidate-tier shape | `architect/specs/candidates/` | **30-80 lines** | Adds `**Open Questions:**` block + 1-2 happy-path scenarios. | -| Plan | `@architect-status:roadmap`; deliverables + plan-tier metadata | `architect/specs/` | untyped (150+) | Adds deliverables table, full scenario set, and `**Rationale:**` / `**Verified by:**` on rules. Hierarchy-axis metadata stays on the `@architect-level` / `@architect-parent` pair. | -| Design | `@architect-status:roadmap`; plan-tier shape plus design scaffolds | `architect/specs/` | untyped (300+) | Adds stubs in `architect/stubs//`, error/edge/integration scenarios, ADR refs. | - -## Mandatory tags per tier - -Every tier carries these five authored baseline tags (plus `@architect-level:epic|slice` for those structural variants — see "Epic and slice variants" below). Effective maturity is derived from file location + authored status and must stay off source. Tiers above idea may add metadata tags (`@architect-completed`, `@architect-product-area`) without changing the baseline. See `pnpm architect:query taxonomy` for the live tag set; do not maintain a hand-curated list here. - -The four-tier ladder is the maturity axis. It is independent of the hierarchy axis (`@architect-level`, `@architect-parent`), which expresses epic→phase→task→slice decomposition. A pattern at any maturity tier can be at any hierarchy level. - -1. `@architect` — gate tag -2. `@architect-pattern:` -3. `@architect-status:` — see ladder table -4. `@architect-product-area:` -5. `@architect-parent:` - -## Epic and slice variants - -Idea-tier files that group other patterns or save a multi-pattern view carry `@architect-level:epic` or `@architect-level:slice`. These are **hierarchy-axis** declarations (not maturity-axis); see [`./spec-pattern-relationships.md`](./spec-pattern-relationships.md) §"Hierarchy axis" for the canonical doctrine. The variants relax two baseline rules: - -- **Parent carve-out.** Epics are top-of-chain, slices are views; neither has an `@architect-parent`. The lint and grader both exempt these levels from the parent requirement. -- **7th tag allowed.** `@architect-level` is a structural tag, not idea-tier metadata, so its presence does not violate the "additional tags are a smell" rule. - -Epic file shape: idea template + a human-facing `**Members:**` bullet list naming each member pattern. Slice file shape: idea template + `**Members:**` + a `**Usage:**` line describing the question the slice answers. Both stay within the ≤30-line soft budget. - -## Effective maturity - -`@architect-maturity` remains a derived/effective concept; do not author it on -source. Canonical defaults still live at -`formal-spec/04-tag-registry.md` § "Status → Maturity Defaults", but authoring -guidance now flows through the ladder's file-location/content rules instead of -a source tag. - -Practical effect at idea tier: a file in `architect/specs/ideas/` with -`@architect-status:candidate` is treated as idea-tier. Candidate-tier lives in -`architect/specs/candidates/`; plan/design tiers stay in `architect/specs/` and -are distinguished by their required content and deliverables/stub scaffolding. - -## Valid promotion paths - -``` -idea ──► candidate ──► plan ──► design -``` - -- **Idea → Candidate:** add `**Open Questions:**` + 1-2 happy-path scenarios; `git mv` from `architect/specs/ideas/` to `architect/specs/candidates/`. Status stays `candidate`. -- **Candidate → Plan:** add deliverables table, `**Rationale:**` / `**Verified by:**` on rules, full scenario set, and any retained hierarchy metadata needed for the pattern; bump `@architect-status:candidate` → `roadmap`. Edit in place — no file move. -- **Plan → Design:** add stubs in `architect/stubs//`, error/edge/integration scenarios, ADR refs. Status stays `roadmap` (it transitions to `active` during the implement-spec session, not here). Edit in place. - -Skipping rungs (idea → plan, candidate → design, etc.) is rejected — promote -through every rung. The only exception is the **refactoring carve-out**: when -backfilling coverage for code that already exists, skip directly to design or -executable tier. Never via plan-level. Rule: `formal-spec/08-spec-evolution.md` -§ "Anti-Patterns" ("Exception: Refactoring specs"). - -## Worked example 1 — idea-tier minimum - -Location: `architect/specs/ideas/copilot-context-bundle.feature` - -```gherkin -@architect -@architect-pattern:CopilotContextBundle -@architect-status:candidate -@architect-product-area:editor -Feature: CopilotContextBundle - assemble pattern context for AI agents - - **User Story:** As a developer, I want a single bundle of pattern context - so that my AI agent has the architectural picture without re-reading files. - - Rule: Bundle is read-only and derived from PatternGraph - **Invariant:** Bundle never carries data not already in the graph. -``` - -Five authored tags, one user story, one rule, one invariant. That is the entire shape. -Adding a deliverables table or a scenario here is a smell — it means the idea -is ready to promote, not that the idea-tier file should grow. - -## Worked example 2 — candidate-tier promotion - -Starting from the idea above, promotion produces: - -Location: `architect/specs/candidates/copilot-context-bundle.feature` - -```gherkin -@architect -@architect-pattern:CopilotContextBundle -@architect-status:candidate -@architect-product-area:editor -Feature: CopilotContextBundle - assemble pattern context for AI agents - - **User Story:** As a developer, I want a single bundle of pattern context - so that my AI agent has the architectural picture without re-reading files. - - **Open Questions:** - - Does the bundle include stub content, or only their resolved targets? - - What is the cache key — pattern name alone, or pattern + session intent? - - Rule: Bundle is read-only and derived from PatternGraph - **Invariant:** Bundle never carries data not already in the graph. - - @acceptance-criteria @happy-path - Scenario: Agent requests bundle for a pattern - Given a pattern named "UserService" exists in the graph - When the agent calls the context API with session "design" - Then the bundle includes deliverables, stubs, and dependency tree -``` - -Mechanical changes: file moved `ideas/` → `candidates/`, the -`**Open Questions:**` block was added, and one happy-path scenario was added. -Status stays `candidate`. The acceptance gate is what later flips -`status:candidate` → `status:roadmap` and starts the plan-tier delta. diff --git a/.agents/skills/_shared/fsm-transitions.md b/.agents/skills/_shared/fsm-transitions.md deleted file mode 100644 index 1ace56c..0000000 --- a/.agents/skills/_shared/fsm-transitions.md +++ /dev/null @@ -1,107 +0,0 @@ -# FSM Transitions (canonical reference) - -Shared reference for the Architect PatternGraph's status transitions -and the `@architect-unlock-reason:` audit-trail requirement. Linked -from `architect-implement-spec`, `architect-verify-handoff`, and the -session-router skill (where transitions surface in the bootstrap). - -The kernel splits "transitions" into two categories that are easy to -conflate: - -1. **Process-Guard FSM transitions** — validated by `architect-guard` at - commit time. These are the four-row table below. -2. **Maturity-driven status flips** — driven by spec-authoring sessions - (the four-tier ladder), governed by the acceptance gate, not by - Process Guard. - -Putting both in the same table makes it look like Process Guard -authorizes all of them. It does not. Keep them separate. - -## Process-Guard FSM transitions (validated) - -``` -roadmap ──► active (implement session starts) -roadmap ──► deferred (work parked) -active ──► completed (implementation done, value transferred) -active ──► roadmap (implementation rolled back) -deferred ──► roadmap (work resumed) -completed ──► (none) (terminal — see unlock-reason rule below) -``` - -Notes: - -- `completed` is terminal under the standard rules. Reopening a - completed pattern requires `@architect-unlock-reason:` (see next - section) AND `architect-guard` authorization. -- Skipping rungs (e.g., `roadmap` → `completed` directly) is rejected - unless the unlock-reason mechanism authorizes it. Use - `pnpm architect:query scope-validate ` as the pre-flight - check that catches bad transitions before they fire. -- Verify a candidate transition programmatically with - `pnpm architect:query query isValidTransition ` - — the verb returns a deterministic answer. - -## Maturity-driven status flips (acceptance-gate, not FSM) - -``` -candidate ──► roadmap (acceptance gate cleared during planning) -``` - -This flip is performed by the spec author at the moment the -`@architect-status` tag is bumped from `candidate` to `roadmap` — -typically in `architect-plan-session` when promoting a candidate to the -plan tier. It is NOT validated by Process Guard's transition rules -(Process Guard's table starts at `roadmap`). The acceptance gate is -human judgment plus the four-tier-ladder shape requirements; see -[`./four-tier-ladder.md`](./four-tier-ladder.md) § "Valid promotion paths". - -Treating `candidate → roadmap` as a Process-Guard transition is a -common mistake — surface the distinction when reviewing FSM-related -spec edits. - -## `@architect-unlock-reason:` requirements - -`architect-guard` requires `@architect-unlock-reason:` on -the spec for any unusual transition: - -- Reopening a `completed` pattern (e.g., bug surfaced, behavior - change required). -- Any transition the standard FSM table above does not include. -- Re-completing a pattern that was reopened (the original - unlock-reason should remain alongside a new one). - -Authoring rules (verified against the guard's runtime checks): - -- Minimum length: **10 characters**. Short reasons like `fix` are rejected. -- Cannot be a placeholder: `test`, `xxx`, `bypass`, `temp`, `todo`, - `fixme`. Placeholder values are treated as no unlock reason at all. -- The reason is human-readable, free-text, and shows up in audit - queries (`pnpm architect:query arch blocking`, `pnpm architect:query overview`). - -## Pre-flight: use scope-validate - -Before transitioning a pattern, run: - -```bash -pnpm architect:query scope-validate -``` - -The `` parameter selects the readiness target. The check -returns PASS / WARN / BLOCKED with explicit reasons, including any FSM -transition the requested session would require. - -If `scope-validate` returns BLOCKED with "FSM allows transition: X → -Y is not valid", the Process-Guard transition table above is the -source of truth — promote through the missing rungs first. - -## Provenance (informational, verified at commit time) - -This file is **self-contained** — the FSM transition table, unlock-reason -rules (10-char minimum, placeholder rejection), and the -`query isValidTransition` verb are all canonical here. Verify the verb -live with `pnpm architect:query query isValidTransition roadmap active`; -verify the FSM behavior live with `pnpm architect:query scope-validate -`. No external doc dependency. - -See also [`./canonical-references.md`](./canonical-references.md) for -the kernel's self-containment and anti-anecdote rules. diff --git a/.agents/skills/_shared/rule-block-template.md b/.agents/skills/_shared/rule-block-template.md deleted file mode 100644 index 8f9c874..0000000 --- a/.agents/skills/_shared/rule-block-template.md +++ /dev/null @@ -1,75 +0,0 @@ -# Rule-Block Template (canonical reference) - -Shared reference for the structured `Rule:` block convention used in -both design specs and executable Gherkin. Linked from -`architect-design-session`, `architect-implement-spec`, -`architect-plan-session`, and `architect-review-spec`. - -## Rule blocks are OPTIONAL - -Rule blocks are **not mandatory**. Use them when the feature defines -business invariants that benefit from structured tracking; skip them -for plain behavior verification. Forcing Rule blocks onto features -that aren't invariant-driven adds noise without information. - -A feature whose intent is "verify this UI button shows the right text -in three states" needs scenarios, not invariants. A feature whose -intent is "the planning state machine never allows X → Y without -unlock-reason" is exactly what Rule blocks were designed for. - -## 4-field template (when Rule blocks are used) - -```gherkin -Rule: - - **Invariant:** <1-2 sentence statement of what must always be true> - - **Rationale:** - - **Verified by:** -``` - -The four fields: - -1. **`Rule:` line** — short, descriptive, one rule per Rule block. -2. **`**Invariant:**`** — 1-2 sentences. State the rule, do not - justify it. -3. **`**Rationale:**`** — why the invariant exists. Reference ADRs - or business context. Avoid restating the invariant. -4. **`**Verified by:**`** — comma-separated list of Scenario names - from this Rule block. The back-link from invariant to test. - -## Verified-by is the back-link - -`**Verified by:**` lets a reader (or query) walk from invariant to -the specific scenarios that prove it holds. Renaming a scenario -without updating Verified-by silently breaks this trace — the trace -appears intact but resolves to nothing. - -When you rename a scenario, grep for the old name in `**Verified by:**` -lines and update. - -## Tier guidance - -| Tier | Rule-block fields | -| ---------- | ----------------------------------------------------------------------------------------------- | -| Idea | `**Invariant:**` only — no rationale, no verified-by (no scenarios exist yet) | -| Candidate | `**Invariant:**` only — open questions and 1-2 happy-path scenarios live OUTSIDE the Rule block | -| Plan | All four fields | -| Design | All four fields | -| Executable | All four fields (transferred from the design tier at implement time) | - -For the full tier table see -[`./four-tier-ladder.md`](./four-tier-ladder.md). - -## Sibling references - -- [`./four-tier-ladder.md`](./four-tier-ladder.md) — tier table for - when to add `**Rationale:**` + `**Verified by:**`. -- [`./canonical-references.md`](./canonical-references.md) — - self-containment and anti-anecdote rules. - -## Provenance (informational) - -The 4-field convention is codified in `formal-spec/05-feature-spec-format.md`; -the kernel statement above is the canonical reference for plugin-internal use. diff --git a/.agents/skills/_shared/session-preamble.md b/.agents/skills/_shared/session-preamble.md deleted file mode 100644 index ca9e745..0000000 --- a/.agents/skills/_shared/session-preamble.md +++ /dev/null @@ -1,81 +0,0 @@ -# Universal Session Preamble (canonical reference) - -Six load-bearing rules that apply to every Architect session type -(`-plan`, `-design`, `-implement`, `-review`, `-handoff`, the -`review-implementation` skill, and any campaign-flow skill). They are -not refactor-specific. - -This file is the canonical full text. The full convention for -multi-session campaigns lives in -[`./multi-session-coordination.md`](./multi-session-coordination.md). - -## The six rules - -1. **Data API first, file-based search second.** Every pattern-related - question goes through `mcp__architect__*` MCP tools or - `pnpm architect:query` (see - [`../architect-data-api/SKILL.md`](../architect-data-api/SKILL.md) for the - verb-by-verb reference) before any `Read` / `Glob` / `Grep`. This is - a discipline, not an enforced gate — the Data API is faster - (sub-ms for MCP, 2–5s for CLI) and more accurate than file scanning. - -2. **Gates are non-negotiable.** The session's `Gates` block is a - complete list, every command runs, a failing gate is - stop-and-surface. No silencing, no mocking, no `--no-verify`. - Validation cadence: run `pnpm typecheck` between phases and - `pnpm typecheck && pnpm test && pnpm validate:all` before any - commit or handoff. Include **all** runtime package test suites - — `pnpm test` filters every `packages/*`, so partial gate coverage - that omits any package allows pre-existing failures to spill into - a later session. - -3. **Commit hygiene.** `chore(scope): imperative summary`; commit body - references issue ids when relevant - (`Closes P0-1, P0-2 from /CONFIRMED-ISSUES.md`); - never `git add -A` on a multi-commit refactor branch (sweeps WIP - into commits). - -4. **Decisions captured before code.** Items needing human judgment go - to `DECISIONS.md` with question / options / recommendation / - consumed-by-session. Without this separation, agents fabricate - answers under pressure. - -5. **Incomplete scope is next-session input, not silent debt — do - not follow the prompt blindly.** When investigation surfaces - drift mid-session, stop and classify: _same-root-cause_ (apply - inline + record) vs _different-root-cause_ (defer + record in - `DECISIONS.md` or the learnings log). Never land a surface-only - commit that leaves gates red. Full classification heuristic and - the entry templates live in - [`./multi-session-coordination.md`](./multi-session-coordination.md); - this is the single most-reused heuristic across multi-session - work. - -6. **Per-session learnings propagate forward.** After each session - the coordinator appends one tight entry to the learnings log and - rewrites the _unstarted_ session prompts' "Scope discipline" - sections with newly-discovered rules. Preambles are calibrated - against real surprises from prior sessions, not boilerplate. - -## When this file is loaded - -Skills loading this preamble explicitly pin the rule set so a session -prompt does not drift from the kernel between commits. This file -covers any session that is part of (or could become part of) a -campaign. - -## Sibling references - -- [`./multi-session-coordination.md`](./multi-session-coordination.md) - — full coordination convention (folder layout, coordinator + worker - split, DECISIONS / learnings templates, scope-discovery rule). The - six rules above are the floor; that file builds the campaign-level - discipline on top. -- [`./canonical-references.md`](./canonical-references.md) — kernel - self-containment + anti-anecdote rule. -- [`./four-tier-ladder.md`](./four-tier-ladder.md) — tier table - referenced by Rule 5 (incomplete scope often surfaces a missing - rung). -- [`./fsm-transitions.md`](./fsm-transitions.md) — referenced by - any session that touches `@architect-status` (covered by Rules 1 - and 2: read state via Data API, never bypass guard). diff --git a/.agents/skills/_shared/spec-pattern-relationships.md b/.agents/skills/_shared/spec-pattern-relationships.md deleted file mode 100644 index d6513b4..0000000 --- a/.agents/skills/_shared/spec-pattern-relationships.md +++ /dev/null @@ -1,136 +0,0 @@ -# Spec ↔ Pattern Relationships (canonical reference) - -Shared reference for the bipartite production↔test pattern graph and -the sanctioned naming conventions. Linked from -`architect-design-session`, `architect-implement-spec`, -`architect-plan-session` (escape-hatch case), and -`architect-review-implementation`. - -## The bipartite pattern graph - -Every production pattern can have a corresponding test pattern that -`implements` it. The PatternGraph carries both as nodes joined by an -`@architect-implements:` edge. - -A test feature carries two file-level tags: - -```gherkin -@architect-pattern:DefineConfigExecutableTests -@architect-implements:DefineConfig -``` - -`@architect-pattern:DefineConfigExecutableTests` declares the test feature as -its own pattern with a distinct name. `@architect-implements:DefineConfig` -declares the realization edge to the production pattern. - -This two-tag shape is what lets queries traverse: "show me the -executable test for `DefineConfig`" walks `implements` edges from the -production pattern node to its test pattern node. - -## Naming conventions for test patterns - -Two suffix conventions are sanctioned: - -| Suffix | Use case | -| ------------------ | ----------------------------------------------------------------------------------------------------------------------------- | -| `*Testing` | Test pattern accompanying a deliberately-designed pattern (the pattern flowed through plan / design before being implemented) | -| `*ExecutableTests` | Test pattern backfilling coverage for code that already ships (the formal escape hatch — see below) | - -Either suffix is acceptable; pick whichever conveys intent better in -context. The PatternGraph treats them identically — the suffix is a -human-facing convention. - -## Forward / reverse link pair (deletion-gate input) - -Two tags form the deletion-gate link pair: - -- **Forward:** the design spec carries - `@architect-executable-specs:` pointing at the eventual - executable feature file. -- **Reverse:** that executable feature carries - `@architect-implements:` declaring the realization edge - back to the focal pattern. - -Both must exist and resolve to each other for the design spec to be -safely deletable. See [`./value-transfer.md`](./value-transfer.md) -§"Pre-deletion gate" for the full gate criteria. - -## `*ExecutableTests` as the formal escape from retroactive plan-level specs - -The kernel flags **retroactive plan-level specs** as a load-bearing -anti-pattern: authoring a fresh plan-level spec for code that already -ships inverts the spec lifecycle. The formal escape is the -`*ExecutableTests` convention: - -1. Author a `tests/features/**/*executable-tests.feature` (or sibling) - file. -2. File-level tags: - `@architect-pattern:ExecutableTests` plus - `@architect-implements:`. -3. Enrich the file's Rule blocks with `**Invariant:**` - (+ `**Rationale:**` / `**Verified by:**` where useful) describing - what the existing code already guarantees. - -This produces graph visibility for the shipped pattern without -authoring a fictitious "planned" design spec that would immediately -become a zombie. - -## Refactoring carve-out - -When backfilling coverage for code that already exists, **skip -directly to design-tier or executable-tier authoring** — never via -idea, candidate, or plan tier. The `*ExecutableTests` convention above -is the executable-tier carve-out; the design-tier carve-out applies -when stub-level scaffolding is genuinely useful for the refactor (e.g., -extracting a new abstraction). - -(Provenance: the carve-out rule was originally codified in -`formal-spec/08-spec-evolution.md` § "Anti-Patterns" ("Exception: -Refactoring specs") in the formal Architect Spec; the kernel -statement above is the canonical reference for plugin-internal work.) - -## Hierarchy axis (epic / phase / task / slice) - -Patterns can be organized into a hierarchy independent of their -maturity. The hierarchy axis carries exactly two authored tags: -`@architect-level` and `@architect-parent`. - -The hierarchy axis uses two tags: - -- `@architect-level:` — declares this - pattern's level in the hierarchy. -- `@architect-parent:` — declares the parent edge to - another pattern. - -Constraints: - -- The level enum is closed: `epic > phase > task > slice`. -- `@architect-parent X` requires `X` to carry `@architect-level` at - a strictly-higher level than the file declaring the parent. - (`task`'s parent is `phase` or `epic`; `slice`'s parent is `task`.) -- A pattern at any maturity tier (idea / candidate / plan / design / - executable) can be at any hierarchy level. Hierarchy and maturity - are independent. -- Cross-package parents resolve via the same `uses`-resolver that - handles cross-package dependencies. Keep the authored form on the - `@architect-parent` edge and let the resolver classify the target. -- Epics and slices are top-of-chain or lateral views and do not - carry `@architect-parent`. - -## Sibling references - -- [`./value-transfer.md`](./value-transfer.md) — full deletion-gate - criteria. -- [`./annotation-ownership.md`](./annotation-ownership.md) — - split-ownership policy that makes the executable feature canonical. -- [`./canonical-references.md`](./canonical-references.md) — - self-containment and anti-anecdote rules. - -## Provenance (informational) - -`@architect-implements`, `@architect-executable-specs`, and -`@architect-pattern` tag formats are derived live via -`pnpm architect:query taxonomy --format json`. The `*ExecutableTests` -and `*Testing` suffix conventions originated in the package family's -executable-coverage pattern doctrine; the statement above is the -canonical form. diff --git a/.agents/skills/_shared/value-transfer.md b/.agents/skills/_shared/value-transfer.md deleted file mode 100644 index 3fe6d40..0000000 --- a/.agents/skills/_shared/value-transfer.md +++ /dev/null @@ -1,150 +0,0 @@ -# Value Transfer (canonical reference) - -Shared reference for the terminal phase of the spec lifecycle: how value -moves out of an ephemeral design spec into durable surfaces, and what -makes a design spec safe to delete. Linked from -`architect-implement-spec` and `architect-review-implementation`. The -existing `architect-implement-spec` skill body is the operational -counterpart — this doc carries the doctrine. - -## Concept - -Design-level specs and stubs are **scaffolds, not permanent -documentation**. Once implementation completes, the spec's value must -transfer to surfaces that survive the spec's deletion. The durable -artifacts are: - -1. **Executable Gherkin** in `tests/features/**/*.feature` — the - primary carrier. Carries pattern identity (`@architect-pattern`), - the realization edge (`@architect-implements:`), status, - dependencies, business invariants (Rule blocks), and scenarios that - prove the invariants hold. -2. **JSDoc `@architect-*` annotations on production code** — additive - carrier. Carries technical wiring (`@architect-uses` when the target - resolves to a declared pattern), "when to use" guidance - (`@architect-usecase`), implementation classification - (`@architect-role`, `@architect-bounded-context`), decision links - (`@architect-decision`), and any architectural `**Rationale:**` - content that doesn't belong in Gherkin. - -## The primary durable artifact is the executable feature file - -Per the split-ownership policy in -[`./annotation-ownership.md`](./annotation-ownership.md), the `.feature` -file is the **canonical pattern definition**. Production-TS JSDoc -annotations are **additive, not mandatory** — sampled completed patterns -(`ConfigLoader`, `DefineConfig`) carry zero `@architect-*` JSDoc on the -production source and are still legitimately complete because the -executable feature carries the full surface. - -The maximalist framing "value must transfer to BOTH surfaces" (executable -Gherkin + JSDoc annotations) is a useful default goal, but it is **not** -the deletion gate. The actual gate is in the **Pre-deletion gate** section -below; the split-ownership policy in `annotation-ownership.md` is the -authority for which surface is mandatory vs additive. - -## Transfer checklist - -| From (ephemeral) | To (durable carrier) | -| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -| Plan-level rule with invariant | `Rule:` block in `tests/features/**/*.feature` carrying `**Invariant:**` (+ `**Rationale:**` + `**Verified by:**` at plan tier) | -| Stub's "When to Use" comment | `@architect-usecase` JSDoc on the implementation (additive) | -| Stub's DD-N decision | `@architect-decision:DD-N` JSDoc referencing the ADR (additive) | -| Design Scenario | Executable `Scenario:` block in `tests/features/` | -| Scenario without a production-code home | Executable scenario alone — no annotation target exists | -| Architectural rationale | Either Gherkin Rule block `**Rationale:**` OR JSDoc free text — pick whichever is more discoverable for the reader | -| Deliverables list | Verified by test coverage + (where annotations exist) `@architect-target` resolution | - -For the bipartite production↔test pattern naming convention (test -patterns carry `@architect-pattern:Testing` or -`ExecutableTests`) see -[`./spec-pattern-relationships.md`](./spec-pattern-relationships.md). -For the optional 4-field Rule template see -[`./rule-block-template.md`](./rule-block-template.md). - -## Anti-patterns (stop) - -- **Zombie design spec.** Leaving a design-level spec in - `architect/specs/` after implementation completes. The spec is - scaffolding; once the building stands, the scaffolding comes down. -- **Half-transferred value.** Transferring rules to executable specs - but not to annotations (or vice versa) where both surfaces should - carry weight. Note: annotations are additive, so transfer to - executable Gherkin alone is often sufficient — apply this anti-pattern - only when both surfaces are genuinely required. -- **Retroactive plan-level spec.** Authoring a fresh design or - plan-level spec for code that already ships. Ephemeral specs describe - _planned_ work — conjuring one back to "cover" shipped behavior - inverts the pipeline. Use the `*ExecutableTests` escape hatch in - [`./spec-pattern-relationships.md`](./spec-pattern-relationships.md). - -## Pre-deletion gate - -A design spec is safe to delete only when **all** of these hold: - -1. **Forward link present.** The design spec carries - `@architect-executable-specs:`. -2. **Forward link resolves.** The path points at a real file under - `tests/features/`. -3. **Reverse link present.** That target feature carries - `@architect-implements:` for the focal pattern. -4. **Rich content has landed.** Every Rule block in the design spec - has a counterpart Rule block in the executable feature carrying - `**Invariant:**` (and, where present in the source, - `**Rationale:**` + `**Verified by:**`). -5. **Architecturally significant rationale lives in JSDoc** for any - production code where the rationale won't fit in Gherkin (judgment - call — annotations are additive). - -When all five hold, deletion is safe. When any fails, fix that surface -before deletion. - -## Mechanical check (when shipped) - -The candidate spec -`architect/specs/value-transfer-state.feature` -proposes: - -- A CLI verb `pnpm architect:query value-transfer ` returning the - per-pattern state (`designSpecPath`, `executableSpecPaths`, - `annotatedSourcePaths`, `forwardLink`, `reverseLinks`, `antipatterns`, - `deletionReady`, `transferComplete`). -- An MCP tool `architect_value_transfer` with the same input shape. -- Composition into `ArchitectBriefDeterministicBundle` so every - session-open brief surfaces anti-patterns as graph-derived ground - truth. - -Until that ships, the manual checklist above is the gate. After it -ships, `architect-implement-spec` and `architect-review-implementation` -will gate `git rm` on `deletionReady === true`. - -## Deletion timing - -The implementer **asks the user** before deleting: - -- **Delete now** — appropriate when the implementation session reviews - the value transfer thoroughly and the pattern is the only one being - reviewed. -- **Defer to code review** (more common) — appropriate when several - related implementations are being reviewed together. The reviewer - batches the spec deletions in a single PR or review pass, after - verifying value transfer across the related set. The - `architect-review-implementation` skill is the canonical owner of - batched deletion. - -Default behavior: **ask, don't auto-delete**. - -## Sibling references - -- [`./annotation-ownership.md`](./annotation-ownership.md) — - split-ownership policy that makes the executable feature canonical. -- [`./spec-pattern-relationships.md`](./spec-pattern-relationships.md) - — bipartite production↔test pattern graph + `*ExecutableTests` - escape hatch. -- [`./canonical-references.md`](./canonical-references.md) — - anti-anecdote and self-containment rules. - -The `value-transfer-state.feature` candidate spec referenced above -(`architect/specs/value-transfer-state.feature`) is -an Architect-internal pointer to the in-progress mechanization of this -gate, not an external doc — keep the reference. diff --git a/.agents/skills/architect-base/SKILL.md b/.agents/skills/architect-base/SKILL.md new file mode 100644 index 0000000..44ef6ca --- /dev/null +++ b/.agents/skills/architect-base/SKILL.md @@ -0,0 +1,336 @@ +--- +name: architect-base +description: MANDATORY first-load for any work in this Architect repo. Shared vocabulary every other skill assumes. Covers what Libar Architect is, the PatternGraph and `@architect-*` tag taxonomy, the four authored tiers plus executable and maintenance levels, the FSM lifecycle, value-transfer doctrine, and the key ADRs. Load it before any architect-scoped Read/Glob/Grep and before any other architect-* skill, whenever work touches Architect, the architect package family, specs/stubs, `pnpm architect:q`, an `architect_*` MCP tool, or a session intent (plan/design/implement/review/refactor/handoff). Does not cover per-session execution detail. That routes to the session skills. +allowed-tools: + - Bash + - Read + - Glob + - Grep +--- + +# Architect base context + +Operational baseline for every session in this Architect repo. Self-contained. No other architect-\* skill has to load first. + +When you load this skill, state briefly that the **architect-base** context is loaded so the user can confirm it activated. + +## 1. What Libar Architect is + +A source-first reliability layer for agentic engineering and end-to-end software delivery. Architect manages the full lifecycle as typed, queryable, managed-as-code process state: requirements, design, architecture, implementation, maintenance. + +Two things live in this repo. + +**The product.** The `@libar-dev/architect-*` package family. + +**The delivery process.** This repo runs that toolchain on itself to plan, design, implement, and review its own work. + +Same source of truth, two audiences. + +**Agents and humans doing the work.** `pnpm architect:q` is the agent read entry point (ADR-014). It hands you Graph, the frozen handle over the live PatternGraph, plus `architect_*` MCP tools, task-oriented context, and FSM-validated transitions. + +**Outputs that consume the projection.** Generated documentation, Architect Studio's view state, architecture-review context, release notes, change logs. + +Annotated production code plus executable Gherkin (`tests/features/`) is the source of truth. Everything else is a projection. + +## 2. The delivery process in this repo + +| Aspect | Value | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Config | `architect.config.ts` at the repo root | +| Working state | `architect/` (specs, decisions, stubs, step-stubs) | +| Source of truth | Annotated `packages/*/src/**/*.ts` + executable Gherkin under `tests/features/` and `packages/*/tests/features/` | +| CLI | `pnpm architect:q ''` (agent read entry point, ADR-014; Graph is the frozen handle) + `pnpm architect:graph ` (named demos + the dangling gate) | +| MCP | `architect` server → `mcp__architect__*` callable tools | +| Validation entry | `pnpm typecheck`, `pnpm test`, `pnpm validate:all`, `pnpm architect:guard --staged` | +| Doc regeneration | `pnpm docs:all` → `docs-live/` (git-tracked, derived. Determinism-gate diff target); `pnpm docs:check` verifies idempotency in place (re-renders, diffs the working tree, writes nothing, non-zero on drift). Usable mid-changeset, where `git diff --exit-code` can't tell an uncommitted edit from a non-deterministic generator | + +When another project consumes this package family, it wires its own `architect.config.ts` and exposes its own `architect:q` / `architect:graph` scripts over the `architect` bin. Those contracts stay stable across architect-managed repos. + +## 3. Architect state, what lives where + +`architect/` holds **working state**, not the source of truth. `@cucumber/gherkin` parses it for projection and extraction. TypeScript compile, ESLint, and vitest exclude it. + +| Folder | Role | Lifetime | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +| `architect/ideations/` | Dated pre-idea ideation / context captures (`YYYY-MM-DD-*.feature`). Parsed working state, distilled into ideas/candidates | Until distilled | +| `architect/specs/ideas/` | Idea-tier specs (lightest authored shape) | Until promotion | +| `architect/specs/candidates/` | Candidate-tier specs (open questions + 1-2 scenarios) | Until promotion | +| `architect/slices/` | Slice-tier multi-pattern lateral views (idea-tier structural variant; `@architect-level:slice`, no `@architect-parent`) | Reference | +| `architect/specs/` | Plan- and design-tier specs (deliverables + full scenarios + stubs) | **Until value transferred to executable Gherkin, then deleted** | +| `architect/stubs/` | Design-tier TS contract stubs (one folder per pattern) | Ephemeral | +| `architect/step-stubs/` | Design-tier stub step definitions | Ephemeral | +| `architect/decisions/` | ADRs / PDRs. Compact, durable, decisions-only (no operational or temporal context) | **Permanent** | + +**Two Gherkin parsers. Don't mix them up.** + +- `@cucumber/gherkin` reads `architect/specs/`, `architect/decisions/`, `formal-spec/` at doc-gen + pattern-graph build time. +- `@amiceli/vitest-cucumber` reads executable specs (`tests/features/`, `packages/*/tests/features/`) at test time. + +## 4. PatternGraph, the read model + +PatternGraph is the read model (ADR-006). A **pattern** is a named architectural unit: a feature, service, component, contract, codec, or spec. Nodes are patterns. Edges are typed relationships. + +**Tag taxonomy** (canonical enumerated set: the generated `docs-live/TAXONOMY.md`): + +- **Identity.** `@architect-pattern:` (one file owns identity) +- **State.** `@architect-status:`; `@architect-maturity` derives from status (idea=consideration, plan=delivery) and an explicit value wins (§04). Explicit is **required only at the idea tier** (`@architect-maturity:idea`, the guard's opt-in), dropped on promotion to candidate, derived elsewhere +- **Structure.** `@architect-bounded-context:`, `@architect-role:` +- **Product.** `@architect-product-area:` (PRD grouping; **required** at idea tier) +- **Edges.** `@architect-uses:` (dependency), `@architect-implements:` (realization, test → production), `@architect-parent:` (hierarchy) +- **Hierarchy axis.** `@architect-level:` (independent of maturity) +- **Implementation enrichment** (on production TS). `@architect-usecase`, `@architect-enforces-decision:` (the structured pattern→ADR edge, distinct from `@architect-decision`, which is a doc-aggregation tag, not this), `@architect-target` (stub forward pointer) +- **Forward link.** `@architect-executable-specs:` (design spec → executable feature) +- **Audit.** `@architect-unlock-reason:` (optional advisory-warning suppressor for completed reopen/edit, and a required marker only for genuinely non-standard transitions) + +> **Depth.** The categories above are the conceptual model. The three orthogonal classification axes (role · bounded-context · layer) and the csv-vs-colon authoring rules live in [`references/taxonomy.md`](references/taxonomy.md). The **complete enumerated set is generated, never hand-maintained.** Read the generated `docs-live/TAXONOMY.md` (regenerate via `pnpm docs:all`). That file is canonical. The categories here teach the shape. They do not enumerate it. + +**Instances** of patterns live in two owners: + +- `.feature` files (canonical for behavioral patterns), tags at the feature level +- `.ts` files (canonical for code-originated patterns: codecs, contracts, utilities), JSDoc `@architect-*` blocks + +**Edges.** `depends-on` / `uses` / `implements` / `see-also` / `parent`. + +**Projections** are Zod-validated **Named Domain Fragments** (`@libar-dev/architect-projection`). The same graph projects into markdown, JSON, context bundles, architecture views, release notes. Fragments are the trust boundary. Anything outside a fragment is anecdote. + +Annotations on this graph are curated. Editorial sparsity is the point. Do not add tags to hit a coverage quota. + +## 5. Entry points + +- **`architect.config.ts`.** Config loader: taxonomy customization, source globs, validation rules. +- **`pnpm architect:q ''`.** Agent read entry point (ADR-014). Builds the live PatternGraph and hands you Graph, the frozen handle. Script the cut, get the conclusion. This is the default. Use it. +- **`architect_*` MCP tools.** Sub-ms per call, snake_case end to end (`architect_scope_validate`, not `architect_scope-validate`). Reach for MCP when bursting ≥5 tools in close sequence, or when Studio is the sink. +- Scanning architect-scoped files to learn pattern state is a smell. "What's the status of X?" is one `architect:q` script. + +## 6. Validation layers + +| Layer | Command | What it checks | +| --------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------ | +| Type system | `pnpm typecheck` | Strict TS (see CLAUDE.md "TypeScript strictness") | +| Annotation lint + DoD | `pnpm validate:all` | Definition-of-done, anti-patterns, dangling references | +| Process Guard (FSM) | `pnpm architect:guard --staged` | FSM transitions, `@architect-unlock-reason` rules, structural invariants | +| Graph integrity | `pnpm architect:graph dangling --baseline --strict` | Cross-pattern reference drift | + +CI enforces all of these. A failing gate stops the work. Never `--no-verify`. + +## 7. Key decision records (load-bearing, decisions-only) + +ADRs / PDRs in `architect/decisions/` are **permanent and decisions-only**. They record a decision plus its rationale, and only durable facts that are not about execution. Status, work-in-progress, ETAs, who is doing what this week: none of that belongs here. That is the difference between a decision record and a worklog. + +Post-1.0, a decision is amended by a **new** ADR, never by editing the old one. During bootstrap (pre-1.0, live-state), records are consolidated **in place**: edit, slim, or delete directly. No amend-chains, and no supersedes / superseded-by edges. Those edges manufacture the history the read model excludes. History lives in git. See [`references/decision-records.md`](references/decision-records.md) §"Amendment rule" and the repo bootstrap doctrine. + +Read the relevant record before changing anything in its area. Use the `.feature` file itself, or `pnpm architect:q 'g.pattern("ADR006SingleReadModelArchitecture")'`. Never paraphrase from memory. + +The load-bearing set: + +- **ADR-003.** Source-First Pattern Architecture +- **ADR-005.** Codec / Renderer Separation +- **ADR-006.** Single Read Model +- **ADR-007.** Coordinated Taxonomy Redesign +- **ADR-009.** Projection Trust Boundary +- **ADR-014 Agent Read Surface** + +> **Not the same as a campaign `DECISIONS.md`.** `architect/decisions/` holds **durable** ADRs (permanent). A campaign's `.pr-coordination/DECISIONS.md` holds **ephemeral** judgment-calls for one active campaign (resolved-with-commit-sha, then archived). Both are called "decisions" and have opposite lifetimes. Do not file durable architecture in the campaign log, or campaign bookkeeping in an ADR. +> +> **Depth.** [`references/decision-records.md`](references/decision-records.md). + +## 8. Annotation ownership + +**Split-ownership principle.** + +- Feature files own **what + when** (planning). +- Production TS owns **how + with what** (implementation). +- Neither duplicates the other. + +A pattern is **identified** by exactly one owner: the feature file for behavioral patterns, the `.ts` file for code-originated patterns (codecs, contracts, utilities). Production TS realizes a feature-owned pattern via `@architect-implements:`, a relation, not an identity claim. A code/contract **stub** in `architect/stubs/` is itself a code-originated owner, so it carries its **own** distinct `@architect-pattern` (plus `@architect-implements`/`@architect-target`). That identity then travels with the code to `src/`. The names differ, so this is not duplication. The lone stub exception is a **step-definition** stub (`architect/step-stubs/`), which never carries `@architect-pattern` (ADR-008). Full split in [`references/annotation-ownership.md`](references/annotation-ownership.md). + +**Production-TS `@architect-*` JSDoc is additive, not mandatory.** A pattern can be `@architect-status:completed` with zero `@architect-*` JSDoc on its source, provided the executable feature carries the full record (identity, status, deps, invariants, scenarios). Annotations are curated, not a coverage quota. They enrich discoverability and do not gate completion. + +A completed, **feature-identity-owned** pattern carries no `@architect-*` identity JSDoc on its realizing production `.ts` at all. Identity, status, deps, and invariants live entirely on its `.feature`. Confirm the current set live rather than trusting a frozen name (samples rot, §16): `pnpm architect:q 'g.patterns.filter(p => p.status === "completed").map(p => [p.name, p.sourceFile])'` (a feature-owned pattern's `sourceFile` is its `.feature`). A reviewer flagging "no annotations on ``" as a value-transfer blocker is mistaken. + +> **Depth.** The per-tag ownership tables (what feature files own vs what production TS owns) plus the code-originated-identity rules live in [`references/annotation-ownership.md`](references/annotation-ownership.md). + +## 9. Detail tiers and maturity levels + +There are **six** levels along the detail/maturity axis. Four are authored in `architect/specs/`. Two are post-spec. + +| Level | Where | What it adds vs the level above | +| ----------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Idea | `architect/specs/ideas/` | User story + 1-3 invariant-only rules; **≤30 lines soft cap** | +| Candidate | `architect/specs/candidates/` | `**Open Questions:**` block + 1-2 happy-path scenarios | +| Plan | `architect/specs/` | Deliverables table, full scenario set, `**Rationale:**` / `**Verified by:**` | +| Design | `architect/specs/` | Stubs in `architect/stubs//`, error/edge/integration scenarios, ADR refs | +| Executable | `tests/features/`, `packages/*/tests/features/` | Realization (`@architect-implements:`) + executable scenarios that prove invariants hold | +| Maintenance | Shipped code + its executable feature | Evolves in place; scenarios grow as behavior grows | + +**Promotion is linear.** `idea → candidate → plan → design → executable`. Skipping rungs is rejected. (The one non-spec-driven exception, backfilling shipped code that has no spec, lives in [`architect-refactor-session`](../architect-refactor-session/SKILL.md), not this spec-driven ladder.) + +> **Depth.** The per-tier line budgets, mandatory-tag sets, epic/slice variants, and worked promotion examples live in [`references/four-tier-ladder.md`](references/four-tier-ladder.md). The 4-field `Rule:` block convention (`Invariant` / `Rationale` / `Verified by`) and its per-tier field requirements live in [`references/rule-block-template.md`](references/rule-block-template.md). + +## 10. The detail-level doctrine + +**Tier line budgets and field requirements are floors and soft caps, not formulaic quotas.** Detail at idea, plan, and design is contextual. The executor judges it. + +Two failure modes to refuse: + +- **Bloat to satisfy the form.** Adding deliverables, stubs, full design scenarios, ADR refs for the 50th instance of an established pattern, a CRUD endpoint, an industry-standard piece of work. Detail you don't need is detail that will rot. +- **Strip context to match the tier.** Truncating real, hard-won session context at the end of planning or design because "we're only at idea / plan tier." Precious nuance gets destroyed in service of the form. + +**Both fail the goal.** Author what is meaningful for THIS pattern in THIS context: + +- **Invest detail** when the work is architecturally significant, non-routine, sensitive (security / data privacy / 3rd-party integration / public-facing), requires external approval, or is context-critical. +- **Skip detail** when the pattern is the Nth instance of a well-understood shape, a CRUD endpoint, or an industry-standard piece with no novel decisions. + +Design-level specs do not always need stubs and full design details. Idea-tier specs are not required to be terse. Use judgment. Too much content is worse than not enough. Both extremes erode the signal. + +**The skip-detail cases are a reviewable smell.** The "skip detail" list above, the Nth instance of an established shape, a CRUD endpoint, an industry-standard piece with no novel decisions, is Architect's standing decision about where prose adds nothing. Re-explaining those shapes, or re-deriving a pattern already defined elsewhere, is a **flaggable redundancy** at spec review ([`../architect-sessions/references/review-spec.md`](../architect-sessions/references/review-spec.md)), not something left to per-session memory. This does **not** narrow the "invest detail" half: deliberate depth on architecturally significant, sensitive, or novel work is design judgment and is never trimmed by this rule. The gate enforces a decision §10 already made. It does not make a new one. + +## 11. FSM lifecycle + +``` + ┌─ (maturity flip, human acceptance gate, not process-guard) + │ +candidate ──┴──► roadmap ──► active ──► completed + │ ▲ │ │ + ▼ │ │ └──► active (advisory reopen) + deferred └────────► roadmap (advisory reopen) +``` + +`deferred` hangs off **`roadmap`**, not `active`. `roadmap ⇄ deferred` is the only deferred edge (`active → deferred` is rejected). `active → roadmap` is the back edge (see below). + +- `candidate → roadmap` is a **maturity flip** (acceptance gate, human judgment). Not a process-guard transition. +- `roadmap → active`, `active → completed`, `active → roadmap`, `roadmap → deferred`, `deferred → roadmap`, `completed → active`, and `completed → roadmap` are process-guard-validated. Invalid jumps are rejected. +- Reopening completed work is **advisory**, not blocked. `@architect-unlock-reason:<≥10 char, not a placeholder>` is optional and suppresses the warning. + +Verify any transition before flipping: + +```bash +pnpm architect:q 'g.fsm.isValidTransition("", "")' # deterministic boolean +# scope-readiness (PASS/WARN/BLOCKED) remains available as the `architect_scope_validate` MCP tool +``` + +> **Depth.** The process-guard transition table, the maturity-flip-vs-FSM distinction, and the `@architect-unlock-reason:` authoring rules live in [`references/fsm-transitions.md`](references/fsm-transitions.md). + +## 12. Spec and pattern relationships + +Production patterns and test patterns are **two nodes** joined by `@architect-implements:`. A test feature carries two file-level tags: + +```gherkin +@architect-pattern:DefineConfigExecutableTests +@architect-implements:DefineConfig +``` + +Two sanctioned suffix conventions: + +- `Testing`. Test pattern accompanying a deliberately designed pattern (flowed through plan / design). +- `ExecutableTests`. Test pattern backfilling shipped code (the formal escape from retroactive plan-level specs). + +Epics and slices are durable, edge-derived navigation nodes. Any prose `**Members:**` list is human-facing orientation only. The authoritative member set is derived from reverse `@architect-parent` edges and persists after member design specs are deleted. + +PatternGraph treats both suffixes the same. The suffix is only a human-facing label. + +> **Depth.** The forward/reverse link pair, the `*ExecutableTests` escape-hatch authoring flow, and the hierarchy axis (`@architect-level` / `@architect-parent`) live in [`references/spec-pattern-relationships.md`](references/spec-pattern-relationships.md). + +## 13. Value transfer and design-spec deletion + +**Deletion is not loss.** It is cleanup of a redundant copy _after_ its value has moved. A design-level spec is a **temporary working copy, not permanent documentation**. Once implementation completes, every piece of its value has a durable home, and only then is the now-duplicated spec removed. Nothing valuable is destroyed. "What did we delete?" is a `git log` question. History lives in git. + +The three working copies and where each one's value goes: + +- **Design-level `.feature` spec** → invariants move to **executable Gherkin** (`tests/features/`, canonical) + rationale to JSDoc; then the `.feature` is **deleted**. +- **Step-definition stubs** (`architect/step-stubs/`) → become the executable feature's real step wiring; then **deleted**. +- **Code/contract stubs** (`architect/stubs/`) → **promoted to `src/`** as a code-originated pattern: their `@architect-pattern` identity **persists** (it travels with the code per ADR-003; `@architect-status` advances `roadmap` → `completed`). The staging copy is removed. The pattern is **not** discarded. + +Durable carriers (where the value lands): + +- **Executable Gherkin** (canonical). Pattern identity, status, dependencies, invariants, scenarios that prove them. +- **Production code + its `@architect-*` JSDoc** (additive). A promoted code/contract stub's contract shape and identity, plus rationale that doesn't fit in Gherkin (decisions, usecases, roles). + +**Pre-deletion gate (high level).** Forward link present + resolves; reverse link present; all Rule blocks with invariants have counterparts in the executable feature. + +**Default.** Ask the user before deleting. Deferring to code review for batched deletion across a related set is more common than delete-immediately. + +> **Depth.** The transfer checklist, the five-criterion pre-deletion gate, and deletion timing live in [`../architect-sessions/references/ephemeral-spec-deletion.md`](../architect-sessions/references/ephemeral-spec-deletion.md). That is the central doctrine every session type should understand. + +## 14. ADR-014 Agent Read Surface + +Agent read entry point: **`pnpm architect:q`**. It builds the live PatternGraph and hands you Graph, the frozen handle `g`. Script the cut. The conclusion comes back. Reusable read algorithms stay named pure core functions, not new Graph methods. Full recipes and quirks live in the dedicated `architect-graph-handle` skill. Load it before real use. The essentials: + +```bash +# Health / inventory / orientation +pnpm architect:q 'g.graph.counts' # status distribution +pnpm architect:q 'g.patterns.filter(p => p.status === "active")' # active work +pnpm architect:graph census # curation candidates per package +pnpm architect:q 'g.findByConcept("taxonomy").slice(0,5)' # fuzzy concept → patterns + +# Per-pattern detail +pnpm architect:q 'g.pattern("")' # need-shaped node (status, edges, maturity) +pnpm architect:q 'g.graph.patterns.find(p => p.name === "")' # full canonical record +pnpm architect:q 'g.invariantsOf("")' # what it guarantees, exec vs authored +pnpm architect:q 'g.specsReverifying([""])' # what re-verifies if it changes + +# Gates (deterministic) +pnpm architect:q 'g.fsm.isValidTransition("","")' # FSM boolean +pnpm architect:graph dangling --baseline --strict # non-zero exit on drift (the CI gate) + +# Impact / architecture cuts +pnpm architect:graph blast HEAD~8 # downstream + at-risk specs of a diff +pnpm architect:q 'g.byFile("packages/.../x.ts")' # file → owner + neighborhood +pnpm architect:q 'g.bySymbol("")' # symbol → architectural usage +``` + +Generated documentation projections live in `docs-live/` (regenerate: `pnpm docs:all`). The +generated `docs-live/TAXONOMY.md` is the canonical enumerated tag set. + +**MCP tools** (`architect_*`, snake_case end to end: `architect_overview`, +`architect_scope_validate`, `architect_bundle`, …) remain the stable typed tools for +burst-mode use and the Studio sink. The canonical inventory is +`packages/architect-mcp/src/tool-registry.ts`. Read it for the current tool set rather than +trusting a count cached here. + +**Quirks worth knowing now** (full list in the graph-handle skill): + +- Never call `architect:q` bare in automation. With a non-TTY stdin and no argument it waits + on stdin. Pass an argument or piped input (`… < /dev/null` is safe). +- q bodies are plain JS function bodies: no `import`/`export`, no TS-only syntax; end with + `return ` (a single argv expression needs no `return`). +- `g.invariantsOf(x) === []` does NOT mean "guarantees nothing". Code-originated contracts + carry their guarantee as a TS type, not a Gherkin Rule (the GUARANTEE recipe disambiguates). +- `g.pattern("") === undefined` can mean parse failure OR doesn't exist. Cross-check + with `g.findByConcept` and `g.graph.featureParseFailures?.find(f => f.patternName === "")`. + +## 15. Bootstrap discipline + +Orient from the live graph, not from file scanning. A cheap first read: + +```bash +pnpm architect:q 'return {counts: g.graph.counts, active: g.patterns.filter(p => p.status === "active").map(p => p.name)}' +``` + +If a pattern name is in scope: + +```bash +pnpm architect:q 'const p = g.pattern(""); return {p, invariants: g.invariantsOf("").length, reverifies: g.specsReverifying([""]).length}' +``` + +Graph answers in about 2s, in-process. That output is the live signal. File scanning gives you +snapshots that can lie. + +## 16. Anti-anecdote, the live graph wins + +When a sample-derived finding (an old session-handoff note, a snapshot folder with a SHA suffix, an n=2 "we tried this twice" worklog, or a skill body that has drifted) appears to contradict the live state: + +- **The live PatternGraph is canonical.** `pnpm architect:q` output reflects the graph as it is right now. A skill paraphrase reflects the graph as it was when written. When they disagree, the live graph wins. +- **A sample is useful for _why_, not _what_.** It explains why a rule exists. It is not authoritative for what the rule currently is. +- **Silence is provisional, not permission.** If the live state is silent on a question a sample answers, treat the sample's finding as provisional and flag it (`FEEDBACK.md`) rather than encoding it as doctrine. + +Surprises are signal. They feed the loop (`FEEDBACK.md`). They do not override the source of truth. + +## 17. What this skill does not cover + +This is the operational baseline (vocabulary + doctrine). Depth lives in [`references/`](references/). Execution lives in two dedicated skills: + +- **`architect-sessions`.** The spec-driven session lifecycle (idea/candidate authoring, design, implement, review-spec, review-implementation, handoff), each behind progressive disclosure. The detailed per-session workflows, the full pre-deletion gate, and the value-transfer execution detail are there. +- **`architect-refactor-session`.** The non-spec-driven carve-out (evolving shipped code in place) and the multi-session / PR coordination conventions for large campaigns. + +If a session needs one of those, load the dedicated skill. Do not paraphrase it from memory. diff --git a/.agents/skills/architect-base/references/annotation-ownership.md b/.agents/skills/architect-base/references/annotation-ownership.md new file mode 100644 index 0000000..b2b9e1e --- /dev/null +++ b/.agents/skills/architect-base/references/annotation-ownership.md @@ -0,0 +1,86 @@ +# Annotation ownership + +Reference for which `@architect-*` tags live on feature files versus on code stubs and production TypeScript. Used by the `architect-sessions` design, implement, and review-implementation references and by `architect-refactor-session`. + +## Split-ownership principle + +Feature files own _what_ and _when_ (planning). Code stubs and production TypeScript own _how_ and _with what_ (implementation). Neither duplicates the other. + +This split is what lets the kernel state: + +- A pattern is **identified** by its feature file. +- Production code **realizes** the pattern via `@architect-implements:` (a relation, not an identity claim). +- Production-TS `@architect-*` annotations are **additive enrichment**, not mandatory completion criteria. + +## Feature files own (planning) + +| Tag | Purpose | +| ----------------------------- | --------------------------------------------------------------------- | +| `@architect-pattern` | Pattern identity (canonical) | +| `@architect-status` | FSM state (`candidate`, `roadmap`, `active`, `completed`, `deferred`) | +| `@architect-bounded-context` | Canonical structural grouping | +| `@architect-uses` | Declared dependency edges for spec, ADR, and test patterns | +| `@architect-implements` | Realization edge (test feature → production pattern) | +| `@architect-executable-specs` | Forward link from design spec to executable feature | +| `@architect-unlock-reason` | Audit-trail for unusual FSM transitions | + +## Code stubs / production TS own (implementation) + +| Tag | Purpose | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | +| `@architect-usecase` | When/how to use | +| `@architect-target` | Stub's forward pointer to eventual production path | +| `@architect-enforces-decision` | ADR/DD reference. The structured pattern→ADR edge (additive); `@architect-decision` is a doc-aggregation tag, not this | +| `@architect-role` | Closed implementation-role enum | + +## Code-originated patterns + +Some patterns have no feature file because the code itself is the canonical definition: codecs (renderers), contracts (Zod schemas), and certain utilities. These patterns identify themselves on `.ts` source via `@architect-pattern:` and carry `@architect-role:codec | contract | utility | ...` on the same file. When source owns identity, that same source file also owns identity-coupled metadata such as `@architect-bounded-context` and any `@architect-uses` edges for that pattern. The PatternGraph extractor accepts production-TS identity for these roles. + +Practical rule: `@architect-bounded-context` belongs on the owner of canonical identity. For planned behavior patterns, that owner is the feature file. For code-originated patterns, that owner is the `.ts` file carrying `@architect-pattern`. Do not duplicate the tag across both owners for the same pattern unless the second copy is an intentionally additive annotation with a different scope. + +## When to use a feature file vs the source for identity + +Use a feature file when the pattern represents planned behavior, business intent, or a UI/integration outcome. Anything where the Gherkin scenarios are part of the pattern's definition. + +Use a `.ts` file when the pattern is purely structural, a contract, a serialization codec, a barrel, or a narrow utility, and a feature file would carry no scenarios beyond "the type compiles." + +## Do not duplicate identity + +"Duplicate" means the **same pattern name** on two owners. If a feature owns identity for pattern `X`, do NOT also author `@architect-pattern:X` on the realizing code. Keep `@architect-bounded-context` and feature-level `@architect-uses` on the owning feature. Put `@architect-implements:X` (relation, not identity) on the realizing file. The feature still owns `X`. + +This is **not** a ban on code carrying _any_ `@architect-pattern`. A code/contract **stub** (or shipped module) realizing a behavioral feature carries its **own, distinct** code-originated identity, for example `@architect-pattern:EmissionDescriptor` (`@architect-role:contract`) with `@architect-implements:TaxonomyDocumentationCluster`. That is the bipartite design↔contract split, the same shape as test↔production, **not** duplication: the names differ, so `mergePatterns` sees no collision. `formal-spec/04-tag-registry.md` makes `@architect-pattern` a **MUST on stubs**, and ADR-003 records that identity **travels with the code from stub through production**. A node-less code stub is the anti-pattern. Its `@architect-implements` edge is dropped and it is invisible to `g.pattern()` reads, `architect_bundle`, and `implementedBy` traversals. The lone exception is the **step-definition stub** (`architect/step-stubs/`), which carries no `@architect-pattern` (ADR-008): the spec owns identity, and the step stub only realizes scenarios. + +Authoring-syntax note: the `@architect-pattern:Name` / `@architect-implements:Name` forms above are naming shorthand. In an actual `.ts` stub or module these tags are **space**-separated, `@architect-pattern EmissionDescriptor`, `@architect-implements TaxonomyDocumentationCluster`, `@architect-target …`, while `@architect-role:` / `@architect-bounded-context:` keep the colon. `.feature` files use the colon for `@architect-pattern:` / `@architect-implements:`. Full rule: [`taxonomy.md`](taxonomy.md). + +## Do not duplicate explanation + +Identity is normalized. Pattern `X` is explained on **one** canonical owner (its feature file, or for a code-originated pattern its `.ts`). Prose is normalized the same way: the pattern's **what and why** live on that one owner, never copied onto its edges. + +- A file carrying `@architect-implements:X` documents **this file's local how**, the implementation choice, the gotcha, the local constraint, not what `X` is or why it exists. _N_ files implementing `X` must not carry _N_ paraphrases of `X`'s purpose. That denormalizes the canonical node's prose onto its realization edges, the prose form of the ADR-006 single-read-model violation. When the local note would add nothing beyond "this realizes X," the `@architect-implements:X` edge alone is the documentation. +- A **step-definition** stub (`architect/step-stubs/`; no `@architect-pattern`, per ADR-008) carries **wiring**, not narration. Re-stating the rule or scenario the spec already owns is the stub form of transcription bloat. The spec owns that prose. The step stub binds it to steps. +- A **code/contract** stub carries its own identity and the shape decisions production code will need (types, signatures, why-this-shape), but not a re-explanation of the behavioral pattern it implements. That lives on the feature it points at via `@architect-implements`. + +This is the authoring-time sibling of the value-transfer **Transcription bloat** anti-pattern in [`../../architect-sessions/references/ephemeral-spec-deletion.md`](../../architect-sessions/references/ephemeral-spec-deletion.md): both enforce one home per explanation. + +## Production-TS annotations are additive, not mandatory + +A pattern can be `@architect-status:completed` with **zero** `@architect-*` JSDoc on the production source, provided the executable feature carries the full record (pattern identity, status, dependencies, invariants, scenarios). + +Annotations are curated, not a coverage quota. + +Implications: + +- Value transfer (see [`../../architect-sessions/references/ephemeral-spec-deletion.md`](../../architect-sessions/references/ephemeral-spec-deletion.md)) does NOT require production-TS JSDoc to exist as a precondition for deletion-readiness. It only requires the executable feature carry the rule content. +- Annotations enrich discoverability for code-first navigation. They do not gate completion. +- A reviewer flagging "no annotations on ``" as a value-transfer blocker is mistaken. Refer them here. + +## Sibling references + +- [`../../architect-sessions/references/ephemeral-spec-deletion.md`](../../architect-sessions/references/ephemeral-spec-deletion.md). How this policy feeds the deletion gate. +- [`./spec-pattern-relationships.md`](./spec-pattern-relationships.md). Bipartite production↔test pattern graph + the `@architect-implements` realization edge. +- [`../SKILL.md`](../SKILL.md) §"Anti-anecdote". The live PatternGraph via `pnpm architect:q` is canonical. A stale skill paraphrase is not. + +## Provenance (informational) + +The split-ownership policy was originally codified in the architect package's methodology doctrine and is now formalized in `formal-spec/03-tag-system.md`. The canonical statement for plugin-internal use lives here in the kernel. Tag definitions and required/repeatable flags are enumerated in the generated `docs-live/TAXONOMY.md`. Re-verify against a fresh regeneration (`pnpm docs:all`) rather than against any stale copy if the two ever diverge. diff --git a/.agents/skills/architect-base/references/decision-records.md b/.agents/skills/architect-base/references/decision-records.md new file mode 100644 index 0000000..e868696 --- /dev/null +++ b/.agents/skills/architect-base/references/decision-records.md @@ -0,0 +1,64 @@ +# Decision records + +How architectural decisions are recorded, what may and may not go in a record, and the two very different things the word "decisions" names in this repo. The summary in [`../SKILL.md`](../SKILL.md) §7 is the always-loaded version. This is the depth. + +## ADRs / PDRs, permanent, decisions-only + +`architect/decisions/` holds Architecture / Product Decision Records as `.feature` records. They are **permanent** and carry **only durable facts that are not about execution**: + +**Belongs in a record.** + +- The decision itself, stated plainly. +- The rationale, why this option over the alternatives. +- The durable constraint the decision imposes (the invariant future work must respect). +- References to the patterns / ADRs it currently **depends on**. Live edges only, never a "supersedes" / "replaces" marker. During bootstrap the replaced record is deleted in place. "What did we replace?" is a `git log` question, not a read-model edge. History lives in git. + +**Never belongs in a record.** + +- Status, work-in-progress, "currently blocked on X". +- ETAs, sprint/phase scheduling, who is doing what this week. +- Step-by-step implementation plans or code snippets. + +That line, durable decision vs operational worklog, is the whole point. A record that accretes temporal context rots the moment the work moves on, and it poisons every projection (release notes, architecture docs) that reads it as ground truth. + +**Amendment rule.** _Post-1.0:_ a decision is amended by authoring a **new** ADR that supersedes the old one, never by editing the original. The history of _why we changed our mind_ is itself durable. _**During bootstrap**_ (pre-1.0, live-state, the standing context; see the repo `CLAUDE.md` / `AGENTS.md` bootstrap doctrine): consolidate **in place**. Edit, slim, or delete the record directly, with **no supersession metadata** (no `@architect-adr-supersedes` / `adr-superseded-by` tags, no "replaces" / "superseded-by" prose). That is read-model history the bootstrap excludes. The replaced record is deleted, not linked. An amend-chain manufactures exactly the history the read model is built to exclude, so a "new superseding ADR" for a record nobody has built on yet is residue, not provenance. The deliberate change of mind is still recorded on its own terms. What is dropped is the append-only chain around it. + +## Read records through the read entry point, not from memory + +The records are the authority. Your recollection is anecdote (see [`../SKILL.md`](../SKILL.md) §"Anti-anecdote"). Read them: + +```bash +pnpm architect:q 'g.pattern("ADR006SingleReadModelArchitecture")' # a specific record +# the projected decision set: docs-live/DECISIONS.md +# ADRs as theme/layer slices (by-theme / layered lenses): docs-live/ARCHITECTURE.md +# (regenerate docs-live/ with `pnpm docs:all`; the architect_documentation MCP tool serves the same projections) +``` + +ADRs also carry `@architect-adr-theme` / `@architect-adr-layer` classification, so the generated `docs-live/ARCHITECTURE.md` renders them grouped into named theme clusters (e.g. `Theme: projections` = ADR-005/006/009/010) with their depends-on/see-also web, and `docs-live/DESIGN-REVIEW.md` carries the same by-theme / by-layer lenses over working-state-inclusive patterns. **"Which decisions cluster around projections / persistence / taxonomy?"** is one lens read. Never grep `architect/decisions/` for it. + +## The load-bearing set (and the nuance each is most often gotten wrong on) + +- **ADR-003.** Source-First Pattern Architecture. TypeScript source owns pattern identity; `@architect-implements` (authored on the test `.feature`) is the _primary_ reverse-traceability edge, distinct from derived reverse edges (`usedBy` / `enables`) which you never hand-author. +- **ADR-005.** Codec / Renderer Separation. The `PatternGraph` is the sole codec/renderer input. +- **ADR-006.** Single Read Model. The read model is the **`PatternGraph`** (assembled graph + `relationshipIndex` + pre-computed views), **not** `ExtractedPattern` (which is the canonical per-pattern _record contract_ the graph is built from). Feature consumers depend on the `PatternGraph`. Graph is the frozen handle over that read model. Direct `scanner/` / `extractor/` imports are sanctioned only in graph-building pipeline code. +- **ADR-007.** Coordinated Taxonomy Redesign. The three orthogonal axes + the closed role enum. See [`./taxonomy.md`](./taxonomy.md). +- **ADR-009.** Projection Trust Boundary. `parseAndProject*` is the raw-input trust boundary for external projection callers, parsed once. +- **ADR-014 Agent Read Surface.** `pnpm architect:q` is the agent read entry point. Graph is the frozen handle. PatternGraph is the read model it exposes as `g.graph`. Reusable read algorithms stay named pure core functions. MCP `architect_*` tools remain the typed burst/Studio tools. + +## Not the same as a campaign `DECISIONS.md` + +Two artifacts share the word "decisions" and have **opposite lifetimes**. Keep them apart: + +| | `architect/decisions/` (ADRs) | `.pr-coordination/DECISIONS.md` | +| ---------- | ------------------------------------------------------------------- | ------------------------------------------- | +| Lifetime | **Permanent** | **Ephemeral** (one campaign) | +| Holds | Durable architectural decisions + rationale | Judgment-calls a campaign needs before code | +| Resolution | Consolidated in place (bootstrap); superseded by a new ADR post-1.0 | Resolved-with-commit-sha, then archived | +| Audience | All future work, all projections | The workers in one campaign | + +Filing durable architecture in the campaign log loses it when the campaign archives. Filing campaign bookkeeping in an ADR poisons the permanent record. The campaign-log shape (tight `Question / Options / Recommendation / Consumed-by / Status` entries) lives in [`../../architect-refactor-session/references/multi-session-coordination.md`](../../architect-refactor-session/references/multi-session-coordination.md). + +## See also + +- [`../SKILL.md`](../SKILL.md) §7. The always-loaded summary and the key-ADR list. +- [`./taxonomy.md`](./taxonomy.md). ADR-007's classification axes in full. diff --git a/.agents/skills/architect-base/references/four-tier-ladder.md b/.agents/skills/architect-base/references/four-tier-ladder.md new file mode 100644 index 0000000..684dc55 --- /dev/null +++ b/.agents/skills/architect-base/references/four-tier-ladder.md @@ -0,0 +1,114 @@ +# Four-tier ladder + +Shared reference for every Architect session-typed skill. The ladder is discriminated by **authored status**, **file location**, and the tier's required content. `@architect-maturity` is derived from status at every tier **except idea**. An idea-tier spec authors an explicit `@architect-maturity:idea`, the opt-in marker the guard's idea-tier checks key on (status `candidate` alone is ambiguous, because the candidate tier shares it). Skills link here instead of inlining the tier table, so tier rules live in one place. + +**Terminology.** "Idea inbox" is the colloquial name for `architect/specs/ideas/`, the folder that holds idea-tier specs awaiting promotion. "Idea tier" and "idea inbox" are used interchangeably across the skills and route to the same planning intent. + +## Tiers + +| Tier | Authored status / location | Folder | Line budget | What this tier adds vs the one above | +| --------- | -------------------------------------------------------------- | ----------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Idea | `@architect-status:candidate`; idea-tier shape | `architect/specs/ideas/` | **≤30 lines (warn-only)** | User story + 1-3 invariant-only rules. Six authored tags total (the five baseline + explicit `@architect-maturity:idea`); structural-variant carve-outs (epic / slice) may add `**Members:**` and `**Usage:**` blocks. See "Epic and slice variants" below. Both still respect the ≤30 budget. Otherwise no `Background:`, no scenarios, no rationale, no verified-by. | +| Candidate | `@architect-status:candidate`; candidate-tier shape | `architect/specs/candidates/` | **30-80 lines** | Adds `**Open Questions:**` block + 1-2 happy-path scenarios; drops the explicit `@architect-maturity:idea` (maturity derives to `idea` from `status:candidate`, still consideration, which releases it from idea-tier gating). | +| Plan | `@architect-status:roadmap`; deliverables + plan-tier metadata | `architect/specs/` | untyped (150+) | Adds deliverables table, full scenario set, and `**Rationale:**` / `**Verified by:**` on rules. Hierarchy-axis metadata stays on the `@architect-level` / `@architect-parent` pair. | +| Design | `@architect-status:roadmap`; plan-tier shape plus design stubs | `architect/specs/` | untyped (300+) | Adds stubs in `architect/stubs//`, error/edge/integration scenarios, ADR refs. | + +## Mandatory tags per tier + +Every tier carries these five authored baseline tags, plus `@architect-level:epic|slice` for those structural variants. See "Epic and slice variants" below. **The idea tier also authors `@architect-maturity:idea`**, the explicit opt-in the guard's idea-tier checks key on. Maturity is otherwise **derived from status** (ADR-007: `idea` maturity = consideration, `plan` = delivery; `DEFAULT_MATURITY_BY_STATUS` maps `candidate→idea`, `roadmap→plan`, …) and normally left to derive (an explicit value still wins, per §04). The candidate tier drops the explicit `:idea` (deriving back to `idea` = still consideration), and `roadmap`+ derives `plan`/`design`. Tiers above idea may add metadata tags (e.g. `@architect-completed` at completion time) without changing the baseline. `@architect-product-area` is **not** an extra metadata tag. It is baseline tag #4, required from idea tier up. See the generated `docs-live/TAXONOMY.md` for the live tag set. Do not maintain a hand-curated list here. + +The four-tier ladder is the maturity axis. It is independent of the hierarchy axis (`@architect-level`, `@architect-parent`), which expresses epic→phase→task→slice decomposition. A pattern at any maturity tier can be at any hierarchy level. + +1. `@architect`. Gate tag +2. `@architect-pattern:` +3. `@architect-status:`. See ladder table +4. `@architect-product-area:` +5. `@architect-parent:` + +**Idea tier adds a 6th.** `@architect-maturity:idea`. This is the explicit discriminator the guard's `detectIdeaTier` requires (`packages/architect-guard/src/lint/idea-tier/`). Without it, an `architect/specs/ideas/` file is _not_ recognized as idea-tier and silently escapes idea-tier validation (line budget, baseline-tag count, parent requirement). Authored only at idea tier; **dropped on promotion to candidate**. Removing it is what releases the spec from idea-tier gating, and maturity then derives to `idea` from `status:candidate` (still consideration, no longer the explicit opt-in). The guard's idea-tier minimum-tag count is the five (gate, pattern, status, **maturity**, product-area), with `@architect-parent` enforced separately, matching `formal-spec/08-spec-evolution.md`'s six-tag idea minimum. + +## Epic and slice variants + +Idea-tier files that group other patterns or save a multi-pattern view carry `@architect-level:epic` or `@architect-level:slice`. These are **hierarchy-axis** declarations (not maturity-axis). See [`./spec-pattern-relationships.md`](./spec-pattern-relationships.md) §"Hierarchy axis" for the canonical doctrine. The variants relax two baseline rules: + +- **Parent carve-out.** Epics are top-of-chain, slices are views. Neither has an `@architect-parent`. The lint and grader both exempt these levels from the parent requirement. +- **`@architect-level` is allowed (not a smell).** It is a structural hierarchy tag, not idea-tier metadata, so its presence does not violate the "additional tags are a smell" rule. An epic/slice therefore carries gate, pattern, status, `@architect-maturity:idea`, product-area, and `@architect-level`. `@architect-parent` omitted. + +Epic file shape: idea template + a human-facing `**Members:**` bullet list naming each member pattern. Slice file shape: idea template + `**Members:**` + a `**Usage:**` line describing the question the slice answers. Both stay within the ≤30-line soft budget. + +## Effective maturity + +`@architect-maturity` is **derived from status** (ADR-007: `idea` = consideration, `plan` = delivery). An explicit value always wins (`formal-spec/04` "explicit always wins"). Canonical defaults live at `formal-spec/04-tag-registry.md` § "Status → Maturity Defaults" (`candidate→idea`, `roadmap→plan`, `active→design`, `completed→executable`). The **one place an explicit tag is _required_** is the idea tier. Elsewhere it is normally left to derive (an explicit override is permitted but rarely needed). + +**Why the idea tier needs the explicit tag.** A file in `architect/specs/ideas/` must author `@architect-maturity:idea` to be recognized as idea-tier by the guard (`packages/architect-guard/src/lint/idea-tier/`). `@architect-status:candidate` alone is _not_ sufficient, because the candidate tier shares that status (and legacy specs may carry no explicit maturity), and the guard **deliberately stopped** inferring idea-tier from it (otherwise those specs cascade false positives through the idea-tier checks). PatternGraph auto-defaults `candidate→idea` for queries, but the guard's idea-tier checks (≤30-line budget, baseline-tag count, parent requirement) only fire on the explicit tag. + +**Why the candidate tier drops the explicit tag.** Promoting idea→candidate **drops** the explicit `@architect-maturity:idea` (status stays `candidate`). Removing it is what releases the spec from idea-tier gating. Its maturity then derives to `idea` from `status:candidate`, still the _consideration_ track (open questions unresolved), exactly as `DEFAULT_MATURITY_BY_STATUS` prescribes. Delivery commitment (`maturity:plan`) normally arrives at the acceptance gate, when status advances to `roadmap`, though an explicit `@architect-maturity:plan` may mark delivery earlier (§04 "explicit always wins"; valid at `status:candidate` per `VALID_COMBINATIONS`). Candidate-tier files normally live in `architect/specs/candidates/` with maturity derived (no explicit tag). Plan/design tiers stay in `architect/specs/` at `@architect-status:roadmap` and are distinguished by required content and deliverables/stub files. + +## Valid promotion paths + +``` +idea ──► candidate ──► plan ──► design +``` + +- **Idea → Candidate.** Add `**Open Questions:**` + 1-2 happy-path scenarios; **drop `@architect-maturity:idea`** (removing it releases the spec from idea-tier gating. Maturity derives to `idea` from `status:candidate`, still consideration. Keeping `:idea` would hold it at idea tier under the ≤30-line budget); `git mv` from `architect/specs/ideas/` to `architect/specs/candidates/`. Status stays `candidate`. +- **Candidate → Plan.** Add deliverables table, `**Rationale:**` / `**Verified by:**` on rules, full scenario set, and any retained hierarchy metadata needed for the pattern; bump `@architect-status:candidate` → `roadmap`. Edit in place. No file move. +- **Plan → Design.** Add stubs in `architect/stubs//`, error/edge/integration scenarios, ADR refs. Status stays `roadmap` (it transitions to `active` during the implement-spec session, not here). Edit in place. + +Skipping rungs (idea → plan, candidate → design, etc.) is rejected. Promote through every rung. (The one non-spec-driven exception, backfilling shipped code that has no spec, is owned by [`architect-refactor-session`](../../architect-refactor-session/SKILL.md), not this spec-driven ladder.) + +## Worked example 1, idea-tier minimum + +> The pattern names and `@architect-product-area:editor` below are **illustrative**. Product-area values are repo-configured (this repo's live enum is `Annotation · Configuration · Generation · Validation · DataAPI · CoreTypes · Process · Projection`; verify in the generated `docs-live/TAXONOMY.md`). The example teaches the tag _shape_, not a value to copy. + +Location: `architect/specs/ideas/copilot-context-bundle.feature` + +```gherkin +@architect +@architect-pattern:CopilotContextBundle +@architect-status:candidate +@architect-maturity:idea +@architect-product-area:editor +@architect-parent:CopilotIntegration +Feature: CopilotContextBundle - assemble pattern context for AI agents + + **User Story:** As a developer, I want a single bundle of pattern context + so that my AI agent has the architectural picture without re-reading files. + + Rule: Bundle is read-only and derived from PatternGraph + **Invariant:** Bundle never carries data not already in the graph. +``` + +Six authored tags, one user story, one rule, one invariant. That is the entire shape. Adding a deliverables table or a scenario here is a smell. It means the idea is ready to promote, not that the idea-tier file should grow. + +## Worked example 2, candidate-tier promotion + +Starting from the idea above, promotion produces: + +Location: `architect/specs/candidates/copilot-context-bundle.feature` + +```gherkin +@architect +@architect-pattern:CopilotContextBundle +@architect-status:candidate +@architect-product-area:editor +@architect-parent:CopilotIntegration +Feature: CopilotContextBundle - assemble pattern context for AI agents + + **User Story:** As a developer, I want a single bundle of pattern context + so that my AI agent has the architectural picture without re-reading files. + + **Open Questions:** + - Does the bundle include stub content, or only their resolved targets? + - What is the cache key, pattern name alone, or pattern + session intent? + + Rule: Bundle is read-only and derived from PatternGraph + **Invariant:** Bundle never carries data not already in the graph. + + @acceptance-criteria @happy-path + Scenario: Agent requests bundle for a pattern + Given a pattern named "UserService" exists in the graph + When the agent calls the context API with session "design" + Then the bundle includes deliverables, stubs, and dependency tree +``` + +Mechanical changes: file moved `ideas/` → `candidates/`, the explicit `@architect-maturity:idea` was **dropped** (releasing the spec from idea-tier gating; maturity now derives to `idea` from `status:candidate`, still consideration), the `**Open Questions:**` block was added, and one happy-path scenario was added. Status stays `candidate`. The acceptance gate is what later flips `status:candidate` → `status:roadmap` and starts the plan-tier delta (where maturity derives to `plan` = delivery). diff --git a/.agents/skills/architect-base/references/fsm-transitions.md b/.agents/skills/architect-base/references/fsm-transitions.md new file mode 100644 index 0000000..d1d7a3a --- /dev/null +++ b/.agents/skills/architect-base/references/fsm-transitions.md @@ -0,0 +1,70 @@ +# FSM transitions + +Reference for the Architect PatternGraph's status transitions and the `@architect-unlock-reason:` audit-trail requirement. The `architect-sessions` implement and handoff references rely on this table, and the `architect_scope_validate` verdicts and `g.fsm.isValidTransition` answers on the read entry point (`architect-graph-handle`, ADR-014) resolve against it. + +The kernel splits "transitions" into two categories that are easy to conflate: + +1. **Process-Guard FSM transitions.** Validated by `architect-guard` at commit time. These are the four-row table below. +2. **Maturity-driven status flips.** Driven by spec-authoring sessions (the four-tier ladder), governed by the acceptance gate, not by Process Guard. + +Putting both in the same table makes it look like Process Guard authorizes all of them. It does not. Keep them separate. + +## Process-Guard FSM transitions (validated) + +``` +roadmap ──► active (implement session starts) +roadmap ──► deferred (work parked) +active ──► completed (implementation done, value transferred) +active ──► roadmap (implementation rolled back) +deferred ──► roadmap (work resumed) +completed ──► active (advisory reopen) +completed ──► roadmap (advisory reopen) +``` + +Notes: + +- `completed` is no longer terminal. Reopening to `active` or `roadmap` is a valid, advisory transition. +- Skipping rungs (e.g., `roadmap` → `completed` directly) is rejected unless the unlock-reason mechanism authorizes it. Use the `architect_scope_validate` MCP tool as the pre-flight check that catches bad transitions before they fire. +- Verify a candidate transition programmatically with `pnpm architect:q 'g.fsm.isValidTransition("","")'`. The check returns a deterministic answer. + +## Maturity-driven status flips (acceptance-gate, not FSM) + +``` +candidate ──► roadmap (acceptance gate cleared during planning) +``` + +This flip is performed by the spec author at the moment the `@architect-status` tag is bumped from `candidate` to `roadmap`, typically during plan-tier authoring (the `architect-sessions` plan reference) when promoting a candidate to the plan tier. It is NOT validated by Process Guard's transition rules (Process Guard's table starts at `roadmap`). The acceptance gate is human judgment plus the four-tier-ladder shape requirements. See [`./four-tier-ladder.md`](./four-tier-ladder.md) § "Valid promotion paths". + +Treating `candidate → roadmap` as a Process-Guard transition is a common mistake. Call out the distinction when reviewing FSM-related spec edits. + +## `@architect-unlock-reason:` requirements + +`architect-guard` treats `@architect-unlock-reason:` as an advisory-warning suppressor for completed reopen/edit and as a required marker for genuinely unusual transitions: + +- Reopening or editing a `completed` pattern when you want the commit path to stay silent instead of warning. +- Any transition the standard FSM table above does not include. +- Re-completing a pattern that was reopened (the original unlock-reason should remain alongside a new one). + +Authoring rules (verified against the guard's runtime checks): + +- Minimum length: **10 characters**. Short reasons like `fix` are rejected. +- Cannot be a placeholder: `test`, `xxx`, `bypass`, `temp`, `todo`, `fixme`. Placeholder values are treated as no unlock reason at all. +- The reason is human-readable, free-text, and shows up in audit reads via `pnpm architect:q` (e.g. `pnpm architect:q 'g.graph.patterns.find(p => p.name === "")'`, the full canonical record). + +## Pre-flight: use scope-validate + +Before transitioning a pattern, run the pre-flight via the `architect_scope_validate` MCP tool (pattern + `design`|`implement` session), and verify the FSM leg deterministically: + +```bash +pnpm architect:q 'g.fsm.isValidTransition("","")' +``` + +The session parameter selects the readiness target. The check returns PASS / WARN / BLOCKED with explicit reasons, including any FSM transition the requested session would require. + +If `architect_scope_validate` returns BLOCKED with "FSM allows transition: X → Y is not valid", the Process-Guard transition table above is the source of truth. Promote through the missing rungs first. + +## Provenance (informational, verified at commit time) + +This file is **self-contained**. The FSM transition table, unlock-reason rules (10-char minimum, placeholder rejection), and the `isValidTransition` check are all canonical here. Verify the check live with `pnpm architect:q 'g.fsm.isValidTransition("roadmap","active")'`. Verify the FSM behavior live with the `architect_scope_validate` MCP tool. No external doc dependency. + +See [`../SKILL.md`](../SKILL.md) §"Anti-anecdote". When a sampled finding contradicts this table, the live graph (`g.fsm.isValidTransition`) wins, not the sample. diff --git a/.agents/skills/architect-base/references/rule-block-template.md b/.agents/skills/architect-base/references/rule-block-template.md new file mode 100644 index 0000000..ff9c4ad --- /dev/null +++ b/.agents/skills/architect-base/references/rule-block-template.md @@ -0,0 +1,64 @@ +# Rule-block template + +Reference for the structured `Rule:` block convention used in both design specs and executable Gherkin. Used by the `architect-sessions` plan, design, implement, and review-spec references and by `architect-refactor-session`. + +## Rule blocks are optional + +Rule blocks are **not mandatory**. Use them when the feature defines business invariants that benefit from structured tracking. Skip them for plain behavior verification. Forcing Rule blocks onto features that aren't invariant-driven adds noise without information. + +A feature whose intent is "verify this UI button shows the right text in three states" needs scenarios, not invariants. A feature whose intent is "the planning state machine never allows X → Y without unlock-reason" is exactly what Rule blocks were designed for. + +## 4-field template (when Rule blocks are used) + +```gherkin +Rule: + + **Invariant:** <1-2 sentence statement of what must always be true> + + **Rationale:** + + **Verified by:** +``` + +The four fields: + +1. **Rule line.** Short, descriptive, one rule per Rule block. +2. **Invariant.** 1-2 sentences. State the rule, do not justify it. +3. **Rationale.** Why the invariant exists. Reference ADRs or business context. Avoid restating the invariant. +4. **Verified by.** Comma-separated list of Scenario names from this Rule block. The back-link from invariant to test. + +## Verified-by is the back-link + +`**Verified by:**` lets a reader (or query) walk from invariant to the specific scenarios that prove it holds. Renaming a scenario without updating Verified-by silently breaks this trace. The trace appears intact but resolves to nothing. + +When you rename a scenario, grep for the old name in `**Verified by:**` lines and update. + +## Distillation (no transcription) + +The `**Rationale:**` and `**Verified by:**` fields are where redundancy accretes. Guard them: + +- A `**Rationale:**` that inverts or re-states its `**Invariant:**` carries no information. Drop it (the field is optional). Keep it only when it gives a **why** the invariant doesn't: an ADR link, a business constraint, a rejected alternative. +- A `**Verified by:**` repeated **verbatim across multiple rules** is a boilerplate smell (the backfill failure mode, e.g. an ADR with the same string on every rule). Each rule's Verified-by names the scenarios that prove **that** rule, so identical strings mean the back-link is fake. + +This is the rule-authoring sibling of the value-transfer **Transcription bloat** anti-pattern ([`../../architect-sessions/references/ephemeral-spec-deletion.md`](../../architect-sessions/references/ephemeral-spec-deletion.md)) and the `review-spec.md` density check. + +## Tier guidance + +| Tier | Rule-block fields | +| ---------- | ---------------------------------------------------------------------------------------------- | +| Idea | `**Invariant:**` only. No rationale, no verified-by (no scenarios exist yet) | +| Candidate | `**Invariant:**` only. Open questions and 1-2 happy-path scenarios live OUTSIDE the Rule block | +| Plan | All four fields | +| Design | All four fields | +| Executable | All four fields (transferred from the design tier at implement time) | + +For the full tier table see [`./four-tier-ladder.md`](./four-tier-ladder.md). + +## Sibling references + +- [`./four-tier-ladder.md`](./four-tier-ladder.md). Tier table for when to add `**Rationale:**` + `**Verified by:**`. +- [`../SKILL.md`](../SKILL.md) §"Anti-anecdote". The live PatternGraph via `pnpm architect:q` is canonical. A stale skill paraphrase is not. + +## Provenance (informational) + +The 4-field convention is codified in `formal-spec/05-feature-spec-format.md`. The kernel statement above is the canonical reference for plugin-internal use. diff --git a/.agents/skills/architect-base/references/spec-pattern-relationships.md b/.agents/skills/architect-base/references/spec-pattern-relationships.md new file mode 100644 index 0000000..c9c26bc --- /dev/null +++ b/.agents/skills/architect-base/references/spec-pattern-relationships.md @@ -0,0 +1,75 @@ +# Spec and pattern relationships + +Reference for the bipartite production↔test pattern graph and the sanctioned naming conventions. Used by the `architect-sessions` plan (escape-hatch case), design, implement, and review-implementation references and by `architect-refactor-session`. + +## The bipartite pattern graph + +Every production pattern can have a corresponding test pattern that `implements` it. The PatternGraph carries both as nodes joined by an `@architect-implements:` edge. + +A test feature carries two file-level tags: + +```gherkin +@architect-pattern:DefineConfigExecutableTests +@architect-implements:DefineConfig +``` + +`@architect-pattern:DefineConfigExecutableTests` declares the test feature as its own pattern with a distinct name. `@architect-implements:DefineConfig` declares the realization edge to the production pattern. + +This two-tag shape is what lets queries traverse. "Show me the executable test for `DefineConfig`" walks `implements` edges from the production pattern node to its test pattern node. + +## Naming conventions for test patterns + +Two suffix conventions are sanctioned: + +| Suffix | Use case | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------- | +| `*Testing` | Test pattern accompanying a deliberately-designed pattern (the pattern flowed through plan / design before being implemented) | +| `*ExecutableTests` | Test pattern backfilling coverage for code that already ships (the formal escape hatch. See below) | + +Either suffix is acceptable. Pick whichever conveys intent better in context. PatternGraph treats them identically. The suffix is a human-facing convention. + +## Forward / reverse link pair (deletion-gate input) + +Two tags form the deletion-gate link pair: + +- **Forward.** The design spec carries `@architect-executable-specs:` pointing at the eventual executable feature file. +- **Reverse.** That executable feature carries `@architect-implements:` declaring the realization edge back to the focal pattern. + +Both must exist and resolve to each other for the design spec to be safely deletable. See [`../../architect-sessions/references/ephemeral-spec-deletion.md`](../../architect-sessions/references/ephemeral-spec-deletion.md) §"Pre-deletion gate" for the full gate criteria. + +## `*ExecutableTests` as the formal escape from retroactive plan-level specs + +The kernel flags **retroactive plan-level specs** as a load-bearing anti-pattern: authoring a fresh plan-level spec for code that already ships inverts the spec lifecycle. The formal escape is the `*ExecutableTests` convention: + +1. Author a `tests/features/**/*executable-tests.feature` (or sibling) file. +2. File-level tags: `@architect-pattern:ExecutableTests` plus `@architect-implements:`. +3. Enrich the file's Rule blocks with `**Invariant:**` (+ `**Rationale:**` / `**Verified by:**` where useful) describing what the existing code already guarantees. + +This produces graph visibility for the shipped pattern without authoring a fictitious "planned" design spec that would immediately become a zombie. + +## Hierarchy axis (epic / phase / task / slice) + +Patterns can be organized into a hierarchy independent of their maturity. The hierarchy axis carries exactly two authored tags: `@architect-level` and `@architect-parent`. + +The hierarchy axis uses two tags: + +- `@architect-level:` declares this pattern's level in the hierarchy. +- `@architect-parent:` declares the parent edge to another pattern. + +Constraints: + +- The level enum is closed: `epic > phase > task > slice`. +- `@architect-parent X` requires `X` to carry `@architect-level` at a strictly-higher level than the file declaring the parent. (`task`'s parent is `phase` or `epic`; `phase`'s parent is `epic`. Epics and slices are exempt. See below.) +- A pattern at any maturity tier (idea / candidate / plan / design / executable) can be at any hierarchy level. Hierarchy and maturity are independent. +- Cross-package parents resolve via the same `uses`-resolver that handles cross-package dependencies. Keep the authored form on the `@architect-parent` edge and let the resolver classify the target. +- Epics and slices are top-of-chain or lateral views and do not carry `@architect-parent`. + +## Sibling references + +- [`../../architect-sessions/references/ephemeral-spec-deletion.md`](../../architect-sessions/references/ephemeral-spec-deletion.md). Full deletion-gate criteria. +- [`./annotation-ownership.md`](./annotation-ownership.md). Split-ownership policy that makes the executable feature canonical. +- [`../SKILL.md`](../SKILL.md) §"Anti-anecdote". The live PatternGraph via `pnpm architect:q` is canonical. A stale skill paraphrase is not. + +## Provenance (informational) + +`@architect-implements`, `@architect-executable-specs`, and `@architect-pattern` tag formats are enumerated in the generated `docs-live/TAXONOMY.md` (regenerate: `pnpm docs:all`). The `*ExecutableTests` and `*Testing` suffix conventions originated in the package family's executable-coverage pattern doctrine. The statement above is the canonical form. diff --git a/.agents/skills/architect-base/references/taxonomy.md b/.agents/skills/architect-base/references/taxonomy.md new file mode 100644 index 0000000..781215b --- /dev/null +++ b/.agents/skills/architect-base/references/taxonomy.md @@ -0,0 +1,79 @@ +# Tag taxonomy + +How `@architect-*` tags are _organized_: the classification axes, the tag categories, and the authoring-syntax rules the lint enforces. This is the **conceptual model**. [`../SKILL.md`](../SKILL.md) §4 is the always-loaded summary. + +**The enumerated tag set is generated, not hand-maintained here.** The canonical output is the generated, git-tracked `docs-live/TAXONOMY.md` (regenerated by `pnpm docs:all`, with per-tag format · required · repeatable · allowed values · example). Read it, never a copy that drifts. This file teaches the _shape_ so that enumeration stays legible. It does not reproduce it. + +## Three orthogonal classification axes + +A pattern is classified along three independent axes (ADR-001 / ADR-007). They do not substitute for one another. A pattern carries a value on each. + +| Axis | Tag | Answers | +| ------------------- | -------------------------------------- | ------------------------------------------- | +| **Role** | `@architect-role:` | _What kind_ of unit is this? | +| **Bounded context** | `@architect-bounded-context:` | _Which context_ does it belong to? | +| **Layer** | (derived / structural) | _Which architectural layer_ does it sit in? | + +### The role enum is closed + +`@architect-role:` draws from exactly these canonical values (generated from the live tag registry. Do not hand-edit between the markers): + + + +``` +projection · service · decider · read-model · codec · contract · barrel · utility +``` + + + +A role outside this set is a lint error. Verify the live enum in the generated `docs-live/TAXONOMY.md` (regenerate: `pnpm docs:all`). + +## Tag categories (the model, not the enumeration) + +Tags fall into a handful of purpose categories. The per-tag detail lives in the generated reference above. What matters _conceptually_ is the category each tag serves: + +- **Gate.** `@architect` marks a file/feature as architect-managed. +- **Identity.** `@architect-pattern` names the pattern. Exactly one owner holds it. +- **State.** `@architect-status` (FSM lifecycle, enum). +- **Classification.** `@architect-role`, `@architect-bounded-context` (the two authored axes above). +- **Product.** `@architect-product-area` (PRD grouping). +- **Relationship edges.** `@architect-uses` (dependency, csv), `@architect-implements` (realization, csv), `@architect-extends` (generalization), `@architect-see-also` (cross-reference, no dependency implied). +- **Hierarchy.** `@architect-parent` (parent edge) + `@architect-level` (epic/phase/task/slice, enum), the hierarchy axis, independent of status. +- **Forward link.** `@architect-executable-specs` (design spec → executable feature). +- **Enrichment** (production TS, additive). `@architect-usecase`, `@architect-enforces-decision` (the structured pattern→ADR edge), `@architect-target` (stub pointer), `@architect-shape` (marks an exported declaration, interface/type/enum/const/function, for API-reference extraction). +- **Audit.** `@architect-unlock-reason` (≥10 chars, required for non-standard FSM transitions). +- **ADR authoring.** The `@architect-adr*` family (`adr`, `adr-status`, `adr-category`, `adr-theme`, `adr-layer`, `adr-supersedes`, `adr-superseded-by`) on decision records. The `adr-supersedes` / `adr-superseded-by` pair is supersession metadata, **not authored during bootstrap**. The replaced record is deleted in place. "What did we replace?" is a `git log` question. History lives in git. `@architect-adr-theme` (`persistence · isolation · commands · projections · coordination · taxonomy · testing`) and `@architect-adr-layer` (`foundation · infrastructure · refinement`) are constrained enums. Confirm a legal value in the generated `docs-live/TAXONOMY.md`, never guess. Those enums feed the generated `docs-live/ARCHITECTURE.md` (by-theme / layered) and `docs-live/DESIGN-REVIEW.md` (by-theme / by-layer) lenses (regenerate: `pnpm docs:all`; the `architect_documentation` MCP tool serves the same lenses). "Which decisions cluster around projections?" is one lens read, not a grep. +- **Aggregation.** Doc-assembly tags (`@architect-overview`, `@architect-decision`, `@architect-intro`). + +`@architect-maturity` is **derived from status** (ADR-007: `idea` = consideration, `plan` = delivery). An explicit value always wins (§04). The **one place an explicit tag is _required_** is the idea tier (`@architect-maturity:idea`, the guard's idea-tier opt-in; without it an `architect/specs/ideas/` file is not recognized as idea-tier). Promotion to candidate **drops** that explicit tag (maturity then derives to `idea` from `status:candidate`, still consideration). `roadmap`+ derives `plan`/`design`. Explicit overrides are permitted elsewhere but rarely needed. See [`./four-tier-ladder.md`](./four-tier-ladder.md) § "Effective maturity". + +## Two tag sources, one reason to always query live + +The generated `docs-live/TAXONOMY.md` projects the **validation registry**, whose live size is generated below (so it cannot drift as the registry grows): + + + +The validation registry currently defines **8 roles**, **21 metadata tags**, and **3 aggregation tags** (**32 total**). + + + +But the scanner also recognizes tags that are **not** in that registry, notably `@architect-executable-specs` and `@architect-usecase`, parsed straight into pattern metadata. So neither the generated doc nor any hand-list is a complete view of _recognized_ tags. When unsure whether a tag is recognized, the live graph is the arbiter: author it and inspect the pattern's parsed metadata (`pnpm architect:q 'g.pattern("")'`). Surprises against this two-source reality go in `FEEDBACK.md`, not into a hand-maintained tag list. + +## Authoring syntax, csv vs colon (lint-enforced) + +Two shapes, do not mix them: + +- **`@architect-uses` is a csv tag, comma-separated, NO colon per item.** `@architect-uses PatternA, PatternB` is correct; `@architect-uses:PatternA` is malformed. Space-separated values (`A B C`) fail `PatternReferenceSchema` and drop the whole node. +- **`@architect-role:` and `@architect-bounded-context:` take a colon.** `@architect-role:codec`. + +**One `@architect-uses` line per pattern, comma-separated.** The parser retains only one `@architect-uses` line. A second line is silently dropped. When adding a dependency to a pattern that already has the tag, **extend the existing line**. Never append a second one. (This is the most common edge-authoring bug. It came up repeatedly during the annotation-re-enablement campaign.) + +## Where tags live (ownership) + +Identity and planning tags live on the owner of the pattern (feature file for behavioral patterns, `.ts` file for code-originated ones). Implementation-enrichment tags live on production TS. The full split is in [`./annotation-ownership.md`](./annotation-ownership.md). Annotations are curated, not a coverage quota. + +## See also + +- [`./four-tier-ladder.md`](./four-tier-ladder.md). Maturity axis (idea/candidate/plan/design) and its mandatory-tag sets. +- [`./spec-pattern-relationships.md`](./spec-pattern-relationships.md). The hierarchy axis (`@architect-level` / `@architect-parent`) in full. +- [`../SKILL.md`](../SKILL.md) §"Anti-anecdote". The generated `docs-live/TAXONOMY.md` wins over any list written here. diff --git a/.agents/skills/architect-data-api/SKILL.md b/.agents/skills/architect-data-api/SKILL.md deleted file mode 100644 index 2289214..0000000 --- a/.agents/skills/architect-data-api/SKILL.md +++ /dev/null @@ -1,485 +0,0 @@ ---- -name: architect-data-api -description: MANDATORY before any work in this Architect repo that touches the PatternGraph, design specs, executable features, or FSM state. Triggers on mentions of `pnpm architect:query`, `architect:query`, any `architect_*` MCP tool name (`architect_overview` / `architect_context` / `architect_scope_validate` / etc.), the CLI verb names (`overview`, `status`, `context`, `dep-tree`, `files`, `scope-validate`, `handoff`, `query`, `pattern`, `bundle`, `list`, `open-questions`, `search`, `arch `, `rules`, `diagnostics`, `tags`, `taxonomy`), session intents (planning / design / implement / review / refactor / handoff) applied to an Architect pattern, FSM transitions, `scope-validate`, `dep-tree`, `arch dangling`, dangling-reference baselines, or PatternGraph queries. Single source of truth for which surface (CLI vs MCP), which flags exist, which verdicts are deterministic, and which quirks bite. Invoke BEFORE the canonical bootstrap in any architect-* session skill — the bootstrap commands live here. Do NOT use for: generic CLI questions unrelated to `pnpm architect:query`, unrelated MCP servers, generic Gherkin work outside the architect family, or sprint/project management. -allowed-tools: - - Bash - - Read - - Glob - - Grep ---- - -# Architect Data API — CLI + MCP - -This skill is the **reference**, not the router. Intent detection lives in -[`../architect-session-router/SKILL.md`](../architect-session-router/SKILL.md). -Once the router has chosen a session intent (planning / design / implement / -review / refactor / handoff), this skill is the authoritative source for how -to talk to the PatternGraph. - -The repo's `CLAUDE.md` already states the rule: **the Architect Data API -(CLI / MCP) is the canonical source. File scanning is not.** Every other -architect-scoped skill defers to this one for the actual verb shapes. - -## When this skill fires - -Every architect-repo session that touches patterns, specs, FSM state, or -executable features. The session-router invokes the canonical bootstrap; the -bootstrap lives here. If you are about to run `Read` / `Glob` / `Grep` against -`architect/`, `packages/architect-*/`, or `tests/features/` to learn about a -pattern, **stop** — there is a verb for that. - -## CLI vs MCP — which to use - -| Surface | Latency | Context cost per call | When to prefer | -| ------------------------------------ | -------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| `pnpm architect:query ` (CLI) | ~2–5s cold, ~0.5s warm cache | One Bash tool result; pastes cleanly into PRs and handoffs | **Default.** Deterministic, easy to share, JSON pipes into `jq`. | -| `architect_*` MCP tools | Sub-millisecond per call | Each call is a separate tool-use round trip | Tool-mediated bursts where you'll call ≥5 verbs back-to-back and the harness can amortize the round-trip overhead. | - -**Doctrine:** default to CLI. Reach for MCP only when you'll burst-call -several verbs in close sequence — the sub-ms-per-call win reverses once you -count per-tool round-trip overhead. The two surfaces share the same data; do -not split documentation per surface. - -## CLI ↔ MCP tool-name mapping (parity) - -Every CLI subcommand has an MCP twin. Names map by snake_casing the CLI form -and prefixing with `architect_`. **The MCP names use underscores end-to-end -— `architect_scope_validate`, not `architect_scope-validate`.** Writing the -hyphenated form will 404 against the registry. - -| CLI subcommand | MCP tool name | -| --------------------- | ------------------------------ | -| `overview` | `architect_overview` | -| `status` | `architect_status` | -| `context` | `architect_context` | -| `dep-tree` | `architect_dep_tree` | -| `files` | `architect_files` | -| `scope-validate` | `architect_scope_validate` | -| `handoff` | `architect_handoff` | -| `pattern` | `architect_pattern` | -| `bundle` | `architect_bundle` | -| `list` | `architect_list` | -| `open-questions` | `architect_open_questions` | -| `search` | `architect_search` | -| `rules` | `architect_rules` | -| `taxonomy` | `architect_taxonomy` | -| `arch neighborhood` | `architect_arch_neighborhood` | -| `arch blocking` | `architect_arch_blocking` | -| `arch coverage` | `architect_coverage` | -| `documentation` | `architect_documentation` | -| (no CLI twin) | `architect_rebuild` | -| (no CLI twin) | `architect_config` | -| (no CLI twin) | `architect_help` | - -Source of truth: `packages/architect-mcp/src/tool-registry.ts`. The current -inventory is **21 MCP tools** — CLAUDE.md still says 18, that line is stale. - -Parity carve-outs (where the surfaces diverge): - -- The CLI `arch ` namespace only partially crosses the boundary. MCP - exposes individual tools for `neighborhood`, `blocking`, and `coverage`; - the remaining subcommands (`roles`, `bounded-context`, `compare`, - `dangling`, `orphans`) are CLI-only. `architect_help` is a static tool - catalog — it does not dispatch missing subcommands. -- The CLI's `query ` whitelist (`isValidTransition`, - `getStatusCounts`, …) has no single MCP twin — use the verbs that wrap - the same data (`architect_status` for counts; FSM checks reach via the - scope-validate output). -- `diagnostics`, `tags`, `sources`, `unannotated`, `repl` are also CLI-only. - -## Pre-flight by session intent - -The **composite bundle** is the new default. `bundle --mode ` -returns deliverables + deps + rules + open-questions + docstring in one -shot. Use it first; drop down to individual verbs only when you need a single -slice. - -### Planning (idea / candidate authoring) - -```bash -pnpm architect:query overview -pnpm architect:query list --status candidate --names-only -pnpm architect:query open-questions [--parent ] # candidate readiness signal -pnpm architect:query context --session planning # if a pattern name is in mind -``` - -`scope-validate` is **not** available at this tier — it only accepts -`design` and `implement`. Idea/candidate readiness is checked structurally -(see [`../_shared/four-tier-ladder.md`](../_shared/four-tier-ladder.md)). - -### Design tier authoring - -```bash -pnpm architect:query overview -pnpm architect:query scope-validate design # gate -pnpm architect:query bundle --mode design --format json # composite -# Drop-downs when you only need a slice: -pnpm architect:query dep-tree -pnpm architect:query rules --pattern -``` - -There is no `stubs` CLI verb. `context --session design` (or the design-mode -bundle) returns stubs. - -### Implement (build from a design-level spec) - -```bash -pnpm architect:query overview -pnpm architect:query scope-validate implement # must be PASS -pnpm architect:query bundle --mode implement --format json -pnpm architect:query files # modification targets -pnpm architect:query rules --pattern --only-invariants # what to encode -pnpm architect:query query isValidTransition active # FSM gate before status flip -``` - -### Review (design-spec gap-finding, pre-implementation) - -```bash -pnpm architect:query overview -pnpm architect:query scope-validate implement # PASS/WARN/BLOCKED is the gate -pnpm architect:query bundle --mode review --format json -pnpm architect:query dep-tree -pnpm architect:query arch blocking # global blocker view -pnpm architect:query files --related -``` - -### Refactor (shipped code, no design spec) - -```bash -pnpm architect:query overview -pnpm architect:query context --session implement # current surface -pnpm architect:query files # touched-file inventory -pnpm architect:query dep-tree # blast radius -pnpm architect:query arch blocking -pnpm architect:query arch dangling --baseline packages/architect-guard/src/lint/dangling-baseline.json --strict - # graph-integrity gate -``` - -### Handoff (end-of-session capture) - -```bash -pnpm architect:query overview -pnpm architect:query context --session # has '=== FSM ===' for implement -pnpm architect:query arch blocking -pnpm architect:query open-questions [--parent ] # forward-looking signal -pnpm architect:query handoff --pattern --session [--modified-file

]... -``` - -### Generic inspection (no specific intent) - -```bash -pnpm architect:query overview -pnpm architect:query search # fuzzy pattern-name search -pnpm architect:query pattern # full detail (note parse-provenance behavior below) -pnpm architect:query taxonomy --count # tag-system snapshot -pnpm architect:query arch neighborhood -``` - -## Verb reference - -Organized by intent bucket. Verbs marked **NEW** landed in the recent -remediation wave and are not yet reflected in older skill bodies. - -### Health & inventory (any session) - -- **`overview`** — text: progress (e.g. `260 delivery patterns (114 completed, - 120 active, 26 planned) = 44%`) + blocking summary + Data-API hint footer. - Note: the hint footer currently advertises a non-existent `stubs - --unresolved` verb — ignore that line; see "Known quirks" below. -- **`status`** — status distribution counts + percentages, no per-pattern detail. -- **`list [--status v] [--role tag] [--parent X] [--count] [--names-only]`** - — pattern catalog. `--parent` is **NEW** and resolves strictly; unknown - parent emits `Parent pattern not found: ` and exits non-zero. - `--names-only` returns a JSON string array — pipe through `jq`. -- **`search `** — fuzzy pattern-name search; JSON - `[{patternName, score, matchType}]`. -- **`taxonomy [--count]`** — `--count` (**NEW**) prints a one-line summary, - e.g. `8 roles | 20 metadata tags | 3 aggregation tags | 31 total`. - `--format json` returns the full `{ root: { tags: [...] } }`. -- **`tags`** — `TagUsageMatrix`: pattern count + per-tag value distribution. -- **`diagnostics`** — JSON array of structural warnings. -- **`sources`**, **`unannotated`** — coverage helpers. - -### Per-pattern detail - -- **`pattern `** — full PatternDetail (deliverables, relationships, - rules, role, maturity, file). **NEW behavior:** when the underlying feature - file fails to parse, this verb reports parse provenance - `(kind, path, parser line:col)` instead of a flat "Pattern not found." - *A "Pattern not found" response is no longer binary* — could mean - "doesn't exist" OR "exists but failed to parse." Cross-check with `search` - or `list --names-only` before concluding it doesn't exist. -- **`context [--session planning|design|implement]`** — curated - bundle: pattern summary, dependencies, architecture neighbors. With - `--session implement`, also includes an `=== FSM ===` line showing current - status + valid transitions + protection level. -- **`files [--related]`** — primary deliverable file. With - `--related`, adds `=== COMPLETED DEPENDENCIES ===`, `=== ROADMAP - DEPENDENCIES ===`, and `=== ARCHITECTURE NEIGHBORS ===` sections. -- **`dep-tree [--depth ]`** — dependency chain walk. -- **`rules [--product-area n] [--pattern n] [--package n] [--feature glob] - [--only-invariants] [--count] [--names-only]`** — business-rule catalog. - `--package` and `--feature` are **NEW**: - - `--package ` filters by canonical workspace name - (e.g. `@libar-dev/architect-projection`). - - `--feature ` matches against `pattern.source.file` with - POSIX-style glob semantics. - -### Composite (the new default pre-flight) - -- **`bundle [--mode plan|design|implement|review] [--include - ] [--estimate-tokens] [--format json]`** — **NEW**. - Composite of deliverables + deps + rules + open-questions + docstring. - Mode default-include sets apply only when `--include` is omitted. Token - estimation is heuristic (chars / 4). - - **Quirk:** repeated `--include` flag silently keeps **only the last - value**. `--include rules --include deps` produces `Includes: [deps]`. - Always use the comma-list form: `--include rules,deps,open-questions`. - -- **`open-questions [--parent ] [--format compact|json]`** — - **NEW**. Returns `OpenQuestionList` fragment: per-pattern open questions - lifted from each spec's `**Open Questions:**` block. The candidate-tier - readiness signal that didn't exist when older skills were written. - -### Gates & validation - -- **`scope-validate [--strict]`** — verdict - `READY` / `READY (with warnings)` / `BLOCKED`. **Only `design` and - `implement` are accepted** — `planning`, `review`, anything else errors - with `Scope type must be design or implement`. Output is a per-criterion - checklist (`[PASS] / [WARN] / [BLOCKED]`) followed by a final verdict line. -- **`query isValidTransition `** — deterministic FSM gate. Returns - `{success: true, data: true|false}`. Use this before flipping - `@architect-status` (see [`../_shared/fsm-transitions.md`](../_shared/fsm-transitions.md)). -- **`arch dangling [--baseline ] [--write-baseline] [--strict]`** — - graph-integrity check. Without flags, JSON-prints every dangling reference. - With `--baseline `, compares against a checked-in baseline; with - `--strict`, exits non-zero on any drift. `--write-baseline` rewrites the - baseline deterministically. The repo's committed baseline lives at - `packages/architect-guard/src/lint/dangling-baseline.json`. -- **`arch blocking`** — text: `X blocked by: Y, Z` lines for every pattern - with incomplete dependencies. The global blocker view. - -### Other architecture verbs - -- **`arch roles`** — role inventory. -- **`arch bounded-context [name]`** — bounded-context inventory; with a name, - the contents of that context. -- **`arch neighborhood `** — local subgraph around the pattern. -- **`arch compare `** — diff two bounded contexts. -- **`arch coverage`** — annotation coverage rollup. -- **`arch orphans`** — patterns with no incoming or outgoing edges. - -### Session-record - -- **`handoff --pattern [--session planning|design|implement|review] - [--modified-file

]...`** — emits `=== HANDOFF ===` block. Pass - `--modified-file` once per file touched. - -### Whitelisted `query` methods - -`query [args...]` is a passthrough to the typed read API. Returns -`{success, data, metadata}` JSON. - -- `query getStatusCounts` → `{completed, active, planned, candidate, total}`. -- `query isValidTransition ` → `{success, data: boolean}`. See - "Gates & validation" above. -- `query getPatternsByStatus ` → array of pattern summaries. -- `query getPatternsByPhase ` → array of pattern summaries. - -### Documentation projection - -- **`documentation [--disclosure ] [--filter - ]...`** — emits projected docs (patterns / architecture / - roadmap / changelog / decisions / taxonomy / requirements-executable / - requirements-specs). The disclosure level controls verbosity. - -### Interactive - -- **`repl`** — interactive shell. Not used in scripted sessions. - -## Output formats & JSON consumption - -| Verb | Default output | `--format json` available | -| --------------------------------- | -------------- | -------------------------- | -| `query ` | JSON | (default) | -| `diagnostics` | JSON | (default) | -| `arch dangling` | JSON | (default) | -| `search` | JSON | (default) | -| `list --names-only` | JSON | (default) | -| `open-questions` | Text | yes (`--format json`) | -| `bundle` | Text | yes (`--format json`) | -| `taxonomy` | Text | yes (`--format json`) | -| `overview` / `status` / `context` / `files` / `scope-validate` / `handoff` / `pattern` / `dep-tree` / `rules` / `tags` / `arch blocking` | Text | text-only today | - -Pipe JSON through `jq` for downstream consumption. Text output is for human -review. - -### Worked JSON shapes - -`query isValidTransition roadmap active`: - -```json -{ - "success": true, - "data": true, - "metadata": { - "timestamp": "2026-05-17T01:06:21.673Z", - "patternCount": 268, - "validation": { "danglingReferenceCount": 2, "malformedPatternCount": 0, "unknownStatusCount": 0, "warningCount": 2 }, - "cache": { "hit": true, "ageMs": 1002463 }, - "pipelineMs": 482 - } -} -``` - -`open-questions --format json` (truncated): - -```json -{ - "children": {}, - "root": { - "count": 2, - "filters": {}, - "items": [ - { - "file": "tests/features/cli/list-parent-child-alpha.feature", - "pattern": "ChildAlpha", - "questions": ["Who owns the alpha follow-up?", "Which signal closes the alpha gap?"], - "status": "active" - } - ], - "kind": "OpenQuestionList" - } -} -``` - -`bundle ChildAlpha --mode design --format json` (truncated to structure): - -```json -{ - "children": {}, - "root": { - "kind": "PatternBundleEntry", - "mode": "design", - "entryRole": "root", - "memberCount": 0, - "members": [], - "includes": ["docstring", "rules", "scenarios", "open-questions"], - "pattern": { "patternName": "ChildAlpha", "status": "active", "maturity": "design", "source": "gherkin", "file": "..." }, - "blocks": { - "docstring": "...", - "openQuestions": ["..."], - "rules": [ { "kind": "BusinessRule", "ruleName": "...", "invariant": "...", "verifiedBy": ["..."], "scenarioCount": 1 } ], - "scenarios": [ { "ruleName": "...", "count": 1, "scenarios": ["..."] } ] - } - } -} -``` - -`arch dangling` (already JSON by default): - -```json -{ - "success": true, - "data": [ - { "pattern": "ArchitectBriefDeterministicBundle", "field": "seeAlso", "missing": "ADR005CodecRendererSeparation" }, - { "pattern": "ModelEnrichedDataAPI", "field": "seeAlso", "missing": "ADR005CodecRendererSeparation" } - ], - "metadata": { /* ... */ } -} -``` - -`scope-validate PatternBundleProjection implement` (text): - -``` -=== SCOPE VALIDATION: PatternBundleProjection (implement) === - -=== CHECKLIST === -[BLOCKED] Dependencies completed: 1/2 completed. Blockers: PatternRelationsFragmentContracts (active) -[BLOCKED] Deliverables defined: No deliverables found in Background table -[PASS] FSM allows transition: Already active — no transition needed -[WARN] Design decisions recorded: No PDR/AD references found in stubs -[WARN] Executable specs location set: No @executable-specs tag found - -=== VERDICT === -BLOCKED: 2 blocker(s) prevent implement session -- Dependencies completed: 1/2 completed. Blockers: PatternRelationsFragmentContracts (active) -- Deliverables defined: No deliverables found in Background table -``` - -## Deterministic gates - -Three verbs are designed to be parsed for a deterministic verdict, not read -as prose: - -1. **`scope-validate `** — the per-criterion - checklist (`[PASS]` / `[WARN]` / `[BLOCKED]`) + final verdict. Treat - `READY` and `READY (with warnings)` as proceed; `BLOCKED` as stop. -2. **`query isValidTransition `** — JSON boolean. The gate before - flipping `@architect-status`. -3. **`arch dangling --baseline --strict`** — non-zero exit on drift. - Use in CI gates and refactor closing checks; otherwise the baseline-less - form reports current drift as JSON. - -## Known quirks - -- **`value-transfer ` is future work.** Referenced in - [`../_shared/value-transfer.md`](../_shared/value-transfer.md) as a planned - verb; ships per `architect/specs/value-transfer-state.feature`. Until then, - walk the manual pre-deletion gate in that shared doc. -- **`pattern ` "not found" surfaces two distinct error paths.** The - command first checks `getPattern`; if that misses, it probes - `findPatternParseFailure` and re-throws a parse-failure-with-provenance - message when one exists. Treat the two error strings as different signals - — cross-check unfamiliar "not found" output against `search` or - `list --names-only`. -- **MCP names use underscores end-to-end.** `architect_scope_validate`, not - `architect_scope-validate`. Hyphenated forms 404 against the registry. -- **`scope-validate` rejects `planning` and `review`.** The error is - `Scope type must be design or implement`. Idea/candidate readiness has no - CLI gate — use the structural checklist in - [`../_shared/four-tier-ladder.md`](../_shared/four-tier-ladder.md). - -## Doctrine cross-references - -- [`../_shared/fsm-transitions.md`](../_shared/fsm-transitions.md) — what - `scope-validate` checklist entries and `query isValidTransition` outputs - mean against the FSM table; `@architect-unlock-reason:` rules. -- [`../_shared/four-tier-ladder.md`](../_shared/four-tier-ladder.md) — which - `--session` value applies at which tier; why planning/idea/candidate are - not `scope-validate` targets. -- [`../_shared/value-transfer.md`](../_shared/value-transfer.md) — the manual - pre-deletion gate that the future `value-transfer` verb will mechanize. -- [`../_shared/canonical-references.md`](../_shared/canonical-references.md) - — anti-anecdote rule: the live CLI output is canonical; older skill bodies - paraphrasing it are not. - -## Anti-patterns (stop) - -- **Reading files before querying.** `Read` / `Glob` / `Grep` against - `architect/`, `packages/architect-*/`, or `tests/features/` to learn about - a pattern. The Data API is faster, more accurate, and more compact. -- **Hand-writing hyphenated MCP names.** `mcp__architect__overview` is fine - as a glob in prose, but the actual callable names are underscored: - `architect_overview`, `architect_scope_validate`, `architect_open_questions`. -- **Using `scope-validate planning`.** Only `design` and `implement` are - accepted. The CLI errors with `Scope type must be design or implement`. -- **Parsing `--format json` shapes by regex.** Pipe to `jq` or parse - structurally. The shapes are stable; regex against them is not. -- **Treating `pattern ` "not found" as binary.** Post-PR, it can mean - parse failure with provenance. Cross-check before concluding. -- **Chaining `--include` flags on `bundle`.** `--include rules --include deps` - silently keeps only `deps`. Use comma-lists. -- **Stitching together overview + context + dep-tree + files + rules manually - when the session-mode bundle would return the same data.** Reach for - `bundle --mode ` first; drop down to single verbs only - when you need a single slice. - -## Provenance - -All claims in this skill were verified against the live CLI on -2026-05-17 against the repo state at HEAD (`main`). Re-verify by running -`pnpm architect:query -- --help` and the relevant subcommand `--help` -forms when in doubt. The Data API is the canonical source — this skill -paraphrases it, but the CLI's own output wins on disagreement. diff --git a/.agents/skills/architect-design-session/SKILL.md b/.agents/skills/architect-design-session/SKILL.md deleted file mode 100644 index b6826d9..0000000 --- a/.agents/skills/architect-design-session/SKILL.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -name: architect-design-session -description: Use when promoting a plan-level Architect spec to design tier — adding deliverables, stubs, exhaustive scenarios, ADR references. Enforces design-only discipline (no implementation drift), uses the Data API for context, treats stubs as ephemeral scaffolds whose value will transfer to code and executable specs at implement time. Do NOT use for: idea-tier or candidate-tier source specs — route to architect-plan-session to promote through the missing rungs first. Also do NOT use for implementation, bugfixes, or generic refactors — design tier writes specs and stubs only, never production code. -allowed-tools: - - Bash - - Read - - Write - - Edit - - Glob - - Grep ---- - -# Architect Design-Tier Session - -You are taking a plan-level spec to design tier. The deliverable is a richer -`.feature` plus stubs in `architect/stubs/`. **Do not write production code in -this session** — that's the implement-spec session. - -## Doctrine references - -This skill assumes the following shared references — read them once -per session if you haven't: - -- [`../_shared/annotation-ownership.md`](../_shared/annotation-ownership.md) - — split-ownership policy: which `@architect-*` tags belong on the - feature file vs on stubs; **code stubs MUST NOT use - `@-pattern`**. -- [`../_shared/rule-block-template.md`](../_shared/rule-block-template.md) - — when Rule blocks belong in a design spec and the optional 4-field - template (Rule blocks are NOT mandatory). -- [`../_shared/spec-pattern-relationships.md`](../_shared/spec-pattern-relationships.md) - — design-tier authoring writes the `@architect-executable-specs:` - forward link; this doc explains how to choose the test-pattern name - (`Testing` vs `ExecutableTests`). -- [`../_shared/canonical-references.md`](../_shared/canonical-references.md) - — anti-anecdote rule; consult the live taxonomy - (`pnpm architect:query taxonomy --format json`) or - `formal-spec/03-tag-system.md` + `formal-spec/04-tag-registry.md` - for tag-usage questions. - -## Pre-flight (mandatory CLI bootstrap) - -Run the canonical design-tier pre-flight from -[`../architect-data-api/SKILL.md`](../architect-data-api/SKILL.md) -§"Design tier authoring" — it covers `overview`, the `scope-validate design` -gate, the design-mode `bundle` (deliverables + stubs + deps + open -questions), and the per-slice drop-downs (`dep-tree`, `rules`). There is -**no** `stubs` CLI verb — `context --session design` (or the design-mode -bundle) returns stubs. - -If `scope-validate` returns BLOCKED, stop and surface the blocker. Do not -attempt to design around a blocked dependency chain. - -## Four-Tier Ladder (entering design tier) - -Canonical reference: [`../_shared/four-tier-ladder.md`](../_shared/four-tier-ladder.md). -Read it once for the tier table, mandatory tags, and full promotion paths. - -This skill operates on the **fourth rung** — design tier, reached only by -promoting an existing plan-tier spec. The Plan → Design delta: - -- Add stubs in `architect/stubs//` -- Add exhaustive scenarios: error paths, edge cases, integration scenarios -- Add ADR refs for significant architectural decisions -- Effective maturity becomes design via the retained plan-tier file plus stubs/exhaustive scenarios; do not author `@architect-maturity` -- Status stays `roadmap` (it transitions to `active` during `architect-implement-spec`, not here) -- Edit in place — no file move - -If the source spec is at idea or candidate tier, **stop** and route through -`architect-plan-session` to promote through the missing rungs. Skipping rungs -is rejected — except for the refactoring carve-out (existing-code coverage may -skip directly to design or executable; see the shared ladder). - -## Design-tier deliverables - -A design-level `.feature` adds the following to the plan-level shape: - -- `Background:` table listing the exact files this design will touch - (deliverables) — full paths, file-by-file -- Exhaustive scenarios: error paths, edge cases, integration scenarios -- Stub references in `architect/stubs//*` -- `**Rationale:**` and `**Verified by:**` on every Rule -- ADR references where significant decisions were made - -## Stubs (ephemeral scaffolds — read this carefully) - -Stubs live in `architect/stubs//`. They: - -- Are TypeScript files with realistic signatures, types, and JSDoc — no real - logic -- May include design-decision (DD-N) comments and "When to Use" guidance -- Are **not compiled, not linted, not tested** — they are staging -- Move to `src/` during implementation, then are **deleted** from `architect/stubs/` - -When authoring stubs, encode design intent that production code will need but -that doesn't fit naturally in Gherkin: types, function signatures, hidden -constraints, why-this-shape rationale. - -## Anti-drift tripwires (stop and redirect if you catch yourself doing any) - -1. Writing real implementation logic in a stub — stop. Stubs carry shape, not behavior. -2. Adding a new `.ts` file under `src/` — wrong session. Stop and hand off to `architect-implement-spec`. -3. Running `pnpm test` or modifying `tests/features/` — wrong session. -4. Editing files outside the deliverables table — if you discover the design needs to touch a file you didn't list, **add it to the table** before editing. -5. Re-deriving pattern data outside `PatternGraph` — read via the canonical Data API verbs in [`../architect-data-api/SKILL.md`](../architect-data-api/SKILL.md) §"Verb reference". Do not parallel-pipeline the data. -6. Inventing a new business rule from scratch without an invariant — every rule needs `**Invariant:**`. -7. Promoting an idea straight to design — design tier requires plan tier first. If the source is an idea or candidate, route through `architect-plan-session`. - -## Ephemeral spec principle (mandatory understanding) - -Design-level specs and stubs are **scaffolds, not permanent documentation**. -At implementation time, the implement-spec skill will: - -1. Transfer rule content + business value to `**Invariant:** / **Rationale:** / **Verified by:**` blocks inside executable Gherkin in `tests/features/` -2. Transfer architectural intent and rationale to JSDoc `@architect-*` annotations on the production code -3. **Delete** the design-level `.feature` from `architect/specs/` -4. **Delete** the stubs from `architect/stubs/` - -Authoring expectation: write the design-level spec knowing it will be deleted. -Make every line worth reading by the implementer. Do not write anything that -won't transfer to either an annotation or an executable scenario. - -## Acceptance criteria for design tier - -Before completing this session, verify with the Data API: - -```bash -pnpm architect:query scope-validate implement # must return PASS -pnpm architect:query context --session implement # must include deliverables -``` - -If `scope-validate implement` returns WARN or BLOCKED, the design is -not ready — fix the gaps before claiming done. - -## Do not - -- Do not implement. -- Do not delete the design-level spec or its stubs in this session — the - implement-spec skill owns that step, after value transfer. -- Do not skip stubs for "obvious" patterns. If a behavior is architecturally - relevant, it gets a stub. -- Do not author scenarios that the executable test layer can't reach — design - scenarios are written to become executable. diff --git a/.agents/skills/architect-graph-handle/SKILL.md b/.agents/skills/architect-graph-handle/SKILL.md new file mode 100644 index 0000000..9106594 --- /dev/null +++ b/.agents/skills/architect-graph-handle/SKILL.md @@ -0,0 +1,223 @@ +--- +name: architect-graph-handle +description: Agent read interface over the live PatternGraph (ADR-014). Load for graph state such as a pattern's status/deps/rules, an architectural slice, neighborhoods, blast radius of a diff, what a pattern guarantees, which specs re-verify a change, or when you would otherwise grep/Read across files to learn the architecture. One command (`pnpm architect:q ''`) builds the graph live in-process and binds `g`, a typed object whose methods return plain composable data. Script the cut in plain JS. The complete frozen read model is `g.graph`, deterministic transition operations are `g.fsm`, and reusable read algorithms stay pure core functions. Ordinary grep over annotated source remains the complement for content-level search. MCP `architect_*` tools remain for burst-mode and Studio use. +allowed-tools: + - Bash + - Read + - Glob + - Grep +--- + +# Architect graph handle (`pnpm architect:q`) + +`g` is the live, in-memory handle over this repo's PatternGraph, the graph of architectural +patterns (services, contracts, codecs, projections, specs) built from annotated source. You +write a line of JS. `g` answers it in-process and only your conclusion comes back, roughly ⅕ +the context of grep or a verb round-trip, because the data never leaves the process. + +This is the primary agent read interface (ADR-014). The old `pnpm architect:query` verb CLI +is gone. Pattern state, architectural slices, and impact cuts all go through `q`. Beside it: +grep over annotated source for content-level search the graph doesn't index, the +`architect_*` MCP tools for burst-mode use and the Studio sink, and the deterministic gates +(`pnpm architect:guard`, `pnpm architect:graph dangling`, `pnpm docs:check`). + +## The command + +```bash +pnpm architect:q '' # argv +pnpm architect:q < playground/scratch/my-cut.ts # stdin, for multi-line scripts +pnpm architect:graph # named demos + the dangling gate +``` + +`--conditions=source` is already baked into these `pnpm` scripts. Don't add it. The graph +builds fresh from the working tree each call (~2s, no cache), so a just-saved annotation +shows on the next call. + +Inside a script, `g`, `inspect` (node:util), `execFileSync` (node:child_process), and +`REPO_ROOT` (repo-root abs path) are injected. cwd is the repo root. Two rules, because the +body is compiled as a function body: (1) no `import`/`export` and no TS-only syntax +(type annotations, ``, `!`). It's plain JS at eval time. (2) End an argv/stdin +body with `return ` (inspect-printed) and/or `console.log`. A single argv +expression (`g.patterns.length`) works too, no `return` needed. + +> Never call `architect:q` bare from automation or hooks. With no arg and a non-TTY stdin that +> never sends EOF, it waits on stdin. Always pass an arg or piped input (`… < /dev/null` is safe). + +## What `g` exposes + +```ts +g.patterns // PatternNode[]: {name, status, maturity, role, boundedContext, productArea, + // sourceFile, level, parent, children[], uses[], usedBy[], implementedBy[], + // implements[], enforcesDecisions[], ruleCount, scenarioCount} +g.pattern(name) // one PatternNode | undefined +g.fileToPattern(file) // repo-rel .ts → owning pattern name | undefined +g.graph // complete, deeply frozen PatternGraph (ADR-006 read side): + // .patterns · .counts · .byStatus · .byNormalizedStatus + // .relationshipIndex · .tagRegistry · .archIndex + +g.fsm // four deterministic transition operations: + // .isValidTransition · .validateTransition + // .getValidTransitionsFrom · .getProtectionSummary + +// entry adapters: the grep→graph bridge (you start from a string / file / symbol, not a name): +g.findByConcept('rate limiter') // fuzzy concept → ranked curated patterns (+ why each matched) +g.byFile('packages/.../x.ts') // file → owner + neighborhood (dark files get the mechanical one) +g.bySymbol('ProjectionBundle') // exported symbol → defining file(s) + who imports it (.importedByPatterns) + +// the spec bridge: invariants & at-risk specs of ANY maturity, labeled exec vs authored: +g.invariantsOf(patternOrFile) // "what does this guarantee?" → Invariant[] (maturity + provenance) +g.specsReverifying(filesOrNames) // "what re-verifies if these change?" → AtRiskSpec[] +g.blastRadius(changedFiles) // exhaustive impact over the mechanical graph (+ .atRiskSpecs, reaches dark files) + +// curation-assist: +g.fanInCandidates() · g.graphDiff() · g.census() · g.driftFlags(existsFn) + +`g.mech` imports are diagnostic context, not authored architecture. Dark imports default to no action; only add a curated edge when the significance rubric shows an intentional architectural dependency. + +// escape hatches: the raw shapes +g.authored // {patterns, relationshipIndex} (the curated core) +g.mech // {symbols, edges, …} (mechanical import graph / firehose) +``` + +Accessors return plain data, no `{success, data}` envelopes. Compose them directly. Bridge +return shapes, so you don't have to inspect-and-guess: + +```ts +Invariant { rule, text, pattern, maturity, provenance, featureFile, provenByScenarios[], cohort? } +AtRiskSpec { scenario, pattern, featureFile, line?, maturity, provenance, semanticTags[], cohort? } +bySymbol → { symbol, definedIn[{file,kind,pkg,pattern?}], importedByFiles[], importedByPatterns[] } +``` + +`provenance` is `'executable'` (a live test proves it) or `'authored'` (a working-spec). +`cohort` is present only when the realizing feature covers >1 pattern, so the result isn't +specific to your one query. Full field shapes live in +`packages/architect-core/src/graph/schema.ts` + `graph.ts`. The published pure contract is +`@libar-dev/architect-core/graph`. Source/config/git IO remains in `architect-cli`. + +## Where to reach + +| You're starting from… | want… | reach for | +| ---------------------------------------- | ----------------------------------- | -------------------------------------------------------------- | +| a pattern **name** | its state / deps / rules | `g.pattern` / `g.graph.relationshipIndex` / `g.invariantsOf` | +| a **concept string** | which patterns relate | `g.findByConcept` | +| a **file** | owner + neighborhood (even if dark) | `g.byFile` | +| a **symbol** | architectural usage | `g.bySymbol` | +| a **diff / changeset** | impact + which specs re-verify | `g.blastRadius` / `g.specsReverifying` | +| an **FSM transition** | is it legal? | `g.fsm.isValidTransition(from, to)` | +| a **custom cross-cut** | a slice no method pre-bakes | script it (see [references/recipes.md](references/recipes.md)) | +| **file contents** (strings, code idioms) | textual matches | plain grep. The graph doesn't index bodies | +| a **burst** of ≥5 typed reads, or Studio | stable typed tools | the `architect_*` MCP tools | + +## Examples + +Graph state, not file scanning. + +```bash +# who owns this file, and what's around it? (replaces several greps; maps results into the architecture) +pnpm architect:q 'g.byFile("packages/architect-projection/src/fragments/base.ts")' + +# where does this exported symbol get used, architecturally? +pnpm architect:q 'g.bySymbol("ProjectionBundle").importedByPatterns' + +# which patterns relate to a concept I only have as a phrase? +pnpm architect:q 'g.findByConcept("taxonomy").slice(0,5).map(h => [h.name, h.score])' +``` + +Pattern state. Each old verb is one script. + +```bash +pnpm architect:q 'g.pattern("GraphHandle")' # detail (need-shaped) +pnpm architect:q 'g.graph.counts' # status distribution +pnpm architect:q 'g.fsm.isValidTransition("roadmap","active")' # deterministic FSM gate +pnpm architect:q 'g.patterns.filter(p => p.status === "active").map(p => p.name)' +``` + +What does this guarantee, and is it proven? + +```bash +# invariants of a pattern, each labeled live-test (executable) vs authored working-spec +pnpm architect:q 'g.invariantsOf("GraphHandle").map(i => ({rule:i.rule, maturity:i.maturity, provenance:i.provenance}))' +``` + +> `invariantsOf` covers Gherkin invariants (Rule blocks). A code-originated contract such as +> `ProjectionContext` returns `[]` because its guarantee is its TS type, not a Rule. `[]` is +> not "guarantees nothing." `pnpm architect:graph invariants ` prints a note for that +> case. The GUARANTEE recipe disambiguates in one line. + +Blast radius of a change, plus which specs re-verify. Save to +`playground/scratch/headline.ts` (no `import`; end with `return`), pipe it in: + +```js +const changed = ['packages/architect-core/src/graph/graph.ts']; // or a git diff list +const b = g.blastRadius(changed); +const specs = g.specsReverifying(changed); +return { + downstreamPatterns: b.mechPatterns.length, // exhaustive impact (reaches dark files) + specsReverifying: specs.length, + byProvenance: specs.reduce((m, s) => ((m[s.provenance] = (m[s.provenance] || 0) + 1), m), {}), +}; +``` + +```bash +pnpm architect:q < playground/scratch/headline.ts +``` + +To seed from a real diff, build `changed` in-script. `execFileSync` and `REPO_ROOT` are injected: +`execFileSync('git', ['diff','--name-only','HEAD~10','--'], {encoding:'utf8', cwd: REPO_ROOT}).split('\n').filter(Boolean)`. + +Navigate and reshape in-process. An argv body may hold statements: + +```bash +# projection-role patterns with zero downstream consumers. Deletion candidates. +pnpm architect:q 'const ps = g.patterns.filter(p => p.role === "projection" && p.usedBy.length === 0); return ps.length' +``` + +Raw shapes when no view fits. `g.mech` is one property away: + +```bash +pnpm architect:q 'const t = g.mech.edges.filter(e => e.typeOnly).length; return `${t}/${g.mech.edges.length} import edges are type-only`' +``` + +## Named commands (`pnpm architect:graph `) + +```bash +pnpm architect:graph census # curation candidates, then diagnostic node/edge coverage per package +pnpm architect:graph diff # mechanical ⋈ authored: shared / dark / aspirational +pnpm architect:graph blast HEAD~8 # impact: downstream + at-risk specs of a diff +pnpm architect:graph fan-in # curation assist: load-bearing, uncurated modules +pnpm architect:graph drift # scoped drift: dangling uses / orphaned source (→ 0) +pnpm architect:graph maturity # the maturity ladder +pnpm architect:graph find taxonomy # E1 concept → patterns +pnpm architect:graph file packages/.../x.ts # E2 file → owner + neighborhood +pnpm architect:graph symbol ProjectionBundle # E3 symbol → defining pattern + importedBy +pnpm architect:graph invariants # "what does this guarantee?" (with the contract-empty note) +pnpm architect:graph specs HEAD~8 # specs re-verifying a diff, labeled +``` + +Named commands are runnable documentation over the handle. They're scripts, not contracts. +The one exception is the machine gate CI consumes, frozen by the second-caller bar: + +```bash +pnpm architect:graph dangling --baseline packages/architect-guard/src/lint/dangling-baseline.json --strict +``` + +## Script the rest, freeze almost nothing + +Most questions are a script over the exposed shapes, not a new method. The handle freezes +only irreducible cross-source joins: the entry adapters +(`findByConcept`/`byFile`/`bySymbol`), the spec bridge (`invariantsOf`/`specsReverifying`), +and `blastRadius`. A `groupBy` over an exposed field stays a script, on purpose. Freezing +thin traversals is how this would quietly become the verb wall ADR-014 deleted. When no +view fits, drop to `g.mech` / `g.authored` and script against the raw shapes. + +## Depth + +- [references/recipes.md](references/recipes.md). The "script the rest" recipe set + (STATE · I1 · MEMBERS · A1 · A2 · GUARANTEE · TRIAGE · IMPACT · DRIFT · COMPOSE · escape + hatch) plus the freeze-vs-script graduation bar. +- `packages/architect-core/src/graph/schema.ts` + `graph.ts`. Published Graph methods and + field shapes. `packages/architect-cli/src/handle/graph.ts` owns only live IO composition. +- `architect/decisions/adr-014-agent-read-surface.feature`. The decision record: why the + verb CLI is gone, what stayed frozen, the trust posture. +- `playground/CONTEXT.md`. Experiment notes behind this design: two-layer model, + curation not drift, context-efficiency numbers. diff --git a/.agents/skills/architect-graph-handle/references/recipes.md b/.agents/skills/architect-graph-handle/references/recipes.md new file mode 100644 index 0000000..b6b71e2 --- /dev/null +++ b/.agents/skills/architect-graph-handle/references/recipes.md @@ -0,0 +1,347 @@ +# Recipes. Script the rest + +The published Graph (`@libar-dev/architect-core/graph`) freezes only the irreducible +joins: the grep→graph entry adapters (`findByConcept`/`byFile`/`bySymbol`), the +spec-bridge (`invariantsOf`/`specsReverifying`), and the firehose (`blastRadius`). +Everything else is a script you write. Freezing one-consumer traversals is how +the handle would quietly become the verb wall ADR-014 deleted. + +Every recipe below is a runnable `q` body. Save one to `playground/scratch/.ts` +and pipe it through `q` (the script bakes in `--conditions=source`): + +```bash +pnpm architect:q < playground/scratch/.ts +# …or inline: echo 'return g.patterns.length;' | pnpm architect:q +``` + +`q` injects `g` (the live handle), `inspect`, `execFileSync`, and `REPO_ROOT`, and runs +your script with cwd at the repo root. No imports, no `loadGraph()` boilerplate, and +`git`/path shell-outs are stable wherever you invoke it. Two rules, because the body is +compiled as a function body: (1) no `import`/`export` and no TS-only syntax +(type annotations, ``, `!`). It's plain JS at eval time. (2) End with +`return ` (inspect-printed) and/or `console.log`. + +Script over `g.patterns` (decoded `PatternNode[]`), `g.pattern(name)`, +`g.invariantsOf(x)`, `g.specsReverifying(x)`, `g.blastRadius(files)`, the entry adapters, +the complete frozen `g.graph`, the deterministic `g.fsm`, and the raw escape +hatches `g.mech` / `g.authored`. Read `packages/architect-core/src/graph/schema.ts` + +`graph.ts` for the shapes. + +> Import `Graph`, `createGraph`, schemas, types, and trusted pure views from +> `@libar-dev/architect-core/graph`. Named pure kernels such as +> `getDependencyContext` and `getRulesForPattern` come from `@libar-dev/architect-core`. +> Callers supply already-built graph values. Source/config/git IO belongs to their +> composition root. For ad-hoc live repository reads, the piped `q` form is still +> the entry point. + +--- + +## STATE. What is the state of X + +Pattern-state questions are direct reads. No verb needed: + +```js +// one pattern's decoded state (need-shaped) +return g.pattern('ProjectionBundle'); +// the full canonical record and direct deterministic reads: +// g.graph.patterns.find((p) => p.name === 'X') · g.graph.counts +// g.patterns.filter((p) => p.status === 'active') +// g.fsm.isValidTransition('roadmap', 'active') ← the FSM gate, one call +``` + +```js +// status distribution (the old `status` verb) +const byStatus = {}; +for (const p of g.patterns) byStatus[p.status] = (byStatus[p.status] ?? 0) + 1; +return byStatus; +``` + +```js +// workable: roadmap patterns whose deps are all completed (the old `arch workable`) +return g.patterns + .filter((p) => p.status === 'roadmap') + .filter((p) => p.uses.every((u) => g.pattern(u)?.status === 'completed')) + .map((p) => p.name); +``` + +--- + +## I1. If I change this pattern, what breaks? + +A thin transitive walk over the curated `usedBy` edges. For the exhaustive answer that +reaches dark files, use `g.blastRadius(files)`, the firehose. This is the curated-edge +version: the architecture's own answer, no mechanical graph. + +```js +function downstream(name) { + const seen = new Set(), + q = [name]; + while (q.length) + for (const u of g.pattern(q.shift())?.usedBy ?? []) + if (!seen.has(u)) { + seen.add(u); + q.push(u); + } + return [...seen]; +} +return downstream('ProjectionFragmentContracts').length; // → N patterns downstream (curated edges) +``` + +Leave it a script. One consumer, one already-structured field (`usedBy`). A short walk an +agent won't get wrong. Freezing it would add a verb that hides a for-loop. + +--- + +## MEMBERS. What is in this epic, and at what maturity? + +Epic→member membership (`@architect-parent`) is a first-class decoded field: `p.parent` +and its inverse `p.children`. An epic's member set is a direct read, the usual cut for a +design review of capability X. Group the members by maturity to see what is proven +(`executable`), still design, or still idea-tier. + +```js +const epic = g.pattern('DocumentationProjection'); +const order = { executable: 0, design: 1, plan: 2, idea: 3 }; +return epic.children + .map((n) => g.pattern(n)) + .sort((a, b) => order[a.maturity] - order[b.maturity] || a.name.localeCompare(b.name)) + .map( + (m) => + `[${m.maturity.padEnd(10)}] ${m.name} (${m.status}${m.implementedBy.length ? ', live test' : ''})`, + ) + .join('\n'); +``` + +`children` is an exposed field. Members by maturity is a `sort`/`map` over it, the +freeze-vs-script bar, not a method. + +--- + +## A1. How is this kind of thing done here? + +Filter by `role`, rank by `maturity` so the strongest precedent (an `executable`-proven +pattern) sorts first, and pull a sample invariant as the "what it guarantees" hint. + +```js +const order = { executable: 0, design: 1, plan: 2, idea: 3 }; +const precedents = g.patterns + .filter((p) => p.role === 'projection') + .sort((a, b) => order[a.maturity] - order[b.maturity] || a.name.localeCompare(b.name)) + .slice(0, 4); +for (const p of precedents) { + const inv = g.invariantsOf(p.name)[0]; + console.log(`[${p.maturity}] ${p.name} ${p.sourceFile ?? ''}`); + if (inv) console.log(` e.g. invariant: ${inv.text.slice(0, 80)}…`); +} +``` + +Precedent is the agent's to choose: by role, by context, or a fuzzy `findByConcept` first. +A verb would freeze one definition. The script lets the agent pick. `role` is populated but +coarse. Combine with `g.findByConcept(intent)` or a `boundedContext` filter to narrow. + +--- + +## A2. What context or seam am I extending? + +Group by the seam axis. `boundedContext` is the doctrine-correct seam and the denser field. +Use it. `productArea` is the coarser org axis. Fall back to it only where `boundedContext` +is absent. + +```js +const bySeam = new Map(); +for (const p of g.patterns) + if (p.boundedContext) + (bySeam.get(p.boundedContext) ?? bySeam.set(p.boundedContext, []).get(p.boundedContext)).push( + p.name, + ); +for (const [ctx, members] of [...bySeam].sort((a, b) => b[1].length - a[1].length)) + console.log(`${ctx.padEnd(26)} ${members.length} members`); +``` + +A one-line `groupBy` over an exposed field stays a recipe, never a method. + +--- + +## GUARANTEE. What does X guarantee? + +`g.invariantsOf(x)` returns `[]` for ~40% of patterns: the code-originated contracts +(`role:contract`/`codec`, a `.ts` source) whose guarantee is their TypeScript type, not a +Gherkin Rule block. Don't read that `[]` as "guarantees nothing." The follow-up below splits +the cases `[]` collapses: + +```js +function guaranteeOf(x) { + const inv = g.invariantsOf(x); + if (inv.length) + return { kind: 'invariants', count: inv.length, sample: inv[0].text.slice(0, 60) }; + const node = g.pattern(x) ?? g.pattern(g.fileToPattern(x) ?? ''); + if (!node) return { kind: 'unresolved', x }; // not a pattern, not a mapped .ts file + if (node.sourceFile?.endsWith('.ts')) + // code-originated contract → read the TYPE + return { kind: 'structural', role: node.role, typeAt: node.sourceFile }; + return { kind: 'none-yet', pattern: node.name }; // real .feature pattern, no Rule blocks yet +} +return [ + guaranteeOf('ProjectionBundle'), + guaranteeOf('ApiReferenceProjection'), + guaranteeOf('NoSuchPattern'), +]; +``` + +> `structural` is not "a contract never has invariants." It only means no Gherkin Rule reaches +> it. A code-originated contract realized by a live test returns real `executable` +> invariants, so the recipe calls `invariantsOf` first and never infers emptiness from +> `role`. Don't shortcut "it's a contract, so `[]`". Ask the graph. + +Leave it a script, not a handle method. It's a thin field-check over already-exposed fields, +not an irreducible cross-source join. Whether this earns a frozen `g.guarantee()` is an +ADR-010 second-real-caller question. The `invariants` CLI command is the first. If a second +programmatic caller appears, promote it. Until then, script it. + +--- + +## TRIAGE. Which annotations are noise, which need edges + +An annotated pattern carrying zero architectural-significance signal is one of two things. +Mechanical fan-in is the discriminator. Near-zero importers means true noise (REMOVE). Many +importers means load-bearing but under-annotated (ADD edges). Significance is any of: a +curated edge, a rule/scenario, a realization (`implements` OR `implementedBy`), a decision +enforced, `children` (it's a parent/epic), or a structural role. Those are all first-class +node fields, so the filter needs no escape hatch. + +```js +const STRUCTURAL = new Set(['contract', 'codec', 'decider', 'read-model']); +const fanIn = new Map(); +for (const e of g.mech.edges) + if (e.fromFile !== e.toFile) + (fanIn.get(e.toFile) ?? fanIn.set(e.toFile, new Set()).get(e.toFile)).add(e.fromFile); +return g.patterns + .filter( + (p) => + !p.uses.length && + !p.usedBy.length && + !p.ruleCount && + !p.scenarioCount && + !p.implements.length && + !p.implementedBy.length && + !p.enforcesDecisions.length && + !p.children.length && + !STRUCTURAL.has(p.role ?? '') && + p.sourceFile?.endsWith('.ts'), + ) + .map((p) => ({ name: p.name, role: p.role ?? '—', fanIn: fanIn.get(p.sourceFile)?.size ?? 0 })) + .sort((a, b) => a.fanIn - b.fanIn) + .map( + (t) => + `${String(t.fanIn).padStart(3)} imp ${t.name} [${t.role}] → ${t.fanIn <= 1 ? 'REMOVE? (noise)' : 'ADD edges? (load-bearing)'}`, + ) + .join('\n'); +``` + +Significance is the curator's definition to tune. A verb would freeze one policy. Mechanical +imports are evidence, not authored architecture. Dark imports default to no action. The ADD +side (an intentional dependency that merits a curated `uses` edge) is +`g.graphDiff().aspirational` / `pnpm architect:graph fan-in`, but each candidate still needs +the significance rubric and a human-readable architectural reason. This recipe is the REMOVE +side plus the load-bearing-but-edge-dark cross-check. + +--- + +## IMPACT. File-level impact is `blastRadius`, not `specsReverifying` + +A demand-map trap worth knowing: `g.specsReverifying([implFile])` can return `0` for a +real realizing impl file. That file's tests live on the cluster spec it implements, not on +a feature of its own, and `specsReverifying` walks a pattern's own plus +reverse-`implementedBy` scenarios, not the forward `implements` edge. For "I changed this +file, what re-verifies?", reach for `g.blastRadius([file]).atRiskSpecs` (exhaustive, +reaches the cluster via the mechanical graph) or seed `specsReverifying` with the pattern +name of what the file implements. + +```js +// file → at-risk specs (the reliable file-level form) +return g.blastRadius([ + 'packages/architect-projection/src/projections/documentation-composition/taxonomy-embedded.ts', +]).atRiskSpecs.length; +``` + +--- + +## DRIFT. What ran ahead of its design? + +A unit backed by a live test whose own design status still lags. The handle does not +fabricate this as a maturity label. Executable provenance is clamped to `executable` +maturity. A live verifier is the realization rung. The signal lives here instead, as a +deliberate query, where it is informative rather than contradictory. + +```js +// patterns realized by a live test (tests/features) but whose status is not yet `completed` +const realized = new Set(); +for (const p of g.patterns) + for (const i of g.authored.relationshipIndex[p.name]?.implementedBy ?? []) + if (i.file && i.file.includes('tests/features')) realized.add(p.name); +const drift = [...realized] + .map((n) => g.pattern(n)) + .filter((p) => p && p.status !== 'completed') + .map((p) => `${p.name} [${p.status}]`) + .sort(); +return `${drift.length} drift (live test ∧ status ({ p, inv: g.invariantsOf(p) })) + .filter(({ inv }) => inv.length && inv.every((i) => i.provenance === 'authored')); +return `${exposed.length} at-risk patterns rest only on authored (unproven) invariants`; +``` + +`execFileSync` and `REPO_ROOT` are injected. The explicit `cwd: REPO_ROOT` keeps it correct +even if you later lift it into a standalone file. Three primitives compose into a fourth +question, in-process, no envelope, ~⅕ the context of a verb round-trip. + +--- + +## ESCAPE HATCH. Raw shapes when no view fits + +Drop to `g.mech` / `g.authored` for anything the views don't cover. The mechanical graph is +right there. + +```js +const typeOnly = g.mech.edges.filter((e) => e.typeOnly).length; +return `${typeOnly}/${g.mech.edges.length} import edges are type-only`; +``` + +This is the whole bet: the agent is not limited to the view library. Views are a starting +toolkit. Raw authored and mechanical shapes are always one property away. + +--- + +## When does a recipe graduate to a handle method? + +Only when it clears both axes of the bar (ADR-014 §3): + +1. Many consumers (ADR-010's second-caller). Several other recipes need it first. +2. Irreducible join. It hides a sharp cross-source join an agent would hand-roll wrong + (the 2-hop `pattern→implementedBy→featureFile→rules` is the canonical example; a `groupBy` + over an exposed field is not). + +Reached often but still a thin traversal? Stays a recipe. Document it here. Hard join, one +consumer? Stays a recipe. Script it inline. Both at once and it's earned the handle. Nothing +else gets frozen. diff --git a/.agents/skills/architect-implement-spec/SKILL.md b/.agents/skills/architect-implement-spec/SKILL.md deleted file mode 100644 index b986cc6..0000000 --- a/.agents/skills/architect-implement-spec/SKILL.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -name: architect-implement-spec -description: MANDATORY when the user is implementing an Architect pattern from its design-level spec — triggers on: "implement" + pattern name, mentions of architect/specs/.feature or architect/specs/, scope-validate implement PASS, FSM transition to active, transferring value from stubs to JSDoc annotations and executable Gherkin, or deleting the ephemeral design spec. Also triggers on: building deliverables in listed order, @architect-pattern/@architect-implements annotations, transferring invariants to tests/features/. Do NOT use for: idea/candidate-tier sources where scope-validate is BLOCKED — route to architect-review-spec to surface the blocker first. Also do NOT use for generic code implementation without a spec, refactoring already-shipped code (annotate the existing executable feature instead), bugfixes, one-off prototypes, or idea/candidate-tier specs (route to architect-plan-session for promotion through plan and design tiers first). Invoke BEFORE writing any production code for an Architect pattern; the spec IS the implementation prompt, do not create wrapper documents. -allowed-tools: - - Bash - - Read - - Write - - Edit - - Glob - - Grep ---- - -# Architect Implementation Session - -The design-level `.feature` is your implementation prompt. The stubs encode -shape decisions. Together they specify exactly what to build. This session -ends with the spec's value living in production code and executable specs; -deletion of the design spec is a separate decision (see "Deletion" below). - -## Value Transfer (concept) - -Design-level specs and stubs are **ephemeral scaffolding**. The -durable artifacts after this session are: (a) executable Gherkin in -`tests/features/**/*.feature` carrying `@architect-implements:` -and the rule content, and (b) — additively — JSDoc `@architect-*` -annotations on production code. The executable feature is the -**canonical pattern definition** per the split-ownership policy; -production-TS annotations enrich discoverability but do not gate -completion. Full doctrine, transfer checklist, and pre-deletion gate: -[`../_shared/value-transfer.md`](../_shared/value-transfer.md). - -Related references this skill assumes: - -- [`../_shared/annotation-ownership.md`](../_shared/annotation-ownership.md) - — split-ownership: code stubs MUST NOT use `@-pattern`; - production-TS annotations are additive. -- [`../_shared/spec-pattern-relationships.md`](../_shared/spec-pattern-relationships.md) - — bipartite production↔test pattern naming - (`Testing` / `ExecutableTests`); forward/reverse - link pair. -- [`../_shared/fsm-transitions.md`](../_shared/fsm-transitions.md) — - valid FSM transitions and `@architect-unlock-reason:` requirements. - -## Pre-flight (mandatory CLI bootstrap) - -Run the canonical implement pre-flight from -[`../architect-data-api/SKILL.md`](../architect-data-api/SKILL.md) §"Implement" -— it covers `overview`, the `scope-validate` gate, the implement-mode -`bundle`, `files`, `rules --only-invariants`, and the `query isValidTransition` -FSM gate. - -If `scope-validate implement` is not PASS, stop. Either the design -is incomplete (route to `architect-design-session`) or a dependency is blocked -(route to `architect-review-spec` to find the blocker). - -## Implementation order (strict) - -1. **Transition FSM to active** before any code change: - ```bash - pnpm architect:query query isValidTransition active - ``` - The verb returns a deterministic verdict — proceed only if it confirms - the transition is valid. See - [`../architect-data-api/SKILL.md`](../architect-data-api/SKILL.md) - §"Deterministic gates" for the JSON shape. Then bump `@architect-status` - from `roadmap` to `active` in the spec via your normal edit flow. See - [`../_shared/fsm-transitions.md`](../_shared/fsm-transitions.md) for - the full Process-Guard transition table and the - `@architect-unlock-reason:` rules for unusual transitions. -2. **Read all deliverable target files** listed in the spec's Background table. -3. **Read the stubs** — they encode design decisions (DD-N) and "When to Use" guidance. -4. **Implement deliverables in the order listed**, guided by Rules + Scenarios. -5. **After each deliverable:** run the closest targeted typecheck / test - slice for the files you just touched, then run `pnpm typecheck` - before the next phase boundary. Before any commit or handoff, run - `pnpm typecheck && pnpm test && pnpm validate:all`. Do not batch - verification to the end. -6. **Author / refine executable Gherkin** under `tests/features/` as you go — - transferring the design Scenarios with `**Invariant:** / **Rationale:** / -**Verified by:**` blocks intact. To enumerate just the invariants that need - to land, use `pnpm architect:query rules --pattern --only-invariants`. -7. **Add `@architect-*` JSDoc annotations** to every production file you create - or modify — at minimum `@architect-implements:` (the realization - edge). Production code MUST NOT carry `@architect-pattern` — pattern - identity belongs to the feature file per - [`../_shared/annotation-ownership.md`](../_shared/annotation-ownership.md). - Add `@architect-uses` / `@architect-usecase` / `@architect-decision` / - `@architect-role` / `@architect-bounded-context` as additive enrichment - where they help discoverability. Reverse edges derive from the declared - `@architect-uses` targets, they are not authored directly. -8. **When ALL deliverables complete:** transition the spec to `completed`, - regenerate docs, then perform the value-transfer-and-delete step (next). - -## Value transfer (verify before deletion) - -Walk the **Pre-deletion gate** in -[`../_shared/value-transfer.md`](../_shared/value-transfer.md) before -proposing deletion. The gate has five criteria (forward link present, -forward link resolves, reverse link present, rich content has landed, -architecturally significant rationale lives in JSDoc where Gherkin -can't carry it). When the `pnpm architect:query value-transfer ` verb -ships (per -`architect/specs/value-transfer-state.feature`), it -returns the same gate's verdict as a deterministic -`deletionReady: boolean` — until then, walk the criteria manually. - -The transfer checklist (rule → executable Gherkin Rule block, stub -"When to Use" → JSDoc, etc.) lives in -[`../_shared/value-transfer.md`](../_shared/value-transfer.md) -§"Transfer checklist". Every line of the design spec that won't -transfer is dead weight — either it transfers, or recognize it was -never worth writing. - -## Deletion (ask the user first) - -Two valid outcomes. **Default: ask the user** which one applies. - -- **Delete now** — appropriate when this session reviewed the value - transfer thoroughly and the pattern is the only one being reviewed. -- **Defer to code review** (more common) — appropriate when several - related implementations are being reviewed together. The reviewer - uses `architect-review-implementation` to verify value transfer - across the related set and batches the spec deletions in a single - PR or review pass. - -Phrase the prompt to the user something like: "Value transfer is -verified for ``. Delete the design spec now, or defer to code -review where related implementations are batched (the more common -path)?" - -If the user authorizes deletion now: - -```bash -git rm architect/specs/.feature # delete the design spec -git rm -r architect/stubs// # if stubs directory exists -pnpm architect:query overview # confirm pattern shows completed -pnpm docs:all # regenerate docs -``` - -If the user defers to code review: - -- Leave the design spec and stubs in place. -- In your handoff note, name `architect-review-implementation` as the - recommended next skill for the reviewer. - -If you cannot transfer value because something still depends on it, -that's a **zombie spec** smell — investigate. Either the dependency -is wrong, or the spec is doing something durable it shouldn't be -doing. - -## Anti-patterns (stop and redirect) - -- **Wrapper documents.** Do not create a "context" or "session-prep" markdown - alongside the spec. The spec is the prompt. -- **Retroactive specs at any tier.** If you discover code that already - implements the pattern, do not author a fresh idea, candidate, plan, or - design-level spec for it. Every tier of the four-tier ladder describes - _planned_ work — conjuring an ephemeral spec back to "cover" shipped - behavior inverts the pipeline and leaves a zombie behind. Tag an existing - executable feature with `@architect-implements:` and enrich its - rich content (`**Invariant:**` / `**Rationale:**` / `**Verified by:**` on - rules) instead. Refactoring carve-out: when capturing behavior of code that - already exists, skip directly to design or executable level — never via - idea, candidate, or plan tier. See `formal-spec/08-spec-evolution.md` - § "Anti-Patterns" ("Exception: Refactoring specs"). -- **Zombie design specs.** Leaving the design spec in `architect/specs/` after - implementation is a lie at worst, noise at best. -- **Half-transferred value.** Transferring rules to executable specs but not to - annotations (or vice versa) leaves the architectural picture incomplete. - -## Big-gap escape hatch - -If during implementation you discover the design has a major gap that requires -new architectural decisions (not just clarifications), **stop**. Do not paper -over it. Report the gap to the user and recommend re-entering -`architect-design-session` or `architect-review-spec`. Shipping an -under-specified design as code is worse than reopening the design conversation. - -## Do not - -- Do not skip the FSM transition to `active` before coding. -- Do not delay annotations to a follow-up PR — they are part of the implementation. -- Do not declare done without value transfer + spec/stub deletion. -- Do not introduce backward-compatibility shims (no `@deprecated`, no - `// eslint-disable`, no `@ts-expect-error`, no re-export aliases). The - No-BC guard will fail CI. diff --git a/.agents/skills/architect-plan-session/SKILL.md b/.agents/skills/architect-plan-session/SKILL.md deleted file mode 100644 index 08890f1..0000000 --- a/.agents/skills/architect-plan-session/SKILL.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -name: architect-plan-session -description: Use when capturing a new idea, refining a candidate spec, or deciding what to build next in the Architect platform. Enforces the minimum-Gherkin-by-tier philosophy — idea specs are ≤30 lines (warn-only soft budget) with 6 tags and invariant-only rules, candidates add open questions and a single happy-path scenario, plan and design tiers come later. Prevents the verbose-spec anti-pattern. Do NOT use for: design-tier work (stubs, deliverables tables, exhaustive scenarios — route to architect-design-session), implementing already-shipped code (retroactive specs are forbidden — route to architect-implement-spec to enrich an existing executable feature instead), or generic product brainstorming outside the Architect pattern model. -allowed-tools: - - Bash - - Read - - Write - - Edit - - Glob - - Grep ---- - -# Architect Plan-Tier Session - -You are at the lightest tier of spec authoring. The single most common failure -mode is **producing a verbose, deliverables-loaded spec for an idea that has -not been committed to delivery**. Resist it. - -## Doctrine references - -This skill operates under shared references — read them once per -session if you haven't: - -- [`../_shared/four-tier-ladder.md`](../_shared/four-tier-ladder.md) — - tier table, mandatory tags, valid promotion paths. -- [`../_shared/rule-block-template.md`](../_shared/rule-block-template.md) - — at idea tier, `**Invariant:**`-only Rule blocks; the full 4-field - template applies only at plan tier and below. -- [`../_shared/spec-pattern-relationships.md`](../_shared/spec-pattern-relationships.md) - — when the "what to capture" is for code that already ships, route - to the `*ExecutableTests` escape hatch instead of authoring an - idea/candidate/plan-tier spec for it (this is the formal exit from - the retroactive-plan-level-spec anti-pattern). -- [`../_shared/canonical-references.md`](../_shared/canonical-references.md) - — anti-anecdote rule; defer to the live taxonomy - (`pnpm architect:query taxonomy --format json`) and `formal-spec/` - over sample notes. - -## Pre-flight - -You are here because the router selected `planning` intent. Run the canonical -planning pre-flight from -[`../architect-data-api/SKILL.md`](../architect-data-api/SKILL.md) §"Planning" -— it covers `overview`, `list --status candidate`, `open-questions`, and -`context --session planning`. - -Note: `scope-validate` only accepts `design` or `implement`. There is no -`scope-validate planning`. Skip it at this tier — idea/candidate -readiness is structural (see the four-tier ladder below). - -## Four-Tier Ladder - -Canonical reference: [`../_shared/four-tier-ladder.md`](../_shared/four-tier-ladder.md). -Read it once for the tier table, mandatory tags, `DEFAULT_MATURITY_BY_STATUS` -defaults, and the valid promotion paths, then return here for plan-tier -authoring guidance. - -This skill operates on the **first two rungs** of the ladder — idea and -candidate. Plan and design tiers belong to other skills. - -### Five-tag idea-tier minimum - -An idea-tier spec carries five authored tags. `@architect-maturity` is derived -and must not be written on source; the tier is conveyed by file location and -the minimum idea-tier shape. - -1. `@architect` — the gate tag -2. `@architect-pattern:` -3. `@architect-status:candidate` -4. `@architect-product-area:` -5. `@architect-parent:` - -Any additional tag at idea tier is a smell, **except** `@architect-level:epic` or `@architect-level:slice` — those are structural and exempt the file from the `@architect-parent` requirement. - -## Idea-tier template (write exactly this shape, no more) - -Location: `architect/specs/ideas/.feature`. - -```gherkin -@architect -@architect-pattern: -@architect-status:candidate -@architect-product-area: -Feature: - - - **User Story:** As a , I want so that . - - Rule: - **Invariant:** -``` - -That is the ENTIRE shape at idea tier. Five authored tags, one user story, one -rule with one invariant. Add a second rule only if the idea genuinely encodes -two distinct constraints. - -### Epic / slice variants - -When the file groups other patterns (epic) or saves a multi-pattern view (slice), add `@architect-level:epic` or `@architect-level:slice` and drop `@architect-parent`. Use these shapes: - -**Epic:** - -```gherkin -@architect -@architect-pattern: -@architect-status:candidate -@architect-product-area: -Feature: - - - **User Story:** As , we want so that . - - **Members:** - - - - - - Rule: - **Invariant:** -``` - -**Slice:** same as epic with `@architect-level:slice` and a `**Usage:**` line under the members. Slices live in `architect/slices/.feature`, not `architect/specs/ideas/`. - -To list the members of an existing epic directly from the graph (instead of -hand-tracking them in the `**Members:**` bullet list), run -`pnpm architect:query list --parent --names-only`. Unknown parent -names exit non-zero with `Parent pattern not found: `. - -## Candidate-tier delta (add only when promoting from idea) - -Idea shape plus: - -```gherkin - **Open Questions:** - - - - - - @acceptance-criteria @happy-path - Scenario: - Given - When - Then -``` - -The promotion delta is mechanical: `git mv` the file from -`architect/specs/ideas/.feature` to -`architect/specs/candidates/.feature`, add the `**Open Questions:**` -block, and add 1-2 happy-path scenarios. `@architect-status` stays `candidate` -until the acceptance gate promotes the spec to `roadmap` (which becomes the -plan tier). - -## Anti-patterns at idea tier (block these aggressively) - -The following five rules are the idea-tier anti-pattern set codified in `formal-spec/08-spec-evolution.md` § "Anti-Patterns at Idea Tier" and inlined in [`../_shared/four-tier-ladder.md`](../_shared/four-tier-ladder.md). They are non-negotiable at the idea tier of the four-tier ladder: - -- **Do not add deliverables.** Ideas are not committed to files. -- **Do not add phase/effort/priority.** Planning metadata means commitment. -- **Do not add ADRs.** If an idea requires a decision, note it in the parent epic, not here. -- **Do not write narrative descriptions.** One-line Feature description only. If you need more than one line, the idea is ready for candidate tier. -- **Do not enumerate scenarios.** Rules with invariants are sufficient at idea tier. - -### Additional anti-patterns (this skill, applies to all planning-tier work) - -- **No `**Rationale:**`or`**Verified by:**` on rules at idea tier.** Those are plan-tier additions. -- **No retroactive plan-level specs.** If you discover code that already implements the idea, do NOT author a plan-level spec for it. Tag an existing executable feature with `@architect-implements:` and enrich it. Plan-level specs are for _planned_ work only. - -> **Tripwire — retroactive plan-level specs.** This is the single most common -> failure mode of this session type. If the validator reports missing Gherkin -> coverage for a pattern that is _already shipping_, the correct fix is to tag -> an existing executable feature with `@architect-implements:` and -> enrich its rich content — never to author a fresh plan-level spec in -> `architect/specs/`. A plan-level spec is supposed to die after implementation; -> conjuring one back to "cover" shipped behavior inverts the pipeline and -> leaves a zombie spec behind. Refactoring exception: when backfilling coverage -> for code that already exists, skip to design-level or executable tier -> directly. Never via plan-level. See `formal-spec/08-spec-evolution.md` -> § "Anti-Patterns" ("Exception: Refactoring specs"). - -## Promotion deltas - -Full promotion table lives in [`../_shared/four-tier-ladder.md`](../_shared/four-tier-ladder.md) -under "Valid promotion paths". The plan-session skill is responsible for the -**Idea → Candidate** transition only: - -- Add `**Open Questions:**` block + 1-2 happy-path scenarios -- `git mv architect/specs/ideas/.feature architect/specs/candidates/.feature` -- `@architect-status` stays `candidate` until the acceptance gate later - -Candidate → Plan and Plan → Design promotions are out of scope here — they -edit in place, and they belong to subsequent sessions. - -## Output for this session - -Either: - -- (a) you authored a fresh idea spec under `architect/specs/ideas/`, or -- (b) you promoted an existing idea to candidate tier (added open questions + one scenario, moved it to `architect/specs/candidates/`), or -- (c) you decided not to write anything yet — refining intent in conversation is a valid outcome at this tier. - -If (c), say so explicitly and recommend the user re-invoke when ready. - -## Do not - -- Do not invoke `architect-design-session` from here. Promotion to design tier - is a separate decision and a separate session. -- Do not author scenarios at idea tier even if the user asks for them — promote - to candidate first, with the explicit track flip. -- Do not skip the dogfooding feedback step from the router skill. diff --git a/.agents/skills/architect-refactor-session/SKILL.md b/.agents/skills/architect-refactor-session/SKILL.md index 3a0efa4..824cc20 100644 --- a/.agents/skills/architect-refactor-session/SKILL.md +++ b/.agents/skills/architect-refactor-session/SKILL.md @@ -1,6 +1,6 @@ --- name: architect-refactor-session -description: MANDATORY when modifying shipped code WITHOUT a design-level Architect spec — triggers on "refactor", "rename", "extract", "inline", "consolidate", "split package", "move file", "tidy up", "clean up shipped code", or any change to production files for an Architect pattern whose status is `completed` and whose design spec has already been deleted. Operationalizes the kernel's refactoring carve-out — skip the four-tier ladder, evolve the existing executable feature in place (or create a `ExecutableTests` feature if none exists), preserve every documented invariant unless `.pr-coordination/DECISIONS.md` authorizes a change. Multi-session refactor campaigns (touching ≥3 packages) coordinate through `.pr-coordination/` per the canonical layout. Do NOT use for implementing a design-level spec (route to architect-implement-spec — refactor never authors a new plan-level spec for shipped code), bug fixes that restore a documented invariant (just patch + add scenario, no carve-out needed), or feature work that needs a fresh pattern (route to architect-plan-session). Invoke BEFORE any production-code edit on shipped patterns. +description: MANDATORY when modifying shipped code that has NO design-level Architect spec. Triggers on refactor, rename, extract, inline, consolidate, split-package, move-file, or any production-code edit on a `completed` pattern whose design spec was already deleted. Operationalizes the kernel's refactoring carve-out. Skip the four-tier ladder, evolve the existing executable feature in place, preserve documented invariants unless `.pr-coordination/DECISIONS.md` authorizes a change. Invoke before the edit. Do NOT use for implementing a design spec, bug fixes that restore an invariant, or feature work needing a fresh pattern. Those route to architect-sessions. DO NOT USE for spec-driven development. allowed-tools: - Bash - Read @@ -10,131 +10,184 @@ allowed-tools: - Grep --- -# Architect Refactor Session +# Architect refactor session Refactor sessions modify shipped code that has no design-level -`.feature` spec — the spec was deleted at original implement-time, and +`.feature` spec. The spec was deleted at original implement-time, and the executable Gherkin in `tests/features/` is now the canonical -pattern definition. There is nothing to "implement from"; there is +pattern definition. There is nothing to "implement from". There is existing code to evolve and an existing executable feature whose invariants must continue to hold (or be deliberately changed under a -recorded decision). +recorded decision). **This skill is only for non-spec-driven development. DO NOT USE for refactoring based on a design-level spec.** -## Premise — value transfer without a spec +## Premise: value transfer without a spec The kernel's value-transfer doctrine still applies, but the source has inverted. A normal implement session transfers value FROM an ephemeral -design spec INTO durable carriers (executable Gherkin + annotations); -a refactor session transfers value FROM existing durable carriers +design spec INTO durable carriers (executable Gherkin + annotations). +A refactor session transfers value FROM existing durable carriers THROUGH the code edit AND BACK INTO the same carriers, possibly evolved. The pre-deletion gate from -[`../_shared/value-transfer.md`](../_shared/value-transfer.md) does -not apply — there is no spec to delete — but the **invariant carriers** +[`../architect-sessions/references/ephemeral-spec-deletion.md`](../architect-sessions/references/ephemeral-spec-deletion.md) does +not apply. There is no spec to delete. The **invariant carriers** still gate completion. Use the adapted gate below in §"Adapted invariant-carrier gate". ## Doctrine references -- [`../_shared/session-preamble.md`](../_shared/session-preamble.md) - — six universal rules. Rule 5 (incomplete scope is next-session - input) is load-bearing: refactors concentrate the "scope expands - mid-session" risk more than any other session type. -- [`../_shared/multi-session-coordination.md`](../_shared/multi-session-coordination.md) - — `.pr-coordination/` layout, coordinator/worker split, and the - scope-discovery rule. Required when the refactor touches ≥3 - packages or spans ≥3 sessions. -- [`../_shared/four-tier-ladder.md`](../_shared/four-tier-ladder.md) - — refactoring carve-out: skip idea / candidate / plan tiers. Never - author a retroactive spec for shipped code. -- [`../_shared/spec-pattern-relationships.md`](../_shared/spec-pattern-relationships.md) - — `ExecutableTests` is the formal escape hatch when shipped - code lacks a `tests/features/.feature`. Bipartite naming - applies. -- [`../_shared/annotation-ownership.md`](../_shared/annotation-ownership.md) - — split-ownership policy: production code MUST NOT add - `@architect-pattern`. Add `@architect-uses` / +Load [`architect-base`](../architect-base/SKILL.md) (vocabulary) and [`architect-sessions`](../architect-sessions/SKILL.md) (the universal session rules + value-transfer concept) first. This skill builds on both. The depth this session leans on: + +- [`./references/multi-session-coordination.md`](./references/multi-session-coordination.md) + `.pr-coordination/` layout, coordinator/worker split, the campaign + rules, and the scope-discovery rule (Rule 5. Load-bearing: refactors + concentrate the "scope expands mid-session" risk more than any other + session type). Required when the refactor touches ≥3 packages or + spans ≥3 sessions. +- [`../architect-base/references/four-tier-ladder.md`](../architect-base/references/four-tier-ladder.md) + The maturity ladder this carve-out skips (base owns the rungs). The + carve-out itself, skip idea / candidate / plan and capture + already-shipped behavior at executable-tier (a `*ExecutableTests` + feature, or evolve the one in place) rather than revive the deleted + design spec, is this skill's own subject (see Premise, Refactor + order, Anti-patterns below). (Provenance: + `formal-spec/08-spec-evolution.md` § "Exception: Refactoring specs" + lets a refactoring spec skip candidate and plan, going to design-level + _or_ executable; this skill narrows that to the executable + `*ExecutableTests` convention. See Anti-patterns.) +- [`../architect-base/references/spec-pattern-relationships.md`](../architect-base/references/spec-pattern-relationships.md) + `ExecutableTests` is the formal escape hatch when shipped + code lacks a `tests/features/.feature`. Bipartite naming applies. +- [`../architect-base/references/annotation-ownership.md`](../architect-base/references/annotation-ownership.md) + Split-ownership policy: production code realizing a feature-owned + pattern uses `@architect-implements`, not a duplicate `@architect-pattern` + (but a code-originated pattern, codec / contract / utility, owns its + `@architect-pattern` on the `.ts`). Add `@architect-uses` / `@architect-usecase` / `@architect-decision` / - `@architect-role` / `@architect-bounded-context` as additive enrichment only. -- [`../_shared/rule-block-template.md`](../_shared/rule-block-template.md) - — 4-field `Rule:` template (`**Invariant:**` / `**Rationale:**` / + `@architect-role` / `@architect-bounded-context` as additive enrichment. +- [`../architect-base/references/rule-block-template.md`](../architect-base/references/rule-block-template.md) + 4-field `Rule:` template (`**Invariant:**` / `**Rationale:**` / `**Verified by:**`) for any new or modified Rule block in the executable feature. -- [`../_shared/value-transfer.md`](../_shared/value-transfer.md) — - invariant-carrier rules and anti-patterns (zombie spec, +- [`../architect-sessions/references/ephemeral-spec-deletion.md`](../architect-sessions/references/ephemeral-spec-deletion.md) + Invariant-carrier rules and anti-patterns (zombie spec, half-transferred value, retroactive plan-level spec). Skip §"Pre-deletion gate"; honor §"Anti-patterns". -- [`../_shared/fsm-transitions.md`](../_shared/fsm-transitions.md) — - consult only when the refactor reopens a `completed` pattern - (`completed` → `active` requires `@architect-unlock-reason:` ≥10 - non-placeholder characters). Most refactors never change status. +- [`../architect-base/references/fsm-transitions.md`](../architect-base/references/fsm-transitions.md) + Consult only when the refactor reopens a `completed` pattern + (`completed` → `active` is advisory; `@architect-unlock-reason:` ≥10 + non-placeholder characters suppresses the warning). Most refactors never change status. ## Pre-flight (mandatory CLI bootstrap) -`scope-validate` is intentionally absent — the verb only accepts -`design` or `implement` and refactors have no spec to validate. +Scope validation is intentionally absent. Scope readiness +(the `architect_scope_validate` MCP tool) only covers `design` or +`implement` sessions and refactors have no spec to validate. + +Run the graph-handle pre-flight per +[`../architect-graph-handle/SKILL.md`](../architect-graph-handle/SKILL.md) +(ADR-014). For a refactor that means: -Run the canonical refactor pre-flight from -[`../architect-data-api/SKILL.md`](../architect-data-api/SKILL.md) §"Refactor" -— it covers `overview`, `context --session implement` (current surface), -`files` (touched-file inventory), `dep-tree` (blast radius), `arch blocking`, -and `arch dangling --baseline ... --strict` (the graph-integrity gate used in -the closing checks below). +- **Orientation.** + `pnpm architect:q 'return {counts: g.graph.counts, active: g.patterns.filter(p => p.status === "active").map(p => p.name)}'` +- **Touched-file inventory.** + `pnpm architect:q 'const p = g.pattern(""); return {file: p?.sourceFile, realizing: p?.implementedBy}'` +- **Dependency context.** + `pnpm architect:q 'g.graph.relationshipIndex[""]'` +- **Blocked work.** + `pnpm architect:q 'g.patterns.filter(p => p.status === "roadmap" && p.uses.some(u => g.pattern(u)?.status !== "completed")).map(p => p.name)'` +- **Graph-integrity gate** (also used in the closing checks below): + `pnpm architect:graph dangling --baseline packages/architect-guard/src/lint/dangling-baseline.json --strict` -If `pnpm architect:query` returns no rows for the pattern (the pattern is +If `g.pattern("")` returns `undefined` (the pattern is unknown to the graph), stop. Either the pattern name is wrong, or the -work is feature work disguised as refactor — route to -`architect-plan-session`. +work is feature work disguised as refactor. Route to +[`architect-sessions`](../architect-sessions/SKILL.md) and its +[`plan`](../architect-sessions/references/plan.md) reference. ## Refactor order (strict) 1. **Identify the executable feature.** Locate the file under `tests/features/` carrying `@architect-implements:` (use - `files ` and the `context --session implement` output). + the pre-flight inventory one-liner. `g.pattern("")` + exposes `sourceFile` and `implementedBy`). If absent, create it as `tests/features//-executable-tests.feature` per - [`../_shared/spec-pattern-relationships.md`](../_shared/spec-pattern-relationships.md); + [`../architect-base/references/spec-pattern-relationships.md`](../architect-base/references/spec-pattern-relationships.md); tag it with `@architect-pattern:ExecutableTests` and `@architect-implements:`. The new file is the durable - artifact — never substitute a retroactive design-level spec. + artifact. Never substitute a retroactive design-level spec. 2. **Read before edit.** Read the executable feature first; read every - production file listed by `files `; read `dep-tree -` to understand the blast radius. Do not skim. + production file the inventory one-liner lists (`sourceFile` + + `implementedBy`); read the + `pnpm architect:q 'g.graph.relationshipIndex[""]'` output + to understand the blast radius. Do not skim. 3. **Capture decisions before code.** Any invariant the refactor intends to change must be entered in `.pr-coordination/DECISIONS.md` (or, for solo-session refactors, the working note the user accepts) BEFORE the production-code edit lands. Refactor's most - common drift mode is "the invariant looks wrong, just rewrite it"; - this gate stops that. + common drift mode is "the invariant looks wrong, just rewrite it". + This gate stops that. 4. **Edit production code in dependency-leaf-first order.** After each edit, run the closest targeted typecheck / test slice for the - surface you changed, then run `pnpm typecheck` at the next phase + files you changed, then run `pnpm typecheck` at the next phase boundary. Before any commit or handoff, run `pnpm typecheck && - pnpm test && pnpm validate:all`. Do not batch verification to the - end. Per - [`../_shared/session-preamble.md`](../_shared/session-preamble.md) - Rule 2, gates are non-negotiable. +pnpm test && pnpm validate:all`. Do not batch verification to the + end. Per [`architect-sessions`](../architect-sessions/SKILL.md) + §"Universal session rules", gates are non-negotiable. 5. **Update executable Gherkin in lockstep with code.** Every changed - behavior must surface as a new or edited Scenario; every changed - invariant must surface in the corresponding Rule block carrying + behavior must appear as a new or edited Scenario; every changed + invariant must appear in the corresponding Rule block carrying the full 4-field content from - [`../_shared/rule-block-template.md`](../_shared/rule-block-template.md). + [`../architect-base/references/rule-block-template.md`](../architect-base/references/rule-block-template.md). A previously-documented invariant that no longer holds requires a - matching `DECISIONS.md` entry — no silent rewrites. + matching `DECISIONS.md` entry. No silent rewrites. 6. **Refresh `@architect-*` annotations.** On every production file touched, update declared `@architect-uses` edges when dependency direction changed; refresh `@architect-usecase` if the "when to use" guidance shifted; add or update `@architect-decision:DD-N`, `@architect-role`, and `@architect-bounded-context` where the refactor changed those semantics. Reverse edges derive from `@architect-uses`, - they are not authored directly. Production code MUST NOT add - `@architect-pattern` (per - [`../_shared/annotation-ownership.md`](../_shared/annotation-ownership.md)). + they are not authored directly. Do not add a duplicate + `@architect-pattern` for a feature-owned pattern (use + `@architect-implements`); a code-originated pattern keeps its own + `@architect-pattern` on the `.ts` (per + [`../architect-base/references/annotation-ownership.md`](../architect-base/references/annotation-ownership.md)). + +## Edge-authoring heuristics (refactor-specific) + +Two recurring refactor cases are easy to get wrong because the truthful +edge is not the most obvious-looking one. + +- **Produced fragments.** When a projection or builder genuinely + constructs a fragment (for example its return type / `kind:` literal + proves it produces `PatternDetail`), author the edge on the producer: + ` @architect-uses `. Do **not** hang the edge on a + pure re-export barrel when a truthful producer exists. That inverts + the dependency and lies to the graph. +- **Producerless grouping barrels.** When a barrel is only a module + grouping file and no truthful producer exists, `barrel → +submodule` edges are acceptable. Verify against the barrel's actual + exports/imports; if there is no concrete dependency to point at, + defer rather than invent a phantom edge. +- **CLI subprocess tests.** An executable feature that drives the CLI + through `runCommand("foo ...")` may + `@architect-implements:` when the command string + maps **1:1** to one named production pattern. The command invocation + is the concrete fact that authorizes the edge. If the command fans out + across several patterns or no single production pattern exists, defer + rather than guess. + +Read back every such edge through the graph handle after authoring. +The file edit is not proof until +`pnpm architect:q 'g.pattern("")'` (the node carries +`uses`/`usedBy`) or `g.graph.relationshipIndex[""]` shows +the intended relationship in the live graph. ## Adapted invariant-carrier gate The five criteria below replace the §"Pre-deletion gate" in -[`../_shared/value-transfer.md`](../_shared/value-transfer.md). All +[`../architect-sessions/references/ephemeral-spec-deletion.md`](../architect-sessions/references/ephemeral-spec-deletion.md). All five must hold before declaring the refactor done. 1. **Executable feature present.** A file under `tests/features/` @@ -151,37 +204,43 @@ five must hold before declaring the refactor done. record). 4. **Annotations refreshed.** Every production file touched carries the additive `@architect-*` annotations expected by split - ownership. No new `@architect-pattern` on production code; no - stale `@architect-uses` referencing removed dependencies. -5. **Graph integrity.** `dep-tree ` after-state matches the - refactor's intent — no surprise edges. `arch blocking` shows no - new blockers introduced by the refactor. (Run both verbs again - after the final commit.) Use - `pnpm architect:query arch dangling --baseline packages/architect-guard/src/lint/dangling-baseline.json --strict` - as the deterministic graph-integrity gate — non-zero exit means the + ownership. No `@architect-pattern` that _duplicates_ a feature-owned + pattern's identity (use `@architect-implements`), though an extracted + code-originated pattern (codec / contract / utility) does own its + `@architect-pattern` on the `.ts`; no stale `@architect-uses` + referencing removed dependencies. +5. **Graph integrity.** The + `g.graph.relationshipIndex[""]` after-state matches the + refactor's intent. No surprise edges. The blocked-work script + (`pnpm architect:q 'g.patterns.filter(p => p.status === "roadmap" && p.uses.some(u => g.pattern(u)?.status !== "completed")).map(p => p.name)'`) + shows no new blockers introduced by the refactor. (Run both reads + again after the final commit.) Use + `pnpm architect:graph dangling --baseline packages/architect-guard/src/lint/dangling-baseline.json --strict` + as the deterministic graph-integrity gate. Non-zero exit means the refactor introduced (or removed) a dangling reference and the drift must be resolved before declaring done. When all five hold, the refactor is durable. **No spec deletion -step** — the executable feature was already the durable artifact and +step.** The executable feature was already the durable artifact and remains in place. ## Multi-session campaign mode When `.pr-coordination/` carries an active campaign (per -[`../_shared/multi-session-coordination.md`](../_shared/multi-session-coordination.md)): +[`./references/multi-session-coordination.md`](./references/multi-session-coordination.md)): - Defer to `EXECUTION-PLAN.md` for ordering, gates, and closing invariants. -- Read the matching `sessions/NN-slug.md` worker prompt — execute +- Read the matching `sessions/NN-slug.md` worker prompt. Execute exactly that scope; do not re-plan. - Append a tight per-session entry to `SESSION-REPORTS-AND-LEARNINGS.md` at session end, including any - drift surfaced and how it was classified (same-root-cause vs - different-root-cause per Rule 5 of the session preamble). + drift found and how it was classified (same-root-cause vs + different-root-cause per Rule 5 in + [`./references/multi-session-coordination.md`](./references/multi-session-coordination.md)). - Do not edit `EXECUTION-PLAN.md`, `state.json`, or unstarted session prompts under `sessions/`. The coordinator owns those. - Coordinator self-restraint is the load-bearing primitive — a + Coordinator self-restraint is the load-bearing rule. A worker that rewrites the plan becomes another coordinator and collapses the split. @@ -190,20 +249,24 @@ When `.pr-coordination/` carries an active campaign (per - **Retroactive plan-level spec.** Authoring a fresh idea / candidate / plan / design-level `.feature` for shipped code. Stop. Author or enrich a `ExecutableTests` feature instead. This is the - single most common refactor mistake — there is no spec because + single most common refactor mistake. There is no spec because there should be no spec. - **Silent invariant change.** Editing a Rule block's `**Invariant:**` line without a `DECISIONS.md` entry. Revert the edit, capture the decision, then re-apply. - **Half-transferred value.** Code edited but executable Gherkin not - updated, or vice versa. Both surfaces must move together — running + updated, or vice versa. Code and executable Gherkin must move together. Running only targeted slices, or only `pnpm typecheck`, is not a substitute for updating the carrier. -- **Pattern identity in code.** Adding `@architect-pattern` to a - production-TS file. Pattern identity belongs to the feature file - per - [`../_shared/annotation-ownership.md`](../_shared/annotation-ownership.md); - refactor never moves it. +- **Duplicating feature-owned identity in code.** Adding + `@architect-pattern:X` to production-TS for a pattern `X` a feature + file already owns. Use `@architect-implements:X` instead; a refactor + never _moves_ a behavioral pattern's identity off its feature + (per + [`../architect-base/references/annotation-ownership.md`](../architect-base/references/annotation-ownership.md)). + This does **not** bar a code-originated pattern, codec / contract / + utility, including one an `extract` refactor creates, from owning its + own `@architect-pattern` on the `.ts`, as such patterns always have. - **Zombie executable feature.** Stripping every Scenario from a feature without removing the file. Either the pattern still ships (the feature stays rich) or the pattern is being retired (the @@ -214,19 +277,20 @@ When `.pr-coordination/` carries an active campaign (per ## Big-gap escape hatch -If the refactor surfaces a missing architectural decision (not just a +If the refactor reveals a missing architectural decision (not just a clarification), stop. Do not paper over it with a quick edit and a silent invariant change. Report the gap to the user and recommend -routing to `architect-plan-session` to author a NEW pattern for the -emergent concern — never a retroactive pattern for the existing -shipped code. Shipping an under-decided refactor is worse than -re-opening the design conversation. +routing to [`architect-sessions`](../architect-sessions/SKILL.md) and its +[`plan`](../architect-sessions/references/plan.md) reference to author a +NEW pattern for the emergent concern. Never a retroactive pattern for +the existing shipped code. Shipping an under-decided refactor is worse +than re-opening the design conversation. ## Do not - Do not author a new design-level spec for shipped code (the kernel's retroactive-spec anti-pattern). -- Do not delete or recreate `architect/specs/.feature` — it +- Do not delete or recreate `architect/specs/.feature`. It does not exist and must not exist; that is the carve-out's premise. - Do not skip executable-Gherkin updates with the rationalization "the code change is the doc"; the kernel does not accept that. diff --git a/.agents/skills/_shared/multi-session-coordination.md b/.agents/skills/architect-refactor-session/references/multi-session-coordination.md similarity index 61% rename from .agents/skills/_shared/multi-session-coordination.md rename to .agents/skills/architect-refactor-session/references/multi-session-coordination.md index 86bab1e..6d976d9 100644 --- a/.agents/skills/_shared/multi-session-coordination.md +++ b/.agents/skills/architect-refactor-session/references/multi-session-coordination.md @@ -1,8 +1,10 @@ -# Multi-Session / PR Coordination (canonical reference) +# Multi-session / PR coordination (canonical reference) + +**This skill is only for non-spec-driven development. DO NOT USE for refactoring based on a design-level spec.** The convention for any pull request whose work is large or risky enough that a single agent session cannot land it cleanly in one pass. This is -**not refactor-specific** — feature PRs with cross-cutting changes, +**not refactor-specific**. Feature PRs with cross-cutting changes, review follow-up waves, dep-bump waves, security-audit fixes, and staged migrations all benefit. Refactor PRs benefit most because they concentrate the "scope expands mid-session" risk. @@ -13,23 +15,46 @@ scope-discovery rule below. ## When this applies -- **Always:** any PR with ≥2 logical chunks, any review follow-up, +- **Always.** Any PR with ≥2 logical chunks, any review follow-up, any modernization sweep, any staged migration, any refactor that touches ≥3 packages. -- **Strongly recommended:** any PR with ≥1 architectural decision, - any PR likely to surface drift mid-session, any PR a fresh agent +- **Strongly recommended.** Any PR with ≥1 architectural decision, + any PR likely to reveal drift mid-session, any PR a fresh agent session could not complete from the diff alone. -- **Optional:** a 1-commit PR with no decisions and no cross-cutting - surface — the lone `DECISIONS.md` + the scope-discovery rule are +- **Optional.** A 1-commit PR with no decisions and no cross-cutting + change. The lone `DECISIONS.md` + the scope-discovery rule are enough; the folder layout is overhead. -The six universal rules from -[`./session-preamble.md`](./session-preamble.md) are the floor for -every session in any campaign. This file adds the package layout, the -templates, and the campaign-specific discipline (coordinator split, -scope-discovery handling) on top. - -## Folder layout — `.pr-coordination/` +## The campaign rules (beyond the universal three) + +The three universal session rules, **graph handle first** (`pnpm architect:q` +over `g.graph` / `g.fsm`, ADR-014), **gates +non-negotiable**, **commit hygiene**, are the floor for every session +(stated in [`../../architect-sessions/SKILL.md`](../../architect-sessions/SKILL.md) +§"Universal session rules"). A campaign adds three more, which the +sections below operationalize: + +4. **Decisions captured before code.** Anything needing human judgment + goes to `DECISIONS.md` (template below) _before_ the edit that + depends on it. Without this separation, agents fabricate answers + under pressure. +5. **Incomplete scope is next-session input, not silent debt.** When + investigation reveals drift mid-session, stop and classify + (same-root-cause → fix inline + record; different-root-cause → + defer + record). Never land an incomplete commit. See + "Scope-discovery handling" below. The single most-reused heuristic + across multi-session work. +6. **Per-session learnings propagate forward.** After each session the + coordinator appends one tight entry to the learnings log and + rewrites the unstarted prompts' "Scope discipline" sections with + newly-discovered rules. Preambles are calibrated against real + surprises, not boilerplate. + +This file adds the package layout, the templates, and the +campaign-specific discipline (coordinator split, scope-discovery +handling) on top of those six. + +## Folder layout: `.pr-coordination/` Coordination artifacts live in a committed plan package at the repo root: @@ -52,7 +77,7 @@ the next campaign. The package is **committed to git** so every agent runtime (Claude Code, OpenCode, Codex/GPT, …) sees the same convention. Per-agent persistent memory (`~/.claude/`, opencode session store) -MUST NOT hold convention-level guidance — it hides context from other +MUST NOT hold convention-level guidance. It hides context from other runtimes. ## Coordinator + worker split (≥3 sessions) @@ -61,10 +86,10 @@ A campaign with three or more sessions defaults to a **coordinator-plus-worker** topology. The split is load-bearing. - **The coordinator** (typically a long-lived session) holds the - campaign's working memory: decisions, drift surfaces, prior-session + campaign's working memory: decisions, drift findings, prior-session learnings, the unstarted-session prompts. It **never touches code**, never runs gates, never makes commits. Its only job is the - prompt-and-memory pipeline — pre-session brief, mid-campaign drift + prompt-and-memory pipeline: pre-session brief, mid-campaign drift classification, post-session learning extraction, and propagation of new rules into the next session's prompt. - **The workers** (fresh agent sessions, any runtime) read the plan @@ -74,10 +99,10 @@ A campaign with three or more sessions defaults to a resume" property. - **Self-restraint defines the coordinator.** A coordinator that runs gates becomes another worker; a coordinator that does less is the - load-bearing primitive. + load-bearing rule. Worker session prompts under `sessions/NN-slug.md` are -**paste-ready**: a fresh agent opens the file, executes it, runs +**Paste-ready.** A fresh agent opens the file, executes it, runs gates, commits, returns. Runtime-specific shortcuts (Claude Code's `/fork`, OpenCode skill names) are **optional conveniences** described by the underlying action ("run a parallel inventory subagent") so any @@ -86,20 +111,20 @@ runtime can execute the prompt. ## DECISIONS.md template ``` -# Decisions — questions that need human judgment +# Decisions: questions that need human judgment > Tight entries only. Implementation details live in the session > prompt that consumes the decision, not here. Rewrites that bloat > this file with code snippets or step-by-step plans should be > rejected. -## D-1 — +## D-1: -- **Question:** -- **Options:** -- **Recommendation:** +- **Recommendation.**

")`), direct dependency context (`g.graph.relationshipIndex["

"]`), realizing files (`p?.sourceFile` / `p?.implementedBy`), invariants (`g.invariantsOf("

")`), plus the `architect_scope_validate` MCP gate. FSM checks go through `g.fsm.isValidTransition`. The default pre-flight is one handle call: `pnpm architect:q 'const p = g.pattern("

"); return {p, invariants: g.invariantsOf("

"), reverifies: g.specsReverifying(["

"]).length}'`. +- The work shape tells you which reference to read and which gate to honor, not a different command set. +- For a typed per-pattern bundle, call the `architect_bundle` or `architect_context` MCP tool. Mode/session inputs change which blocks are included by default, but defaults are good and the returned data is dominated by what the pattern actually is. Do not over-rely on intent flags. They are receding over time. + +Run the pre-flight from [`architect-graph-handle`](../architect-graph-handle/SKILL.md) (ADR-014) before any architect-scoped `Read` / `Glob` / `Grep`. File scanning to learn pattern state is a smell. One `pnpm architect:q` script answers it. + +## The spec is a temporary record (value transfer) + +The single idea every session type must hold: a design-level spec is a temporary record, not permanent documentation. Deleting that record never destroys value. It exists to carry intent from planning into implementation. Once the code stands, every piece of its value has already moved to a durable home, and only the now-redundant copy is removed. **Deletion is not loss.** "What did we delete?" is a `git log` question. The lifecycle ends in **value transfer.** The spec's invariants move into executable Gherkin (`tests/features/`, canonical) and its rationale into `@architect-*` JSDoc on production code (additive), then the design `.feature` is deleted. Not everything under `architect/` is deleted. A **code/contract stub** (`architect/stubs/`) is **promoted to `src/`**. Its `@architect-pattern` identity persists with the code (ADR-003). Only the staging copy is removed. A **step-definition stub** becomes the executable feature's step wiring. See [`references/ephemeral-spec-deletion.md`](references/ephemeral-spec-deletion.md) for which artifact goes where. + +This is why no session leaves the spec around as docs, why retroactive plan-level specs for shipped code are forbidden, and why the implement and review-implementation references end in a deletion gate rather than an archive step. The execution detail (the transfer checklist, the five-criterion pre-deletion gate, deletion timing: ask first; defer-to-code-review is the common path) lives in [`references/ephemeral-spec-deletion.md`](references/ephemeral-spec-deletion.md). + +## Universal session rules + +Three rules hold for every session here. The campaign-coordination rules (decisions-before-code, scope-discovery classification, learnings propagation) are refactor/campaign-flavored and live in [`architect-refactor-session`](../architect-refactor-session/references/multi-session-coordination.md): + +1. **Graph handle first.** Every pattern-state question goes through `pnpm architect:q ''` over `g.graph` / `g.fsm` before any file read. Reach for `architect_*` MCP tools when you need a typed gate (`architect_scope_validate`, `architect_handoff`) or a burst of ≥5 reads. Reusable algorithms stay pure core functions, not handle methods. Do not call a facade (`g.api`) or a retired verb alias. The handle is faster and more accurate, and its output is the canonical signal. `architect-base` §15 is the bootstrap discipline. +2. **Gates are non-negotiable.** The validation sequence (`pnpm typecheck && pnpm test && pnpm validate:all`, plus `pnpm architect:guard --staged` for FSM) runs before any commit or handoff. A failing gate is stop-and-report. Never `--no-verify`. Never silence it. +3. **Commit hygiene.** Stage explicit files (never `git add -A` on a multi-commit branch); `type(scope): imperative summary`; commit/push only when the user asks. + +## Disclosure map: pick your reference + +| You are about to… | Open | Note | +| ---------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------ | +| capture a new idea / refine a candidate / decide what to build | [`references/plan.md`](references/plan.md) | lightest tiers; no scope gate target | +| promote a plan-level spec to design (stubs, deliverables, ADRs) | [`references/design.md`](references/design.md) | writes specs + stubs only, never production code | +| build a design spec end-to-end | [`references/implement.md`](references/implement.md) | FSM → active, value transfer, deletion gate | +| find gaps in a spec **before** implementing | [`references/review-spec.md`](references/review-spec.md) | output is a gap list, not a rewrite | +| verify value transfer on **completed** work / batch-delete specs | [`references/review-implementation.md`](references/review-implementation.md) | per-pattern verdict; deletion is opt-in | +| wrap a session for the next one | [`references/handoff.md`](references/handoff.md) | forward-looking note, not a recap | +| modify shipped code with **no** design spec | [`architect-refactor-session`](../architect-refactor-session/SKILL.md) | separate skill. The carve-out | + +### Disambiguation (the old router rules, kept) + +- **`review` ≠ `review-implementation`.** The first reviews **specs before** implementation (gap-finding). The second reviews **implementations after** merge (value-transfer verification + batched deletion). Pick by lifecycle phase. +- **Qualified four-tier phrases route to planning.** "idea inbox", "idea tier", and "architectural slice" mean the lightest tier. Open [`references/plan.md`](references/plan.md), not `design.md`, even when the user is asking about slice scope. +- **Bare words do not route.** "epic", "slice", "candidate" alone are too broad in everyday English ("epic refactor", "take a slice of the array"). Only the qualified Architect phrases or an explicit pattern context belong here. +- **If intent is genuinely ambiguous, ask once** before opening a reference. Do not guess. + +## Each reference is self-sufficient + +Every file in [`references/`](references/) leads with a short context-gathering step, the lean execution sequence anchored to the graph handle, and a one-line pointer to the natural next session. They cite `architect-base/references/*` for doctrine depth rather than restating it. Open exactly the one your work shape needs. diff --git a/.agents/skills/architect-sessions/references/design.md b/.agents/skills/architect-sessions/references/design.md new file mode 100644 index 0000000..45549e1 --- /dev/null +++ b/.agents/skills/architect-sessions/references/design.md @@ -0,0 +1,79 @@ +# Design: plan to design promotion + +Taking a plan-level spec to design tier. The deliverable is a richer `.feature` plus stubs in `architect/stubs/`. **Do not write production code in this session.** That is [`implement.md`](implement.md). + +Doctrine depth: split-ownership (which tags live on the feature vs on stubs; a **code/contract stub carries its own code-originated `@architect-pattern`**, a _distinct_ name, plus `@architect-implements`/`@architect-target`, while a **step-definition stub MUST NOT carry `@architect-pattern`** per ADR-008) in [`../../architect-base/references/annotation-ownership.md`](../../architect-base/references/annotation-ownership.md); the optional 4-field Rule template in [`../../architect-base/references/rule-block-template.md`](../../architect-base/references/rule-block-template.md); choosing the test-pattern name (`Testing` vs `ExecutableTests`) in [`../../architect-base/references/spec-pattern-relationships.md`](../../architect-base/references/spec-pattern-relationships.md). + +## Gather context first + +Before promoting, confirm the design has somewhere solid to stand. Extract from the plan-level spec and the normative source (ADR/redesign/brief). Ask only about gaps: + +1. **Source of truth.** Which ADR / redesign doc / brief does this design realize? Read it. Its types and constraints must land in the deliverables. +2. **Deliverable files.** Which exact files will this touch? (Becomes the `Background:` table.) +3. **Reuse.** Do the proposed types/schemas already exist in `packages/`? Reference and reuse. Don't redefine. +4. **Decisions.** Are there genuinely new architectural decisions (→ ADR refs + stub DD-N), or is this the Nth instance of an established shape (→ keep it lean)? + +The detail level is **contextual** (`architect-base` §10): invest depth where the work is architecturally significant or sensitive; skip stubs and exhaustive scenarios for routine, well-understood shapes. Too much detail rots. Stripping hard-won nuance to "match the tier" destroys signal. Both fail. + +**Distill as you author. Re-explanation won't survive value transfer.** Before writing rule or rationale prose, check §10's "skip detail" cases. If this is the Nth instance of an established shape, or an industry-standard piece (a CRUD endpoint, a standard codec, a barrel), **reference the established pattern / ADR and stop.** Do not re-derive what it is or why it's shaped that way. A `**Rationale:**` that only restates its `**Invariant:**` is dead weight the implement-time transfer gate ([`ephemeral-spec-deletion.md`](ephemeral-spec-deletion.md) §"Transcription bloat") will strip anyway. Spend words where the work is genuinely novel. Spend none re-narrating the standard. + +## Pre-flight + +Run the pre-flight from [`../../architect-graph-handle/SKILL.md`](../../architect-graph-handle/SKILL.md) (ADR-014). Orient with `pnpm architect:q 'return {counts: g.graph.counts, active: g.patterns.filter(p => p.status === "active").map(p => p.name)}'`, then run the scope gate, the `architect_scope_validate` MCP tool for `` at `design`, then pull the pattern's context in one handle call: `pnpm architect:q 'const p = g.pattern(""); return {p, relations: g.graph.relationshipIndex[""], invariants: g.invariantsOf(""), reverifies: g.specsReverifying([""]).length}'`. For a typed bundle, call the `architect_bundle` or `architect_context` MCP tool. The design-mode bundle carries **no** `stubs` / `deliverables` / `deps` block. The spec's deliverables and stubs come through `architect_context` with session `design` (its `=== SPEC ===` section), not the bundle. + +If `architect_scope_validate` returns BLOCKED, **stop and report the blocker.** Do not design around a blocked dependency chain. If the source spec is at idea or candidate tier, **stop** and route through [`plan.md`](plan.md) to promote through the missing rungs. Skipping rungs is rejected (except the refactoring carve-out, which is [`architect-refactor-session`](../../architect-refactor-session/SKILL.md), not this). + +## Plan to design delta + +A design-level `.feature` adds, on top of the plan-level shape: + +- `Background:` table listing the exact files this design will touch. Full paths, file-by-file. +- Exhaustive scenarios: error paths, edge cases, integration scenarios. +- Stub references in `architect/stubs//*`. +- `**Rationale:**` and `**Verified by:**` on every Rule. +- ADR references where significant decisions were made. + +Status stays `roadmap` (it transitions to `active` during implement, not here). Edit in place. No file move. + +## Stubs: temporary contract files (read carefully) + +Stubs live in `architect/stubs//`. They: + +- Are TypeScript files with realistic signatures, types, and JSDoc. **No real logic.** +- **Carry their own code-originated identity.** In `.ts` JSDoc, author: `@architect` + `@architect-pattern ` (a _distinct_ name from the design pattern, e.g. `EmissionDescriptor` for `TaxonomyDocumentationCluster`) + `@architect-role:contract` + `@architect-status roadmap` + `@architect-bounded-context:` (optional enrichment) + `@architect-implements ` + `@architect-target `. **Mind the syntax per file type** (the lint enforces it; full rule in [`../../architect-base/references/taxonomy.md`](../../architect-base/references/taxonomy.md)): in `.ts` JSDoc `@architect-pattern` / `@architect-implements` / `@architect-target` / `@architect-status` are **space**-separated, while `@architect-role:` / `@architect-bounded-context:` / `@architect-product-area:` take a **colon**. `.feature` files use a colon for `@architect-pattern:` / `@architect-implements:`. `@architect-status` is **always `roadmap`** on a stub. It advances only when the stub is promoted to `src/` (see implement.md). This is mandated by `formal-spec/04-tag-registry.md` + `07-stub-format.md` (`@architect-pattern`/`@architect-implements`/`@architect-target` are MUST on stubs) and ADR-003 ("identity travels with code from stub through production"), and it makes the stub a first-class graph node: `g.pattern("")` resolves, and the design pattern's `implementedBy` points back at it. (A _step-definition_ stub under `architect/step-stubs/` is the exception. No `@architect-pattern`, per ADR-008, because the spec owns identity there.) +- May include design-decision (DD-N) comments and "When to Use" guidance. These travel with the code to `src/`. +- Are **not compiled, not linted, not tested**. They are staging. +- Move to `src/` during implementation: the **contract identity persists** there as a code-originated pattern (its `@architect-status` advances `roadmap` → `active` → `completed` with the build, _not_ frozen at design-time `roadmap`); only the design `.feature` is deleted at value transfer. The stub is the embryo of the shipped pattern, not throwaway. It leaves `architect/stubs/` by being promoted, not discarded. + +Encode in stubs the design intent production code will need but Gherkin can't carry naturally: types, function signatures, hidden constraints, why-this-shape rationale. + +## Anti-drift tripwires (stop and redirect if you catch yourself) + +1. Writing real implementation logic in a stub. Stubs carry shape, not behavior. +2. Adding a `.ts` file under `src/`. Wrong session. Hand off to [`implement.md`](implement.md). +3. Running `pnpm test` or editing `tests/features/`. Wrong session. +4. Editing a file outside the deliverables table. **Add it to the table** before editing. +5. Re-deriving pattern data outside `PatternGraph`. Read via the graph handle (`pnpm architect:q` over `g.graph` / `g.fsm`). Don't parallel-pipeline. +6. Inventing a business rule with no `**Invariant:**`. +7. Promoting an idea straight to design. Design requires plan tier first. Route through [`plan.md`](plan.md). + +## The spec you write here will be deleted + +Design-level specs and stubs are temporary records. At implement time their value transfers to executable Gherkin (invariants/rationale/verified-by) and JSDoc, then the `.feature` and stubs are deleted (full doctrine: [`ephemeral-spec-deletion.md`](ephemeral-spec-deletion.md)). **Author every line knowing it will be deleted.** Make it worth the implementer's read. Anything that won't transfer to an annotation or an executable scenario should not be written. + +## Acceptance criteria for design tier + +Verify with the graph handle and the typed MCP gates before claiming done: + +- The `architect_scope_validate` MCP tool for `` at `implement` **must return PASS**. +- The `architect_context` MCP tool for `` with session `implement` **must include deliverables**. + +WARN or BLOCKED on `implement` means the design is not ready. Fix the gaps first. + +## Do not + +- Do not implement. +- Do not delete the design spec or its stubs here. [`implement.md`](implement.md) owns that, after value transfer. +- Do not skip stubs for architecturally relevant behavior, and do not author scenarios the executable layer can't reach (design scenarios are written to become executable). + +**Next session.** When `architect_scope_validate` for `` at `implement` is PASS, continue in [`implement.md`](implement.md). If it returns WARN/BLOCKED, run [`review-spec.md`](review-spec.md) to enumerate the gaps first. diff --git a/.agents/skills/architect-sessions/references/ephemeral-spec-deletion.md b/.agents/skills/architect-sessions/references/ephemeral-spec-deletion.md new file mode 100644 index 0000000..4289ef9 --- /dev/null +++ b/.agents/skills/architect-sessions/references/ephemeral-spec-deletion.md @@ -0,0 +1,186 @@ +# Ephemeral-spec deletion (value-transfer execution detail) + +The terminal phase of the spec lifecycle: how value moves out of an +ephemeral design spec into executable Gherkin and JSDoc, and what makes a design +spec safe to delete. The **concept** (specs are temporary records, the +lifecycle ends in deletion) is required context for every session +type and lives in [`../SKILL.md`](../SKILL.md) §"The spec is a +temporary record". This file is the **execution detail** the implement and +review-implementation references use: the transfer checklist, the +five-criterion pre-deletion gate, and deletion timing. + +## Concept + +Design-level specs and step-definition stubs are **temporary records, not +permanent documentation**. Once implementation completes, the spec's +value must transfer to artifacts that survive the spec's deletion. (A +**code/contract stub** is the exception: it is not deleted but +_promoted_. It carries its own `@architect-pattern` identity to `src/` +per ADR-003, where it persists as a code-originated pattern; only the +behavioral design `.feature` is deleted.) The durable artifacts are: + +1. **Executable Gherkin** in `tests/features/**/*.feature`. The + primary carrier. Carries pattern identity (`@architect-pattern`), + the realization edge (`@architect-implements:`), status, + dependencies, business invariants (Rule blocks), and scenarios that + prove the invariants hold. +2. **JSDoc `@architect-*` annotations on production code.** Additive + carrier. Carries technical wiring (`@architect-uses` when the target + resolves to a declared pattern), "when to use" guidance + (`@architect-usecase`), implementation classification + (`@architect-role`, `@architect-bounded-context`), decision links + (`@architect-decision`), and any architectural `**Rationale:**` + content that doesn't belong in Gherkin. + +## The primary durable artifact is the executable feature file + +Per the split-ownership policy in +[`../../architect-base/references/annotation-ownership.md`](../../architect-base/references/annotation-ownership.md), the `.feature` +file is the **canonical pattern definition**. Production-TS JSDoc +annotations are **additive, not mandatory**. A completed, +feature-identity-owned pattern carries zero `@architect-*` identity JSDoc +on its realizing production source and is still legitimately complete +because the executable feature carries the full identity, status, deps, and invariants. (Confirm the +current set live rather than trusting a frozen name. Samples rot: +`pnpm architect:q 'g.patterns.filter(p => p.status === "completed").map(p => p.name)'`, +then `pnpm architect:q 'const p = g.pattern(""); return {file: p?.sourceFile, realizing: p?.implementedBy}'`.) + +The maximalist framing "value must transfer to BOTH artifacts" (executable +Gherkin + JSDoc annotations) is a useful default goal, but it is **not** +the deletion gate. The actual gate is in the **Pre-deletion gate** section +below; the split-ownership policy in `annotation-ownership.md` is the +authority for which artifact is mandatory vs additive. + +## Transfer checklist + +| From (ephemeral) | To (durable carrier) | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Plan-level rule with invariant | `Rule:` block in `tests/features/**/*.feature` carrying `**Invariant:**` verbatim. Carry `**Rationale:**` **only where it states a why beyond the invariant** (drop it when it merely restates); `**Verified by:**` names the **actual** executable `Scenario:` titles, never a boilerplate string repeated across rules. Distill, don't transcribe. | +| Stub's "When to Use" comment | `@architect-usecase` JSDoc on the implementation (additive) | +| Stub's DD-N decision | `@architect-decision:DD-N` JSDoc referencing the ADR (additive) | +| Design Scenario | Executable `Scenario:` block in `tests/features/` | +| Scenario without a production-code home | Executable scenario alone. No annotation target exists. | +| Architectural rationale | Either Gherkin Rule block `**Rationale:**` OR JSDoc free text. Pick whichever is more discoverable for the reader. | +| Deliverables list | Verified by test coverage + (where annotations exist) `@architect-target` resolution | + +For the bipartite production↔test pattern naming convention (test +patterns carry `@architect-pattern:Testing` or +`ExecutableTests`) see +[`../../architect-base/references/spec-pattern-relationships.md`](../../architect-base/references/spec-pattern-relationships.md). +For the optional 4-field Rule template see +[`../../architect-base/references/rule-block-template.md`](../../architect-base/references/rule-block-template.md). + +## Anti-patterns (stop) + +- **Zombie design spec.** Leaving a design-level spec in + `architect/specs/` after implementation completes. The spec is a + working file; once the code ships, that file comes down. +- **Half-transferred value.** Transferring rules to executable specs + but not to annotations (or vice versa) where both artifacts should + carry weight. Note: annotations are additive, so transfer to + executable Gherkin alone is often sufficient. Apply this anti-pattern + only when both artifacts are genuinely required. +- **Retroactive plan-level spec.** Authoring a fresh design or + plan-level spec for code that already ships. Ephemeral specs describe + _planned_ work. Conjuring one back to "cover" shipped behavior + inverts the pipeline. Use the `*ExecutableTests` escape hatch in + [`../../architect-base/references/spec-pattern-relationships.md`](../../architect-base/references/spec-pattern-relationships.md). +- **Transcription bloat.** Copying rule prose across the transfer + instead of distilling it. Symptoms: a `**Rationale:**` that inverts + its own `**Invariant:**`; the **same** `**Verified by:**` string on + every rule (the backfill smell, e.g. ADR-003's six identical + copies); a step stub or production-JSDoc comment that re-states what + the pattern _is_ rather than its local wiring / how (see + [`../../architect-base/references/annotation-ownership.md`](../../architect-base/references/annotation-ownership.md) + §"Critical: do not duplicate explanation"); a house-motif phrase + where a concrete path / field / ADR ref would be exact. The fix is to + **slim the destination** (executable feature, stub, or JSDoc), then + delete the spec. Never keep the working file because its successor + reads long. + +## Pre-deletion gate + +A design spec is safe to delete only when **all** of these hold: + +1. **Forward link present.** The design spec carries + `@architect-executable-specs:`. +2. **Forward link resolves.** The path points at a real file under + `tests/features/`. +3. **Reverse link present.** That target feature carries + `@architect-implements:` for the focal pattern. +4. **Rich content has landed.** Every Rule block in the design spec + has a counterpart Rule block in the executable feature carrying + `**Invariant:**` (and, where present in the source, + `**Rationale:**` + `**Verified by:**`). +5. **Architecturally significant rationale lives in JSDoc** for any + production code where the rationale won't fit in Gherkin (judgment + call. Annotations are additive). + +When all five hold, deletion is safe. When any fails, fix that artifact +before deletion. + +**Distillation is a transfer-quality check, not a sixth deletion +blocker.** The five criteria gate _whether value landed_; +**Transcription bloat** (above) gates _whether it landed clean_. A +verbose destination never justifies keeping the working file. The remedy is +always to slim the executable feature / stub / JSDoc, then delete. +Verify distillation at review sign-off +([`review-implementation.md`](review-implementation.md)), not by +retaining the spec. + +## Mechanical check (when shipped) + +The candidate spec +`architect/specs/value-transfer-state.feature` +proposes: + +- A deterministic per-pattern value-transfer read returning + (`designSpecPath`, `executableSpecPaths`, + `annotatedSourcePaths`, `forwardLink`, `reverseLinks`, `antipatterns`, + `deletionReady`, `transferComplete`). The planned form is the pure + kernel `projectValueTransferState` / + `parseAndProjectValueTransferState`, plus the typed MCP tool + `architect_value_transfer`. No named CLI command, graph-handle method, + or q-import is added. Plain-JS q bodies cannot import this planned + projection (ADR-014). +- An MCP tool `architect_value_transfer` with the same input shape. +- Composition into `ArchitectBriefDeterministicBundle` so every + session-open brief shows anti-patterns as graph-derived ground + truth. + +Until that ships, the manual checklist above is the gate. After it +ships, the [`implement`](./implement.md) and +[`review-implementation`](./review-implementation.md) references will +gate `git rm` on `deletionReady === true`. + +## Deletion timing + +The implementer **asks the user** before deleting: + +- **Delete now.** Appropriate when the implementation session reviews + the value transfer thoroughly and the pattern is the only one being + reviewed. +- **Defer to code review** (more common). Appropriate when several + related implementations are being reviewed together. The reviewer + batches the spec deletions in a single PR or review pass, after + verifying value transfer across the related set. The + [`review-implementation`](./review-implementation.md) reference is the + canonical owner of batched deletion. + +Default behavior: **ask, don't auto-delete**. + +## Sibling references + +- [`../../architect-base/references/annotation-ownership.md`](../../architect-base/references/annotation-ownership.md). + Split-ownership policy that makes the executable feature canonical. +- [`../../architect-base/references/spec-pattern-relationships.md`](../../architect-base/references/spec-pattern-relationships.md). + Bipartite production↔test pattern graph + `*ExecutableTests` + escape hatch. +- [`../../architect-base/SKILL.md`](../../architect-base/SKILL.md) + §"Anti-anecdote". The live graph is canonical over a stale + paraphrase. + +The `value-transfer-state.feature` candidate spec referenced above +(`architect/specs/value-transfer-state.feature`) is +an Architect-internal pointer to the in-progress mechanization of this +gate, not an external doc. Keep the reference. diff --git a/.agents/skills/architect-sessions/references/handoff.md b/.agents/skills/architect-sessions/references/handoff.md new file mode 100644 index 0000000..bcd6c8a --- /dev/null +++ b/.agents/skills/architect-sessions/references/handoff.md @@ -0,0 +1,78 @@ +# Handoff: end-of-session state capture + +The session is wrapping. Capture exactly what the next session needs: forward-looking pattern state, not a backward-looking recap. + +Doctrine depth: valid FSM transitions + `@architect-unlock-reason:` + what `architect_scope_validate` outputs mean are in [`../../architect-base/references/fsm-transitions.md`](../../architect-base/references/fsm-transitions.md). + +## Pre-flight + +Run the handoff pre-flight from [`../../architect-graph-handle/SKILL.md`](../../architect-graph-handle/SKILL.md) (ADR-014) for forward-looking signal: + +```bash +pnpm architect:q 'return {counts: g.graph.counts, active: g.patterns.filter(p => p.status === "active").map(p => p.name)}' +pnpm architect:q 'const p = g.pattern(""); return {p, invariants: g.invariantsOf(""), reverifies: g.specsReverifying([""]).length}' +pnpm architect:q 'g.patterns.filter(p => p.status === "roadmap" && p.uses.some(u => g.pattern(u)?.status !== "completed")).map(p => p.name)' +grep -rn -A4 'Open Questions' architect/specs/ +``` + +Then write the canonical record with the `architect_handoff` MCP tool (pattern, session intent, modified files). When MCP is unavailable, author the record in this skill's format below. Write one record per pattern for multi-pattern sessions. + +## What to extract + +For each pattern touched: + +| Field | Source | +| -------------------------- | --------------------------------------------------------------------------------------------------- | +| Session intent | What you were doing (`planning` / `design` / `implement` / `review`) | +| Pattern name | The primary pattern under work | +| Current FSM state | `pnpm architect:q 'g.pattern("")?.status'` | +| Transitions made | Your edit history | +| Files modified | Pass as the modified-files input to `architect_handoff` | +| Open dependencies | `pnpm architect:q 'g.graph.relationshipIndex[""]'` minus the satisfied ones | +| Open blockers | `pnpm architect:q 'g.pattern("")?.uses.filter(u => g.pattern(u)?.status !== "completed")'` | +| Outstanding open questions | `grep -rn -A4 'Open Questions' architect/specs/` scoped to this pattern's specs | +| Outstanding work | What you didn't finish, one-line "why" each | + +## Handoff note format + +``` +**Architect handoff: ()** + +- State: (was: ) +- Modified: +- Blockers: +- Outstanding: +- Recommended next: for +``` + +Five fields, no recap of conversation, no thanks-for-this-session prose. The next session reads this verbatim and starts work. + +## Recommended-next table + +Set the `Recommended next:` field from where the session ended (all references are in this skill unless noted): + +| Session ended at | Spec state | Recommended next | +| ------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Idea tier | Idea captured, ready to refine | [`plan.md`](plan.md) (promote idea → candidate) | +| Candidate tier | Open questions resolved, acceptance gate cleared | [`plan.md`](plan.md) (promote candidate → plan; flips status to `roadmap`) | +| Plan tier | Plan-level spec ready for design | [`design.md`](design.md) | +| Design tier | `architect_scope_validate` `` `implement` = PASS | [`implement.md`](implement.md) | +| Design tier | `architect_scope_validate` `` `implement` = WARN/BLOCKED | [`review-spec.md`](review-spec.md) (find gaps) → [`design.md`](design.md) | +| Implement | Spec deleted, value transferred | (none. Pattern complete; optionally start the next pattern's planning) | +| Implement | Value transferred, deletion deferred | [`review-implementation.md`](review-implementation.md) (batched verification + deletion) | +| Review (spec) | Gap list produced | [`design.md`](design.md) to fix, or [`implement.md`](implement.md) if PASS | +| Review (implementation) | Per-pattern verdicts, batched deletion proposed | (none if user authorized deletion; otherwise re-invoke when ready) | +| Refactor (no design spec) | Shipped code evolved in place | [`architect-refactor-session`](../../architect-refactor-session/SKILL.md) | + +The full ladder is in [`../../architect-base/references/four-tier-ladder.md`](../../architect-base/references/four-tier-ladder.md). + +## Anti-patterns (stop) + +- **Free-form recap** ("we talked about X, then I implemented Y…"). Cut it. The handoff is forward-looking only. +- **Skipping the canonical record.** The `architect_handoff` MCP tool (or the authored note above when MCP is unavailable) writes it. Skip it and the next session has no authoritative source. +- **Recommending the wrong next step.** Cross-check the table. Most common miscalls: routing a candidate to design (it needs plan tier first), or routing a BLOCKED design to implement (it needs review-spec first). + +## Do not + +- Do not declare a session "done" without writing the handoff record (`architect_handoff`, or the authored note above). +- Do not commit or push without the user's explicit approval. diff --git a/.agents/skills/architect-sessions/references/implement.md b/.agents/skills/architect-sessions/references/implement.md new file mode 100644 index 0000000..49a4d5f --- /dev/null +++ b/.agents/skills/architect-sessions/references/implement.md @@ -0,0 +1,66 @@ +# Implement: design spec to code + +The design-level `.feature` is your implementation prompt; the stubs encode shape decisions. Together they specify exactly what to build. This session ends with the spec's value living in production code + executable Gherkin. **Deleting the design spec is a separate decision** (see "Deletion" below). + +**The spec IS the prompt. Do not create a wrapper "context" or "session-prep" document.** If the design has a major gap that needs new architectural decisions (not just clarifications), stop and route back to [`design.md`](design.md) / [`review-spec.md`](review-spec.md) rather than papering over it. + +**This is execution, not (re-)planning.** The design is settled. Do not reopen decisions or re-derive an implementation map (blast radius, consumer list, sequencing) that already exists. The `.feature` deliberately holds only the **durable invariants**. The **volatile `file:line` consumer/blast-radius map** is kept _out_ of it (so it can't rot) and parked in a companion under `plans/` (or `.pr-coordination/`, `.sisyphus/plans/`). So before any `Grep`/Explore to learn _what to touch_, **look for that companion**. `plans/-*.md` is the common name. Read it. Use `Grep`/Explore only to **verify** the map against the live tree, never to rebuild it from scratch. Re-deriving a map that already exists (e.g. fanning out search agents to re-discover the blast radius) is wasted work and a sign the companion read was skipped. Independent re-confirmation is corroboration, not a reason to keep deliberating instead of building. + +Doctrine depth: the value-transfer concept is in [`../SKILL.md`](../SKILL.md) §"The spec is a temporary record"; the **execution detail** (transfer checklist + pre-deletion gate) is [`ephemeral-spec-deletion.md`](ephemeral-spec-deletion.md). Split-ownership (realizing code uses `@architect-implements`, not a duplicate `@architect-pattern`; but a code-originated pattern, incl. a promoted stub, owns its own `@architect-pattern` on the `.ts`; JSDoc is additive) is [`../../architect-base/references/annotation-ownership.md`](../../architect-base/references/annotation-ownership.md); the bipartite naming + forward/reverse link pair is [`../../architect-base/references/spec-pattern-relationships.md`](../../architect-base/references/spec-pattern-relationships.md); the FSM table + `@architect-unlock-reason:` rules are [`../../architect-base/references/fsm-transitions.md`](../../architect-base/references/fsm-transitions.md). + +## Pre-flight + +Run the pre-flight from [`../../architect-graph-handle/SKILL.md`](../../architect-graph-handle/SKILL.md) (ADR-014): the status overview (`pnpm architect:q 'return {counts: g.graph.counts, active: g.patterns.filter(p => p.status === "active").map(p => p.name)}'`), the `architect_scope_validate` gate for `` `implement`, the implement-mode composite (`pnpm architect:q 'const p = g.pattern(""); return {p, invariants: g.invariantsOf(""), reverifies: g.specsReverifying([""]).length}'`), the file view (`pnpm architect:q 'const p = g.pattern(""); return {file: p?.sourceFile, realizing: p?.implementedBy}'`), and the FSM gate (`pnpm architect:q 'g.fsm.isValidTransition("","")'`). **Then check `plans/` (and `.pr-coordination/`, `.sisyphus/plans/`) for a companion impact/assessment doc.** If one exists it carries the `file:line` consumer map the `.feature` omits. Read it before grepping (see "execution, not (re-)planning" above). + +If `architect_scope_validate` for `` `implement` is not PASS, **stop.** Either the design is incomplete (→ [`design.md`](design.md)) or a dependency is blocked (→ [`review-spec.md`](review-spec.md) to find the blocker). + +## Implementation order (strict) + +1. **Transition FSM to `active` before any code change.** Verify first: `pnpm architect:q 'g.fsm.isValidTransition("","active")'`. Proceed only on a confirming verdict. For a design spec entering implement, `` is `roadmap`; `isValidTransition` speaks only the four process statuses (`roadmap`/`active`/`completed`/`deferred`), not tier words. Then bump `@architect-status` `roadmap` → `active` in the spec. Unusual transitions need `@architect-unlock-reason:` (the FSM reference). +2. **Read all deliverable target files** listed in the spec's `Background:` table. +3. **Read the stubs.** They encode design decisions (DD-N) and "When to Use" guidance. +4. **Implement deliverables in the order listed**, guided by Rules + Scenarios. +5. **After each deliverable.** Run the closest targeted typecheck/test slice for the files you touched, then `pnpm typecheck` before the next phase boundary. Before any commit or handoff: `pnpm typecheck && pnpm test && pnpm validate:all`. Do not batch verification to the end. +6. **Author / refine executable Gherkin** under `tests/features/` as you go. Transfer the design Scenarios, carrying the `**Invariant:**` verbatim but **distilling** the rest: keep `**Rationale:**` only where it states a why beyond the invariant, and make `**Verified by:**` name the real `Scenario:` titles (never one boilerplate string copied across rules. See [`ephemeral-spec-deletion.md`](ephemeral-spec-deletion.md) §"Transcription bloat"). Enumerate what must land with `pnpm architect:q 'g.invariantsOf("")'`. +7. **Add `@architect-*` JSDoc** to every production file you create or modify. At minimum `@architect-implements:` (the realization edge). Do **not** author `@architect-pattern:X` for a pattern `X` a feature file already owns. That duplicates identity. Use `@architect-implements:X` instead. **A code-originated pattern keeps its own identity on the `.ts`, though.** when you promote a stub to `src/` it **retains** its `@architect-pattern:` + `@architect-role:` (identity travels from stub through production, ADR-003. Do not strip it). Its `@architect-status` is the opposite. It **advances with the FSM** (`roadmap` → `active` → `completed`) as you build it. **Never ship a promoted stub still marked `@architect-status:roadmap`** (that leaves shipped code stale and miscounts delivery progress). A codec/contract/utility defined directly in code likewise owns `@architect-pattern` there. Add `@architect-uses` / `@architect-usecase` / `@architect-decision` / `@architect-role` / `@architect-bounded-context` as additive enrichment. `@architect-uses` is one comma-separated line. Extend it, never add a second line. Reverse edges derive; never author them. Keep that JSDoc **local**. This file's how / why / gotcha. Never a paraphrase of what the pattern _is_ or why it exists (that lives once on the owning feature; restating it per file denormalizes the canonical node. See [`../../architect-base/references/annotation-ownership.md`](../../architect-base/references/annotation-ownership.md) §"Critical: do not duplicate explanation"). +8. **When ALL deliverables complete.** Transition the spec to `completed`, and advance **every code-originated pattern you promoted from a stub** to `completed` too (verify none still reads `@architect-status:roadmap` on shipped `src/`: `pnpm architect:q 'g.patterns.filter(p => p.status === "roadmap").map(p => p.name)'` should not list a pattern whose file is now under `src/`). Then regenerate docs and run the value-transfer-and-delete step below. + +## Value transfer (verify before deletion) + +Walk the five-criterion **pre-deletion gate** in [`ephemeral-spec-deletion.md`](ephemeral-spec-deletion.md) (forward link present + resolves; reverse link present; rich content landed; architecturally significant rationale in JSDoc where Gherkin can't carry it). Until `architect_value_transfer` ships, walk that gate by hand. The planned check is that MCP tool plus the pure `projectValueTransferState` kernel, not a q method or a named `architect:graph` command. q bodies cannot import that projection (ADR-014). Every line of the design spec that won't transfer is dead weight. Either it transfers, or it was never worth writing. + +## Deletion (ask the user first) + +Two valid outcomes. **Default: ask which applies.** + +- **Delete now.** When this session reviewed the value transfer thoroughly and the pattern is the only one in scope. +- **Defer to code review** (more common). When several related implementations are reviewed together; the reviewer batches deletions via [`review-implementation.md`](review-implementation.md). + +Phrase it like: "Value transfer is verified for ``. Delete the design spec now, or defer to code review where related implementations are batched (the more common path)?" + +If the user authorizes deletion now: + +```bash +git rm architect/specs/.feature # delete the design spec (behavioral identity) +git rm -r architect/stubs// # remove the staging copy; a code stub's identity now lives in src/ (promoted in step 7, not discarded) +pnpm architect:q 'g.pattern("")?.status' # confirm the pattern shows completed +pnpm docs:all # regenerate docs +``` + +If the user defers: leave the spec + stubs in place, and name [`review-implementation.md`](review-implementation.md) as the next step in your handoff. If you _cannot_ transfer value because something still depends on the spec, that is a **zombie spec** smell. Investigate. Either the dependency is wrong or the spec is doing something durable it shouldn't. + +## Anti-patterns (stop and redirect) + +- **Wrapper documents.** The spec is the prompt; do not create a parallel context markdown. +- **Retroactive specs at any tier.** Discovering code that already implements the pattern → tag an existing executable feature with `@architect-implements:` and enrich it; never author a fresh idea/candidate/plan/design spec for shipped behavior (the refactoring carve-out backfills via a `*ExecutableTests` feature at executable-tier, never via plan). +- **Zombie design specs.** Leaving the design spec after implementation is a lie at worst, noise at best. +- **Half-transferred value.** Rules to executable specs but not to annotations (or vice versa) where both should carry weight. +- **Backward-compat shims.** No `@deprecated`, `// eslint-disable`, `@ts-expect-error`, or re-export aliases. The No-BC guard fails CI. + +## Do not + +- Do not skip the FSM transition to `active` before coding. +- Do not delay annotations to a follow-up PR. They are part of the implementation. +- Do not declare done without value transfer (+ deletion, or an explicit deferral). + +**Next session.** If deletion was deferred, [`review-implementation.md`](review-implementation.md) verifies value transfer and batches the deletion. Otherwise capture state with [`handoff.md`](handoff.md). diff --git a/.agents/skills/architect-sessions/references/plan.md b/.agents/skills/architect-sessions/references/plan.md new file mode 100644 index 0000000..d249ffa --- /dev/null +++ b/.agents/skills/architect-sessions/references/plan.md @@ -0,0 +1,123 @@ +# Plan: idea and candidate authoring + +The lightest two rungs of the four-tier ladder: capture a new idea, or promote an idea to candidate. The single most common failure mode is **producing a verbose, deliverables-loaded spec for an idea that has not been committed to delivery.** Resist it. + +Doctrine depth (read once if unfamiliar): the tier table + mandatory tags in [`../../architect-base/references/four-tier-ladder.md`](../../architect-base/references/four-tier-ladder.md); the optional Rule-block template (idea tier = `**Invariant:**`-only) in [`../../architect-base/references/rule-block-template.md`](../../architect-base/references/rule-block-template.md); the `*ExecutableTests` escape hatch (for "capture" requests aimed at code that already ships) in [`../../architect-base/references/spec-pattern-relationships.md`](../../architect-base/references/spec-pattern-relationships.md). + +## Gather context first + +Before writing anything, get the few things that decide the spec's shape. Ask conversationally, most-important first. Extract from any brief/doc the user provides and only ask about the gaps: + +1. **Problem + actor.** What capability, for whom, so that what outcome? (This becomes the one-line user story.) +2. **The one invariant.** What must always be true for this to be correct? (This becomes the single Rule.) +3. **Already shipping?** Does code already implement this? If yes, **stop**. An idea/candidate/plan spec is the wrong artifact. Route to the `*ExecutableTests` escape hatch (enrich an existing executable feature). Never a retroactive spec. +4. **Parent / level.** Which epic is this under, or is it itself an epic/slice? + +If the answers aren't there yet, refining intent in conversation is a valid outcome. Say so and stop. Do not manufacture detail to fill a template. + +## Pre-flight + +Run the pre-flight from [`../../architect-graph-handle/SKILL.md`](../../architect-graph-handle/SKILL.md) (ADR-014). Orient with `pnpm architect:q 'return {counts: g.graph.counts, active: g.patterns.filter(p => p.status === "active").map(p => p.name)}'`. Locate with `pnpm architect:q 'g.findByConcept("")'` or `pnpm architect:q 'g.patterns.filter(p => p.status === "candidate").map(p => p.name)'`. For candidate readiness, read the candidate's full record, `pnpm architect:q 'g.graph.patterns.find(p => p.name === "")'`, and check its open-questions block. **No scope gate at this tier.** `architect_scope_validate` (MCP) accepts only `design` and `implement`. Idea/candidate readiness is structural (the ladder reference). + +## Six-tag idea-tier minimum + +An idea-tier spec carries six authored tags: the five cross-tier baseline plus the explicit `@architect-maturity:idea` the guard's idea-tier checks require. Without it the file is _not_ recognized as idea-tier and silently escapes idea-tier validation: + +1. `@architect`. The gate tag. +2. `@architect-pattern:` +3. `@architect-status:candidate` +4. `@architect-maturity:idea`. **Idea tier only** (the guard's idea-tier opt-in). Dropped on promotion to candidate, after which maturity derives from status. +5. `@architect-product-area:` +6. `@architect-parent:` + +Any further tag at idea tier is a smell, **except** `@architect-level:epic` / `@architect-level:slice`. Those are structural and exempt the file from the `@architect-parent` requirement. + +## Idea-tier template (write exactly this shape, no more) + +Location: `architect/specs/ideas/.feature`. + +```gherkin +@architect +@architect-pattern: +@architect-status:candidate +@architect-maturity:idea +@architect-product-area: +@architect-parent: +Feature: - + + **User Story:** As a , I want so that . + + Rule: + **Invariant:** +``` + +Six authored tags, one user story, one rule with one invariant. That is the entire shape. Add a second rule only if the idea genuinely encodes two distinct constraints. + +### Epic / slice variants + +When the file groups other patterns (epic) or saves a multi-pattern view (slice), add `@architect-level:epic` / `@architect-level:slice` and drop `@architect-parent`: + +```gherkin +@architect +@architect-pattern: +@architect-status:candidate +@architect-maturity:idea +@architect-product-area: +@architect-level:epic +Feature: - + + **User Story:** As , we want so that . + + **Members:** + - + - + + Rule: + **Invariant:** +``` + +A **slice** is the same with `@architect-level:slice` and a `**Usage:**` line under the members. Slices live in `architect/slices/.feature`. To list an epic's members from the graph instead of hand-tracking the bullet list: `pnpm architect:q 'g.patterns.filter(p => p.parent === "").map(p => p.name)'`. An unknown parent yields an empty list. Verify the name with `g.findByConcept` before trusting an empty result. + +The `**Members:**` bullets are human-facing orientation only. The authoritative member set is edge-derived from reverse `@architect-parent` links, so keep the list as reader help rather than the source of truth. + +## Candidate-tier delta (only when promoting from idea) + +Idea shape plus an `**Open Questions:**` block and 1-2 happy-path scenarios: + +```gherkin + **Open Questions:** + - + - + + @acceptance-criteria @happy-path + Scenario: + Given + When + Then +``` + +The promotion is mechanical: `git mv architect/specs/ideas/.feature architect/specs/candidates/.feature`, drop the explicit `@architect-maturity:idea` (removing it releases the spec from idea-tier gating; maturity derives to `idea` from `status:candidate`), add the open-questions block, add 1-2 scenarios. `@architect-status` stays `candidate` until the acceptance gate later flips it to `roadmap` (which becomes the plan tier). + +## Notes: non-negotiable at idea tier + +Block these aggressively (the idea-tier anti-pattern set; details in the ladder reference): + +- **No deliverables.** Ideas are not committed to files. +- **No phase / effort / priority / release metadata.** Planning metadata means commitment. +- **No ADRs.** If an idea needs a decision, note it in the parent epic, not here. +- **No narrative.** One-line Feature description. _Needing_ more than one line means the idea is ready for candidate tier. That is signal to promote, not to grow the idea file. +- **No scenarios at idea tier.** Rules-with-invariants suffice. Scenarios belong at candidate tier and above. +- **No `**Rationale:**`/`**Verified by:**` at idea tier.** Those are plan-tier additions. + +> **Tripwire: retroactive plan-level specs (the #1 failure mode).** If the validator reports missing Gherkin coverage for a pattern that is _already shipping_, the fix is to tag an existing executable feature with `@architect-implements:` and enrich it. Never author a fresh plan-level spec. A plan-level spec is meant to die after implementation. Conjuring one back to "cover" shipped behavior inverts the pipeline and leaves a zombie. (Refactoring carve-out: backfilling coverage skips candidate and plan, to the executable `*ExecutableTests` convention in practice. Never a fresh plan-level spec.) + +## Output for this session + +One of: (a) authored a fresh idea spec under `architect/specs/ideas/`; (b) promoted an idea to candidate (open questions + 1 scenario, moved to `architect/specs/candidates/`); or (c) decided not to write yet. Refining intent in conversation is valid at this tier. If (c), say so and recommend re-invoking when ready. + +## Do not + +- Do not author scenarios at idea tier even if asked. Promote to candidate first, with the explicit track flip. +- Do not skip rungs. Candidate → Plan and Plan → Design edit in place and belong to later sessions. + +**Next session.** Once the acceptance gate clears and the candidate is promoted to plan/`roadmap`, the design work continues in [`design.md`](design.md). diff --git a/.agents/skills/architect-sessions/references/review-implementation.md b/.agents/skills/architect-sessions/references/review-implementation.md new file mode 100644 index 0000000..adf28df --- /dev/null +++ b/.agents/skills/architect-sessions/references/review-implementation.md @@ -0,0 +1,87 @@ +# Review (implementation): post-merge value-transfer verification + +The implementations are done; the design specs may or may not still exist. Verify value has transferred to executable Gherkin and JSDoc, then either confirm batched deletion is safe or report what's blocking it. + +> This is the **post-implementation** counterpart to [`review-spec.md`](review-spec.md) (which reviews specs _before_ implementation). The two do not overlap. Pick by lifecycle phase. + +Doctrine depth: the pre-deletion gate + transfer checklist + anti-patterns are in [`ephemeral-spec-deletion.md`](ephemeral-spec-deletion.md); the forward/reverse link pair + `*ExecutableTests` are in [`../../architect-base/references/spec-pattern-relationships.md`](../../architect-base/references/spec-pattern-relationships.md); split-ownership (**production-TS JSDoc is additive. Never flag its absence as a value-transfer blocker**) is in [`../../architect-base/references/annotation-ownership.md`](../../architect-base/references/annotation-ownership.md). + +## Gather context first + +1. **Which patterns?** Reviewing a comma-separated set as a batch is the common case. Get the full list. +2. **Spec state.** Are the design specs still present, or already deleted? (Deleted specs make the forward-link check moot; verify against memory of the spec.) +3. **Authorization.** Is deletion in scope for _this_ session, or review-only? Default is review-only; deletion is opt-in. + +## Pre-flight + +Run the implement-mode pre-flight from [`../../architect-graph-handle/SKILL.md`](../../architect-graph-handle/SKILL.md) (ADR-014) for the reviewer's view of what shipped, plus the global blocker view: + +```bash +pnpm architect:q 'g.patterns.filter(p => p.status === "roadmap" && p.uses.some(u => g.pattern(u)?.status !== "completed")).map(p => p.name)' +``` + +Scope readiness (PASS / WARN / BLOCKED) remains the `architect_scope_validate` MCP tool. For a batch orientation across the whole reviewed set, read the generated design-review document under `docs-live/` (regenerate with `pnpm docs:all`; the `architect_documentation` MCP tool serves the same content). It renders every in-scope pattern status-annotated (`Name (role · status)`, e.g. `MCPServer (service · completed)`) grouped by layer / package / theme, so you can see which patterns are `completed` vs still `active` (and which deliverables are still unbuilt `candidate` / `roadmap` specs) at a glance instead of reconstructing it from per-pattern calls. Then, per pattern in scope: + +```bash +pnpm architect:q 'const p = g.pattern(""); return {p, invariants: g.invariantsOf(""), reverifies: g.specsReverifying([""]).length}' +pnpm architect:q 'const p = g.pattern(""); return {file: p?.sourceFile, realizing: p?.implementedBy}' +``` + +For a typed per-pattern bundle, call the `architect_bundle` or `architect_context` MCP tool. Until `architect_value_transfer` ships, walk the manual gate below. The planned check is that MCP tool plus the pure `projectValueTransferState` kernel, returning `deletionReady`. It is not a q method or a named `architect:graph` command. + +## Per-pattern verification (apply the gate) + +For each pattern: + +1. **Forward link.** Does the design spec carry `@architect-executable-specs:`? (Moot if the spec is already deleted.) +2. **Forward link resolves.** Does that path point at a real file under `tests/features/`? +3. **Reverse link.** Does that target feature carry `@architect-implements:` for the focal pattern? +4. **Rich content landed, and distilled.** Every Rule block in the design spec has a counterpart in the executable feature carrying `**Invariant:**` (and, where present in the source, `**Rationale:**` + `**Verified by:**`), but **distilled, not transcribed.** a `**Rationale:**` that only restates its `**Invariant:**`, a `**Verified by:**` repeated verbatim across rules, or a step stub / JSDoc comment re-explaining the pattern (rather than its local how) is **Transcription bloat** ([`ephemeral-spec-deletion.md`](ephemeral-spec-deletion.md)). Remedy = slim the destination, not block deletion. +5. **Production-TS rationale (judgment).** Architecturally significant rationale that doesn't fit in Gherkin lives in JSDoc, but **annotations are additive**, so absence is not a blocker; presence enriches discoverability. +6. **Graph integrity.** `pnpm architect:graph dangling --baseline packages/architect-guard/src/lint/dangling-baseline.json --strict`. Exit 0 means no new dangling references; non-zero means the graph regressed (resolve the new edge, or deliberately rewrite the baseline with `--write-baseline` and explain why). + +## Output format + +One table for the reviewed set, then a recommended action per pattern: + +``` +**Implementation review: , , ** + +| Pattern | Forward link | Reverse link | Rich content | Annotations (additive) | Deletion-ready | Recommended action | +| ------- | ------------ | ------------ | ------------ | ---------------------- | -------------- | ------------------ | +| | ✓ | ✓ | ✓ | ✓ partial | YES | `git rm ` (batched in this PR) | +| | ✓ | ✗ missing | ✓ | n/a | NO | Add `@architect-implements:` to , then re-review | +| | (spec already deleted) | ✓ | ✓ | n/a | (already done) | confirm earlier deletion was correct | + +**Batched deletion plan.** +- Delete now: , (already done) +- Block on: (reverse link missing) +``` + +Found nothing wrong? State it in one sentence. No elaborate restatement. + +## Spec-deletion step (only if the user authorizes) + +```bash +git rm … +git rm -r … +pnpm architect:q 'return {counts: g.graph.counts, active: g.patterns.filter(p => p.status === "active").map(p => p.name)}' # confirm patterns show completed without lingering specs +pnpm docs:all # regenerate docs +``` + +Confirm with the user before `git rm`. Default is **review only**; deletion is opt-in per session. + +## Anti-patterns (stop) + +- **Re-authoring spec content.** This is verification, not design. If rich content didn't transfer, report the gap; route the fix to the implementer or a follow-up [`implement.md`](implement.md) session. +- **Deleting specs whose value hasn't transferred.** Every pre-deletion gate criterion must hold. +- **Gating on production-TS JSDoc presence.** Annotations are additive; a pattern with zero JSDoc and a complete executable feature is legitimately complete. +- **Reading source via Read/Glob/Grep before the graph-handle pre-flight.** + +## Do not + +- Do not transition the FSM here. Reopening a pattern is a separate [`implement.md`](implement.md) session; `@architect-unlock-reason:` is optional there and suppresses the advisory warning. +- Do not delete specs without explicit user authorization this session. +- Do not paraphrase the implementations back as a summary. Per-pattern verdicts only. + +**Next session.** Capture outcomes with [`handoff.md`](handoff.md); route any blocked pattern's fix back to [`implement.md`](implement.md). diff --git a/.agents/skills/architect-sessions/references/review-spec.md b/.agents/skills/architect-sessions/references/review-spec.md new file mode 100644 index 0000000..3f580a4 --- /dev/null +++ b/.agents/skills/architect-sessions/references/review-spec.md @@ -0,0 +1,82 @@ +# Review (spec): pre-implementation gap-finding + +Find gaps in a spec **before** implementation so the implementer has a complete prompt. **Do not rewrite content.** Do not generate enriched session prompts. Output is a compact gap list. + +> This is the **pre-implementation** review. To review **completed implementations** (verify value transfer + decide batched deletion), use [`review-implementation.md`](review-implementation.md). The two do not overlap. Pick by lifecycle phase. + +Doctrine depth (for judgment calls about Gherkin or pattern conventions): the optional Rule-block template + tier guidance in [`../../architect-base/references/rule-block-template.md`](../../architect-base/references/rule-block-template.md); the tier table in [`../../architect-base/references/four-tier-ladder.md`](../../architect-base/references/four-tier-ladder.md); the bipartite conventions + `*ExecutableTests` escape hatch in [`../../architect-base/references/spec-pattern-relationships.md`](../../architect-base/references/spec-pattern-relationships.md). + +## Gather context first + +Know what "complete" means for _this_ spec before scanning for gaps: + +1. **Tier.** Idea/candidate (structural checklist below) or plan/design (`architect_scope_validate` gate + full checklist)? +2. **Normative source.** What ADR / redesign / brief does the spec derive from? You'll check coverage against it. +3. **Scope of review.** One spec, or several concurrent ones that might collide on the same files? + +## Pre-flight + +Run the pre-flight from [`../../architect-graph-handle/SKILL.md`](../../architect-graph-handle/SKILL.md) (ADR-014): + +```bash +pnpm architect:q 'return {counts: g.graph.counts, active: g.patterns.filter(p => p.status === "active").map(p => p.name)}' +pnpm architect:q 'const p = g.pattern(""); return {p, invariants: g.invariantsOf(""), reverifies: g.specsReverifying([""]).length}' +pnpm architect:q 'g.graph.relationshipIndex[""]' +pnpm architect:q 'g.patterns.filter(p => p.status === "roadmap" && p.uses.some(u => g.pattern(u)?.status !== "completed")).map(p => p.name)' +pnpm architect:q 'const p = g.pattern(""); return {file: p?.sourceFile, realizing: p?.implementedBy}' +``` + +Scope readiness is the `architect_scope_validate` MCP tool; its verdict (PASS / WARN / BLOCKED) frames the rest. For pre-implementation shape review, also read the generated design-review document under `docs-live/` (regenerate with `pnpm docs:all`; the `architect_documentation` MCP tool serves the same content). It draws the live pattern graph _including this not-yet-built spec_ as a component map (by-layer / by-package / by-theme), classified nodes annotated `Name (role · status)` (e.g. `MCPServer (service · completed)`; unbuilt specs render status-only `(candidate)` / `(roadmap)`), so you see how the planned pattern slots into the existing graph instead of grepping feature files. + +**Tier note.** `architect_scope_validate` accepts only `design` and `implement`. For idea/candidate reviews, skip that gate and use the structural checklist below. + +### Idea/candidate-tier structural checklist (no scope gate) + +- **File location matches maturity.** Idea → `architect/specs/ideas/`; candidate → `architect/specs/candidates/`. Mismatch is a gap. +- **Idea-tier six-tag baseline present** (`@architect`, `@architect-pattern`, `@architect-status`, `@architect-maturity:idea`, `@architect-product-area`, `@architect-parent`). The explicit `@architect-maturity:idea` is **required** at idea tier. It is the guard's idea-tier opt-in, so its absence (the file is not recognized as idea-tier) is a gap. Epic/slice swap `@architect-parent` for `@architect-level`. A candidate-tier spec normally has no explicit maturity (it derives to `idea` from `status:candidate`). The maturity gap to catch is a **stray `@architect-maturity:idea` on a non-idea-tier file**. It mis-gates the spec as idea-tier. Do **not** flag an explicit `@architect-maturity:plan` override (delivery track, valid per §04 "explicit always wins" + ADR-007). That is permitted, not a gap. +- **Line budget honoured.** Idea ≤30 (warn-only); candidate 30-80. Over-budget = premature-promotion gap. +- **No deliverables / no phase/effort/priority/release tags at idea tier** = premature plan-tier-metadata gap. +- **Rules carry `**Invariant:**` only at idea tier.** Adding `**Rationale:**`/`**Verified by:**` there is a gap. +- **Candidate carries `**Open Questions:**` + 1-2 happy-path scenarios.** Missing open-questions is the most common gap. Inventory with a content grep (open questions are authored blocks the graph doesn't index): `grep -rn -A4 'Open Questions' architect/specs/`. Scope to an epic's children via `pnpm architect:q 'g.pattern("")?.children'` if needed. +- **No retroactive idea spec for shipped code.** If the pattern already has production code, the idea spec is the wrong artifact. Flag it. + +## The gap-finding checklist (plan/design tier) + +1. **Normative source coverage.** Read the ADR/redesign/brief. Are all its types, constants, and constraints represented in the spec's deliverables? Grep for them in the referenced files. +2. **Deliverable path correctness.** Each `Background:` path must exist (or be one the spec explicitly creates). Check with `pnpm architect:q 'const p = g.pattern(""); return {file: p?.sourceFile, realizing: p?.implementedBy}'` + direct existence. A typo ships a broken implementation. +3. **Type reuse.** If a Zod schema / interface already exists in `packages/`, the spec should reference and reuse it, not redefine it. +4. **Dependency chain.** `pnpm architect:q 'g.graph.relationshipIndex[""]'`. Anything blocking? The global view: `pnpm architect:q 'g.patterns.filter(p => p.status === "roadmap" && p.uses.some(u => g.pattern(u)?.status !== "completed")).map(p => p.name)'`. A dependency that is `roadmap` and unimplemented means not-ready. +5. **Scope-validate state** (the `architect_scope_validate` MCP tool). PASS = ready; WARN = recoverable miss; BLOCKED = upstream dependency or invariant violation. +6. **Implied file modifications.** Does the source imply changes the `Background:` table omits? Common miss: a new type in a shared package needing a barrel re-export. +7. **Edge cases vs scenarios.** For each Rule, is there both a happy-path and at least one error/boundary scenario? +8. **Stub completeness.** Does every architecturally-relevant pattern in the deliverables have a stub? (Stubs are for shape decisions, not trivial functions.) +9. **Overlap with concurrent specs.** Two specs in the same phase touching the same files is a sequencing hazard. Report it. +10. **Ephemeral readiness.** When implemented and deleted, will value transfer cleanly? Does every rule have an `**Invariant:**`? Does every decision have enough rationale to become a JSDoc annotation? A spec that won't transfer cleanly will leave debt. +11. **Graph fit (optional).** The generated design-review document under `docs-live/` (regenerate with `pnpm docs:all`) renders the in-scope spec status-annotated `(role · status)` in the live component graph (by-layer / by-package / by-theme); confirm its depends-on edges land in the expected layer/package cluster and no dependency is unexpectedly an unbuilt `(roadmap)` / `(candidate)` node. +12. **Re-explanation smell (density).** Flag prose that re-explains an established or industry-standard shape (a CRUD endpoint, a standard codec, a barrel) or re-derives a pattern already defined elsewhere. These are `architect-base` §10's "skip detail" cases, not design judgment. Flag a `**Rationale:**` that only restates its `**Invariant:**`, and any `**Verified by:**` string repeated verbatim across rules. Owner: "collapse to a reference / drop the restatement." Do **not** flag deliberate depth on architecturally significant or novel work (§10 "invest detail"). + +## Output format (compact, no rewrites) + +``` +**Gaps found in design spec** + +1. : . Owner: +2. : . Owner: +``` + +Found nothing? Say so in one sentence. Do not produce an elaborate "looks good" restatement. + +## Anti-patterns (stop) + +- **Rewriting the spec.** Report the gap; let the design author fix it. +- **Generating wrapper / enriched-prompt documents.** The spec is the prompt. +- **Implementing what's missing.** This is review; an unclear deliverable is the gap "deliverable unclear," not "I'll write it." +- **Reading source via Read/Glob/Grep before the graph-handle pre-flight.** `g.pattern(...)` / `g.graph.relationshipIndex[...]` first. + +## Do not + +- Do not transition the FSM here. +- Do not delete the design spec. That's [`implement.md`](implement.md), after value transfer. +- Do not paraphrase the spec back as a summary. Report gaps only. + +**Next session.** Route gap fixes back to [`design.md`](design.md); when `architect_scope_validate` for `` `implement` is PASS, proceed to [`implement.md`](implement.md). diff --git a/.agents/skills/architect-verify-handoff/SKILL.md b/.agents/skills/architect-verify-handoff/SKILL.md deleted file mode 100644 index 01f13a4..0000000 --- a/.agents/skills/architect-verify-handoff/SKILL.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -name: architect-verify-handoff -description: Use at session end to capture canonical Architect state for continuation — runs the handoff CLI command, lists current pattern state, dependencies, blockers, modified files, and outstanding work. Produces a compact handoff note, not a session recap. Do NOT use for: mid-implementation status reports — used at session end, not session middle. Also do NOT use for sprint retros, daily standups, or generic "what did I do today" summaries — handoffs are forward-looking pattern state for the next session, not backward-looking recaps. -allowed-tools: - - Bash - - Read - - Glob - - Grep ---- - -# Architect Handoff Verification - -The session is wrapping. Capture exactly what the next session will need — -nothing more. - -## Doctrine references - -When the handoff captures FSM state or routes to a downstream skill, -defer to the shared references: - -- [`../_shared/fsm-transitions.md`](../_shared/fsm-transitions.md) — - canonical valid transitions; `@architect-unlock-reason:` audit-trail - requirement; what `scope-validate` outputs mean. -- [`../_shared/canonical-references.md`](../_shared/canonical-references.md) - — anti-anecdote rule for any judgment call about "what the - methodology says." - -## Pre-flight - -Run the canonical handoff pre-flight from -[`../architect-data-api/SKILL.md`](../architect-data-api/SKILL.md) §"Handoff" -(it covers `overview`, `context`, `arch blocking`, and `open-questions` for -forward-looking signal). Then run the anchor verb of this skill — it writes -the canonical record: - -```bash -pnpm architect:query handoff --pattern --session [--modified-file ...] -``` - -For multi-pattern sessions, run `handoff` per pattern. - -## What to extract - -For each pattern touched: - -| Field | Source | -| ----------------------------- | ----------------------------------------------------------------------- | -| Session intent | What you were doing (`planning` / `design` / `implement` / `review`) | -| Pattern name | The primary pattern under work | -| Current FSM state | `pnpm architect:query context --session implement` — read the `=== FSM ===` line | -| Transitions made this session | Your edit history | -| Files modified | Pass to `--modified-file` flags on handoff | -| Open dependencies | `pnpm architect:query dep-tree ` minus the satisfied ones | -| Open blockers | `pnpm architect:query arch blocking` filtered to anything touching this pattern | -| Outstanding open questions | `pnpm architect:query open-questions [--parent ]` — forward-looking signal | -| Outstanding work | What you didn't finish, with one-line "why" each | - -## Handoff note format - -``` -**Architect handoff — ()** - -- State: (was: ) -- Modified: -- Blockers: -- Outstanding: -- Recommended next: for -``` - -Five fields, no recap of conversation, no thanks-for-this-session prose. -The next session reads this verbatim and starts work. - -## Recommended-next-skill table - -The four-tier ladder has four rungs (idea → candidate → plan → design); the -implement and review skills sit alongside. Use this table to set the -`Recommended next:` field in the handoff note: - -| Session ended at | Spec state | Recommended next skill | -| ---------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| Idea tier | Idea captured, ready to refine | `architect-plan-session` (promote idea → candidate) | -| Candidate tier | Open questions resolved, acceptance gate cleared | `architect-plan-session` (promote candidate → plan; flips status to `roadmap`) | -| Plan tier | Plan-level spec ready for design tier | `architect-design-session` (promote plan → design) | -| Design tier | `scope-validate implement` returns PASS | `architect-implement-spec` | -| Design tier | `scope-validate implement` returns WARN/BLOCKED | `architect-review-spec` (find gaps) then back to `architect-design-session` | -| Implement | Spec deleted, value transferred | (none — pattern complete, optionally start next pattern's planning) | -| Implement | Value transferred, spec deletion deferred to code review | `architect-review-implementation` (batched value-transfer verification + deletion) | -| Review | Gap list produced | `architect-design-session` to fix gaps, OR `architect-implement-spec` if PASS | -| Review-implement | Per-pattern verdicts produced, batched deletion proposed | (none if user authorized deletion this session; otherwise re-invoke when ready) | - -The full ladder lives at [`../_shared/four-tier-ladder.md`](../_shared/four-tier-ladder.md). - -## Anti-patterns (stop) - -- **Free-form recap.** "We talked about X, then I implemented Y, and the user - said Z." Cut it. The handoff is forward-looking only. -- **Skipping `handoff` CLI.** That command writes the canonical record. If you - skip it, the next session has no authoritative source. -- **Recommending the wrong next skill.** Cross-check the recommended-next - skill against the table above. The most common miscalls: routing a candidate - spec to design-session (it needs plan tier first), or routing a BLOCKED - design back to implement (it needs review-spec first to surface the - blocker). - -## Do not - -- Do not declare a session "done" without running `handoff`. -- Do not commit or push without the user's explicit approval (governed by the - Claude Code permission system, not this skill, but worth restating). diff --git a/.agents/skills/omo-plan-author/SKILL.md b/.agents/skills/omo-plan-author/SKILL.md new file mode 100644 index 0000000..eef29c3 --- /dev/null +++ b/.agents/skills/omo-plan-author/SKILL.md @@ -0,0 +1,599 @@ +--- +name: omo-plan-author +description: Use when authoring a work plan for OpenCode / Oh-My-OpenAgent's `/start-work` (Sisyphus executor). Triggers on make an OmO plan, draft a plan for Sisyphus, write a work plan to `.sisyphus/plans/`, or any plan handoff to OmO. Produces a single markdown plan in `.sisyphus/plans/{slug}.md` in the Prometheus plan format, including the boulder.json safety protocol (never delete an in-progress plan). Do NOT use for in-session execution by this Claude session, generic project planning, or Architect spec authoring (route to architect-sessions). +allowed-tools: + - Bash + - Read + - Edit + - Write + - Glob + - Grep +--- + +# OmO plan author (Claude Code to Sisyphus handoff) + +Author OmO-compatible work plans from inside Claude Code so the user can run `/start-work` in OpenCode and have Sisyphus pick them up immediately. This skill encodes the Prometheus Claude-Opus-default plan rules with paths rewritten for this repo's state folder (`.sisyphus/` instead of `.omo/`). + +When you load this skill, briefly state that the **omo-plan-author** skill is loaded so the user can confirm activation. + +## 1. Identity: author, not executor + +**You are authoring a plan. You are NOT executing it. The plan is for Sisyphus (OmO) to execute via `/start-work`.** + +- Output is exactly ONE file: `.sisyphus/plans/{slug}.md`. +- The file is the only deliverable. No drafts, no companion docs, no commits. +- Do not touch source code, do not run tests, do not start implementation. +- Acceptance criteria in the plan must be agent-executable (Sisyphus or its dispatched workers will run them). Never "user manually verifies." + +If the user asks you to also do the work, refuse. Generate the plan; let `/start-work` do execution. OmO/Sisyphus is better at parallel execution than Claude Code is at planning for OmO. + +## 2. Paths in this repo + +Prometheus's upstream prompt targets `.omo/`. This repo uses `.sisyphus/` as the OmO state folder. Rewrite throughout: + +| Upstream (Prometheus) | This repo (use this) | +| ------------------------------------- | ------------------------------------------------------------ | +| `.omo/plans/{name}.md` | `.sisyphus/plans/{slug}.md` | +| `.omo/evidence/task-{N}-{slug}.{ext}` | `.sisyphus/evidence/task-{N}-{slug}.{ext}` | +| `.omo/drafts/` | **Do not use drafts.** Claude Code authoring is single-shot. | +| `.omo/notepads/` (per-plan notes) | `.sisyphus/notepads/{slug}/` | + +The plan body text itself must use the `.sisyphus/...` form. Sisyphus's executor honors the canonical state folder; mismatched paths will leak into evidence files that no one finds. + +## 3. boulder.json: safety protocol (CRITICAL) + +`/start-work` will only pick up a new plan when there is **no active boulder**. `boulder.json` is OmO's "currently-executing plan" pointer. + +**Rule.** Never delete `boulder.json` without first confirming the prior plan is terminal. + +### Read-before-delete protocol + +1. **Read `.sisyphus/boulder.json`**. If it doesn't exist, it is safe. No boulder to remove. Write the new plan and stop. +2. If it exists, parse the JSON. Inspect these fields (observed shape, 2026-05-18): + - `active_plan`: absolute path to the plan markdown. + - `plan_name`: short slug. + - `started_at`: ISO timestamp. + - `session_ids`: array of OmO session IDs that have touched this boulder. + - `task_sessions`: object mapping task key to worker-session metadata (`session_id`, `agent`, `category`, `updated_at`). +3. **Treat the boulder as IN-PROGRESS / PAUSED if any of these are true.** + - `active_plan` resolves to a file that still exists. + - `session_ids` array is non-empty. + - `task_sessions` object has any entry. +4. **If in-progress/paused.** STOP. Do not delete. Report to the user: + - The active plan name + path. + - When it was started. + - The most recent `task_sessions` entry. + - Ask explicitly: "There's an in-progress boulder for `{plan_name}` (last activity {updated_at}). Are you done with it, or do you want to keep it alive and just author the new plan without clearing the boulder?" +5. **Only after the user explicitly confirms the prior plan is done.** Proceed to the cleanup step below. + +### Cleanup step (only when user confirms prior plan terminal) + +```bash +# 1. Delete boulder.json +rm .sisyphus/boulder.json + +# 2. Optionally: clean evidence + notepads for the prior plan slug +# Match exact slug + similar-name variants (the user explicitly wants this nicety). +PRIOR_SLUG="{prior plan_name}" + +# Evidence (best-effort) +ls .sisyphus/evidence/ 2>/dev/null | grep -Ei "^${PRIOR_SLUG}(-|$|\.|_)" +# Show matches first, get user confirmation, then rm. + +# Notepads for the prior plan slug (and close variants) +ls .sisyphus/notepads/ 2>/dev/null | grep -Ei "^${PRIOR_SLUG}(-session[0-9]+)?$" +# Show matches first, get user confirmation, then rm -rf each matched dir. +``` + +**Never delete evidence or notepads silently.** Always show the match list to the user and wait for explicit confirmation. The "similar name" rule is a nicety. Show fuzzy matches, let the user decide. + +### When the user is starting fresh + +If `boulder.json` doesn't exist, no cleanup is needed. Just write the new plan to `.sisyphus/plans/{slug}.md`. + +## 4. Plan workflow + +### Step 1: Interview (if requirements are unclear) + +If the user's request is ambiguous, run a short interview (3-5 targeted questions max): + +- Core objective in one sentence. What does success look like? +- Scope IN / Scope OUT. What's explicitly excluded? +- Test strategy. TDD, tests-after, or no tests + agent QA only? +- Tech constraints. Language, framework, existing patterns to follow? +- Parallelism affordances. Independent modules vs sequential dependencies? + +Skip the interview if the user has already described the work in enough detail; jump straight to plan generation. + +### Step 2: Quick research + +Use `Read`, `Glob`, `Grep` (or the Explore agent) to verify any file/symbol references you plan to put in the plan. Plans that cite files that don't exist will reject in Sisyphus's compliance audit. + +### Step 3: Write the plan + +Use the template in § 7 below. Write to `.sisyphus/plans/{slug}.md`. + +**Incremental-write protocol** (from Prometheus; applies here too): + +- Write the skeleton (all sections except individual TODO bodies) with `Write`. +- Append TODO batches (2-4 tasks per `Edit` call) using `Edit` with `oldString="---\n\n## Final Verification Wave"` as the insertion anchor. +- Read the file back at the end to verify nothing was truncated. +- **Never call `Write` twice on the same file.** It overwrites the first call. + +### Step 4: Present summary, hand off + +Present to the user: + +``` +## Plan Generated: {slug} + +**Key Decisions Made.** +- [Decision 1]: [Rationale] + +**Scope.** +- IN: [list] +- OUT: [list] + +**Guardrails.** +- [Must-NOT-do] + +Plan saved to: `.sisyphus/plans/{slug}.md` + +Next step: +- Open OpenCode, run `/start-work {slug}` to dispatch Sisyphus. +- If a boulder.json was cleared, the workspace is ready. +- If a boulder.json was preserved (prior plan in-progress), pause this plan until that one is done. +``` + +Do not run `/start-work` yourself. It lives in OpenCode, not Claude Code. + +## 5. Long-running execution context (load-bearing for huge-scope plans) + +OmO is used almost exclusively for long-running work. Typical runs are **12-24-48 hours, sometimes days**. Authoring plans for this needs three context pieces that Prometheus's upstream prompt does not state explicitly but which materially change plan shape. + +### 5.1 The executor is a routing layer, not a hero model, and it is GPT, not Claude + +OmO runs multi-day work through three delegation levels. **Atlas** is the read-only conductor. It reads the plan, writes the detailed 50-200-line worker prompts, accumulates wisdom into `.sisyphus/notepads/{slug}/` and passes it forward, enforces gates, and delegates all writes. Next is **category routing** (`ultrabrain` / `deep` / `writing` / `quick` / … to Sisyphus-Junior workers on intent-matched models + fallback chains). Then **specialized subagents** (Oracle architecture, Librarian docs, Explore codebase, Hephaestus deep reasoning), gated before execution by Metis (gap-analysis) and Momus (plan review). Intelligence sits in the routing layer, not in any one worker model. + +**Match plan shape to the executor's model family.** Selection is characteristic-driven and version-specific ("a model isn't just smarter or dumber. It thinks differently"): + +- **Claude** wants mechanics: checklists, templates, step-by-step recipes. +- **GPT** wants goals: _"state the goal and let it figure out the mechanics."_ + +OmO's executors are **GPT, not Claude**. Claude is off-limits for OmO execution (Max-subscription ToS), so write for the GPT characteristic. _Which_ GPT version runs each tier rotates as models ship and the routing config matures, so **read `~/.config/opencode/oh-my-openagent.jsonc` for the live wiring rather than trusting any version named in a skill**. Author **goal-stated scope + verifiable completion criteria, not taxative recipes**. Atlas writes the worker recipes at runtime, and a taxative plan is impossible for discovery work anyway (you cannot pre-enumerate a sweep). This **inverts the old Claude-era "recipe, not goal" rule**. For GPT, state the goal and let exhaustiveness find every file. Pick **categories by the characteristics a task needs** (reasoning depth / exhaustiveness / prose / speed), not by model name; the config resolves the model. + +Still true regardless of family: exhaustiveness is the executor's signature; references must be concrete and verified (§6) but are **starting points, not the boundary**; huge plans are fine (50-200 TODOs; the Single-Plan Mandate §6.1 holds). + +### 5.2 Execution modes: `single-shot` / `loop` / `hybrid-loop` + +Prometheus + Atlas now support three execution shapes. The mode is part of the plan and shapes its phase structure. + +| Mode | When to pick | Plan shape | +| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| `single-shot` | Scope fits one Atlas session without breaching a hard checkpoint (rare for 24h+ scope) | One linear plan, no phase markers, no handover points | +| `loop` | Full scope is fully planned up front, but execution is chunked at critical gates / mandatory commits / time splits. Atlas hands back to Prometheus on phase completion OR on critical issues. | Full plan + explicit phase markers + handover triggers per phase | +| `hybrid-loop` (**ideal for huge scope**) | Prometheus has good context on the full scope but **only plans the first phase in detail**. When Atlas hands over, a fresh Prometheus session inspects completed work and plans the next phase. | Phase 1 detailed + Phase 2+ outlined as scope-only headlines | + +**Default to `hybrid-loop` for any plan whose full scope cannot be Atlas-executed in a single session.** Single-shot is the exception, not the rule. + +The user picks the mode. If they don't say, **ask once**. It changes plan structure significantly. + +When mode is `loop` or `hybrid-loop`, insert a `## Phase Plan` section between TL;DR and Context (template in § 7). + +### 5.3 Gates and mandatory commits: strong-language requirements (CRITICAL) + +Gates and mandatory commits do **not happen** in long Atlas runs unless the plan states them in strong, unambiguous language. This is load-bearing. + +**Write gates as imperatives, not as suggestions.** + +- BAD: "It might be a good idea to run tests after this task." +- BAD: "Consider committing here." +- GOOD: "**MANDATORY GATE. STOP execution until all of: (a) `pnpm typecheck` returns exit 0, (b) `pnpm test` returns 0 failures, (c) `pnpm validate:all` returns exit 0. If ANY check fails, HANDOVER to Prometheus immediately.**" + +**Every commit boundary must be.** + +1. **Explicitly marked** as `COMMIT: MANDATORY` or `COMMIT: NO`. +2. **Named** with the exact commit message (`type(scope): imperative summary`). +3. **Scoped** with the exact file list to stage (never `git add -A`). +4. **Pre-commit gated** with the exact verification command(s). + +Atlas will obey `COMMIT: MANDATORY` + an exact message. Atlas will NOT infer commit intent from prose. Weak language = no commits. + +**Handover triggers (loop / hybrid-loop only).** Write as a closed list per phase. + +``` +HANDOVER TO PROMETHEUS IF ANY: +- Phase scope completed AND F1-F4 verdicts all APPROVE +- A gate failed and the cause is not in the plan's "Must NOT do" list +- A reference cited in the plan resolves to a non-existent file or symbol +- More than {N} tasks have been added beyond the plan's TODO list +- {custom trigger specific to this plan} +``` + +The plan is the contract. If Atlas is unsure, it must hand over. Stating that weakly leads to off-plan execution that's expensive to roll back. + +### 5.4 Scope estimates: buckets, NOT time + +Human-time estimates are nonsensical for these plans. Atlas's clock is not a human's clock, and Atlas-on-XL routinely takes 24+ hours by design. **Drop time framing entirely.** + +The `Estimated Effort` field in the TL;DR uses **scope/complexity buckets**, not duration: + +| Bucket | Meaning | +| -------- | ---------------------------------------------------------------------------------------- | +| `Quick` | One focused file edit, ≤2 acceptance criteria | +| `Short` | Single module, ≤5 acceptance criteria, no cross-cutting concerns | +| `Medium` | Multi-module, single bounded context, ≤15 acceptance criteria | +| `Large` | Multiple bounded contexts, 15-50 acceptance criteria, cross-cutting work | +| `XL` | Multi-package / migration / repo-wide / dependency-bump-cascade, 50+ acceptance criteria | + +Use these as **organizing buckets** when sizing waves. Never as time estimates. Never write "this will take 2 hours" or "estimated 3 days" in a plan body. + +### 5.5 Gates are adversarial: author for proof, not assertion + +OmO's review gates are ruthless and iterative. Oracle / Momus and the Final Verification Wave **reject completion over and over. 50+ rounds is normal.** They stop only when every claim is done and its proof is recorded. Do the work up front: write every acceptance criterion as **evidence-producing**, a command whose captured output lands in `.sisyphus/evidence/task-{N}-…`, never as an assertion a reviewer must take on faith. A criterion that cannot emit a recorded proof bounces the whole completion. Front-load the proofs the gates will demand. The cost of a vague criterion is paid 50× at the end, not once. + +--- + +## 6. Non-negotiable constraints (lifted from Prometheus Claude default) + +These are the same rules that Sisyphus's plan-compliance audit will check. Violate them and the plan rejects at the F1 phase. + +1. **Single plan mandate.** Everything goes into ONE file in `.sisyphus/plans/`. No multi-phase split plans. Large work = longer TODO list, not multiple files. +2. **Maximum parallelism principle.** Granularity rule: one task = one module/concern = 1-3 files. If a task touches 4+ files or 2+ unrelated concerns, SPLIT IT. Target 5-8 tasks per wave; <3 per wave (except the final integration wave) means under-splitting. +3. **Dependency minimization.** Extract shared dependencies (types, interfaces, configs, schemas) as early Wave-1 tasks so subsequent waves can fan out maximally. +4. **Zero-human-intervention verification.** Every acceptance criterion must be agent-executable: command, tool invocation, file/diff check. "User manually verifies/tests/confirms" is FORBIDDEN. +5. **QA scenarios are mandatory per task.** Minimum: 1 happy-path + 1 failure/edge case. Specific selectors, concrete test data, exact assertions, evidence file path. A task without QA scenarios is incomplete and will be rejected. +6. **Evidence paths use `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`.** +7. **No retroactive scope creep in the plan body.** If mid-plan you discover scope is wrong, report it to the user and re-author. Do not just edit silently around it. +8. **Markdown only.** The plan is `.md`. No JSON sidecars, no scripts. + +## 7. Plan template (the deliverable shape) + +This is the Prometheus Claude-default template, paths rewritten to `.sisyphus/`. Render this skeleton into `.sisyphus/plans/{slug}.md` and fill in the bracketed fields. + +```markdown +# {Plan Title} + +## TL;DR + +> **Quick Summary**: [1-2 sentences capturing the core objective and approach] +> +> **Deliverables**: [Bullet list of concrete outputs] +> +> - [Output 1] +> - [Output 2] +> +> **Estimated Effort**: [Quick | Short | Medium | Large | XL], scope bucket, NOT duration (see § 5.4) +> **Execution Mode**: [single-shot | loop | hybrid-loop], see § 5.2 +> **Parallel Execution**: [YES - N waves | NO - sequential] +> **Critical Path**: [Task X → Task Y → Task Z] + +--- + +## Phase Plan + + + +> **Mode**: loop | hybrid-loop +> **Current phase**: {N} of {total} (this plan body covers Phase {N}) + +### Phase 1: {Title} + +- **Scope**: [1-2 sentences capturing what this phase delivers] +- **End condition**: [Concrete trigger, e.g. "F1-F4 verdicts all APPROVE for tasks 1-N", or "Subsystem X compiles and tests green"] +- **Mandatory commit boundaries within phase**: [List the COMMIT: MANDATORY anchors that must land before phase end] +- **Handover trigger** (closed list. Atlas hands back to Prometheus if ANY): + - Phase scope completed AND F1-F4 verdicts all APPROVE + - A gate failed and the cause is not in the plan's "Must NOT do" list + - A reference cited in the plan resolves to a non-existent file or symbol + - More than {N} tasks have been added beyond this phase's TODO list + - [Custom trigger specific to this plan] +- **Detail level**: full TODOs in this plan body + +### Phase 2: {Title} + +- **Scope**: [1-2 sentences. What the next phase will cover] +- **Why deferred to fresh planning**: [Why we plan this fresh after Phase 1 lands. Usually: needs inspection of Phase 1's actual implementation] +- **Detail level**: TBD by next Prometheus session + +### Phase N: ... + +--- + +## Context + +### Original Request + +[User's initial description verbatim] + +### Interview Summary + +**Key Discussions**: + +- [Point 1]: [User's decision/preference] + +**Research Findings**: + +- [Finding 1]: [Implication] + +--- + +## Work Objectives + +### Core Objective + +[1-2 sentences] + +### Concrete Deliverables + +- [Exact file / endpoint / feature] + +### Definition of Done + +- [ ] [Verifiable condition with command] + +### Must Have + +- [Non-negotiable requirement] + +### Must NOT Have (Guardrails) + +- [Explicit exclusion] +- [AI slop pattern to avoid: excessive comments, over-abstraction, generic names like `data`/`result`/`item`/`temp`] +- [Scope boundary] + +--- + +## Verification Strategy (MANDATORY) + +> **ZERO HUMAN INTERVENTION.** ALL verification is agent-executed. No exceptions. +> Acceptance criteria requiring "user manually tests/confirms" are FORBIDDEN. + +### Test Decision + +- **Infrastructure exists**: [YES/NO] +- **Automated tests**: [TDD / Tests-after / None] +- **Framework**: [bun test / vitest / jest / pytest / none] +- **If TDD**: Each task follows RED (failing test) → GREEN (minimal impl) → REFACTOR + +### QA Policy + +Every task MUST include agent-executed QA scenarios (see TODO template below). +Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`. + +- **Frontend/UI**: Use Playwright (playwright skill). Navigate, interact, assert DOM, screenshot. +- **TUI/CLI**: Use interactive_bash (tmux). Run command, send keystrokes, validate output. +- **API/Backend**: Use Bash (curl). Send requests, assert status + response fields. +- **Library/Module**: Use Bash (bun/node REPL). Import, call functions, compare output. + +--- + +## Execution Strategy + +### Parallel Execution Waves + +> Group independent tasks into parallel waves. Each wave completes before the next begins. +> Target: 5-8 tasks per wave. Fewer than 3 per wave (except final) = under-splitting. +``` + +Wave 1 (Start Immediately, foundation + stubs): +├── Task 1: [...] [quick] +├── Task 2: [...] [quick] +└── Task 7: [...] [quick] + +Wave 2 (After Wave 1, core modules, MAX PARALLEL): +├── Task 8: [...] (depends: 3, 5, 7) [deep] +└── Task 14: [...] (depends: 5, 10) [unspecified-high] + +Wave 3 (After Wave 2, integration + UI): +├── Task 15: [...] (depends: 6, 11, 14) [deep] +└── Task 20: [...] (depends: 16) [visual-engineering] + +Wave FINAL (After ALL tasks, 4 parallel reviews, then user okay): +├── Task F1: Plan compliance audit (oracle) +├── Task F2: Code quality review (unspecified-high) +├── Task F3: Real manual QA (unspecified-high) +└── Task F4: Scope fidelity check (deep) +-> Present results -> Get explicit user okay + +Critical Path: [task chain → ...] → F1-F4 → user okay +Parallel Speedup: ~N% faster than sequential +Max Concurrent: [N] (Wave [k]) + +``` + +### Dependency Matrix (full, show ALL tasks) + +- **1**: none / 8, 14 / 1 +- **8**: 3, 5, 7 / 11, 15 / 2 + +> Format: `{task}: {blocked-by} / {blocks} / {wave}` + +### Agent Dispatch Summary + +- **Wave 1**: T1-T4 → `quick`, T5 → `quick`, T6 → `quick`, T7 → `quick` +- **Wave 2**: T8 → `deep`, T9 → `unspecified-high`, T14 → `unspecified-high` +- **Wave 3**: T15 → `deep`, T16 → `visual-engineering` +- **FINAL**: F1 → `oracle`, F2 → `unspecified-high`, F3 → `unspecified-high`, F4 → `deep` + +--- + +## TODOs + +> Implementation + Test = ONE Task. Never separate. +> EVERY task MUST have: Recommended Agent Profile + Parallelization + QA Scenarios. +> **A task WITHOUT QA Scenarios is INCOMPLETE. No exceptions.** + +- [ ] 1. [Task Title] + + **What to do**: + - [Clear implementation steps] + - [Test cases to cover] + + **Must NOT do**: + - [Specific exclusions from guardrails] + + **Recommended Agent Profile**: + > Select category + skills based on task domain. Justify each choice. + - **Category**: `[visual-engineering | ultrabrain | artistry | quick | unspecified-low | unspecified-high | writing | deep]` + - Reason: [Why this category fits the task domain] + - **Skills**: [`skill-1`, `skill-2`] + - `skill-1`: [Why needed. Domain overlap explanation.] + - **Skills Evaluated but Omitted**: + - `omitted-skill`: [Why domain doesn't overlap] + + **Parallelization**: + - **Can Run In Parallel**: YES | NO + - **Parallel Group**: Wave N (with Tasks X, Y) | Sequential + - **Blocks**: [Tasks that depend on this task completing] + - **Blocked By**: [Tasks this depends on] | None (can start immediately) + + **References** (CRITICAL. Be Exhaustive): + + > The executor has NO context from your interview. References are their ONLY guide. + > Each reference must answer: "What should I look at and WHY?" + + **Pattern References** (existing code to follow): + - `path/to/file.ts:45-78`: [why this pattern applies] + + **API/Type References** (contracts to implement against): + - `path/to/types.ts:TypeName`: [shape this code must satisfy] + + **Test References** (testing patterns to follow): + - `path/to/test.ts:describe("...")`: [test structure to mirror] + + **External References** (libraries and frameworks): + - Official docs: `https://...`: [exact section + what to use] + + **WHY Each Reference Matters**: + - [Don't just list files. Explain what pattern/info to extract.] + - Bad: `src/utils.ts` (vague, which utils? why?) + - Good: `src/utils/validation.ts:sanitizeInput()` uses this sanitization pattern for user input + + **Acceptance Criteria**: + + > **AGENT-EXECUTABLE VERIFICATION ONLY.** No human action permitted. + > Every criterion MUST be verifiable by running a command or using a tool. + + **If TDD (tests enabled):** + - [ ] Test file created: path/to/test.ts + - [ ] [test command] → PASS (N tests, 0 failures) + + **QA Scenarios (MANDATORY. Task is INCOMPLETE without these):** + + > Minimum: 1 happy path + 1 failure/edge case per task. + > Each scenario = exact tool + exact steps + exact assertions + evidence path. + +``` + +``` +Scenario: [Happy path. What SHOULD work] +Tool: [Playwright / interactive_bash / Bash (curl)] +Preconditions: [Exact setup state] +Steps: 1. [Exact action, specific command/selector/endpoint] 2. [Next action, with expected intermediate state] 3. [Assertion, exact expected value] +Expected Result: [Concrete, observable, binary pass/fail] +Failure Indicators: [What specifically would mean this failed] +Evidence: .sisyphus/evidence/task-{N}-{scenario-slug}.{ext} + +Scenario: [Failure/edge case] +Tool: [same format] +Preconditions: [Invalid input / missing dependency / error state] +Steps: 1. [Trigger the error condition] 2. [Assert error is handled correctly] +Expected Result: [Graceful failure with correct error message/code] +Evidence: .sisyphus/evidence/task-{N}-{scenario-slug}-error.{ext} +``` + +> **Specificity requirements.** Specific CSS selectors, concrete test data, exact assertions, wait conditions where relevant, at least ONE failure/error scenario per task. +> +> **Anti-patterns (scenario is INVALID if it looks like this).** +> +> - "Verify it works correctly". HOW? What does "correctly" mean? +> - "Check the API returns data". WHAT data? WHAT fields? +> - "Test the component renders". WHERE? WHAT selector? +> - Any scenario without an evidence path + +**Evidence to Capture.** + +- [ ] Each evidence file named: `task-{N}-{scenario-slug}.{ext}` +- [ ] Screenshots for UI, terminal output for CLI, response bodies for API + +**Commit.** MANDATORY | NO (groups with N) + +- **If MANDATORY.** Atlas MUST commit at this boundary. Weak language = no commit. +- Message: `type(scope): imperative summary` (exact, no placeholders) +- Files: `path/to/file1`, `path/to/file2` (exact, no `git add -A`) +- Pre-commit gate: `exact verification command(s)`. STOP commit on non-zero exit. + +**Gate after this task** (if applicable): + +- **MANDATORY GATE.** [exact condition, e.g. "`pnpm typecheck && pnpm test` must return exit 0"] +- **On gate failure.** HANDOVER to Prometheus immediately (do NOT silently retry, do NOT mask) + +--- + +## Final Verification Wave (MANDATORY: after ALL implementation tasks) + +> 4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user; wait for explicit "okay" before completing. +> +> **Never mark F1-F4 as checked before getting user's okay.** + +- [ ] F1. **Plan Compliance Audit.** `oracle` + Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, curl endpoint, run command). For each "Must NOT Have": search codebase for forbidden patterns. Reject with file:line if found. Check evidence files exist in `.sisyphus/evidence/`. Compare deliverables against plan. + Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT` + +- [ ] F2. **Code Quality Review.** `unspecified-high` + Run `tsc --noEmit` + linter + `bun test` (or this repo's equivalent: `pnpm typecheck && pnpm test && pnpm validate:all`). Review all changed files for: `as any`/`@ts-ignore`, empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic names (data/result/item/temp). + Output: `Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | VERDICT` + +- [ ] F3. **Real Manual QA.** `unspecified-high` (+ `playwright` skill if UI) + Start from clean state. Execute EVERY QA scenario from EVERY task. Follow exact steps, capture evidence. Test cross-task integration (features working together, not isolation). Edge cases: empty state, invalid input, rapid actions. Save to `.sisyphus/evidence/final-qa/`. + Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT` + +- [ ] F4. **Scope Fidelity Check.** `deep` + For each task: read "What to do", read actual diff (git log/diff). Verify 1:1. Everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance. Detect cross-task contamination: Task N touching Task M's files. Flag unaccounted changes. + Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT` + +--- + +## Commit Strategy + +- **1.** `type(scope): desc`, file.ts, `pnpm typecheck && pnpm test` (or this repo's pre-commit chain) + +--- + +## Success Criteria + +### Verification Commands + +```bash +command # Expected: output +``` + +### Final Checklist + +- [ ] All "Must Have" present +- [ ] All "Must NOT Have" absent +- [ ] All tests pass +- [ ] All evidence files exist in `.sisyphus/evidence/` +- [ ] F1-F4 verdicts all APPROVE +- [ ] User explicit "okay" recorded + +--- + +## 8. What you do NOT do here + +- **No Metis / Oracle / Momus dispatch.** Those are OmO-internal agents Claude Code cannot dispatch. If the user explicitly wants Momus high-accuracy review, tell them they need to open OpenCode and run the plan through Prometheus directly (this skill is the lightweight Claude-side path). +- **No `/start-work` invocation.** That command lives in OpenCode. Tell the user to run it themselves. +- **No execution.** Even if the user begs. Generate the plan, hand off, done. +- **No drafts.** Single-shot authoring. The final plan IS the artifact. +- **No edits to anything outside `.sisyphus/plans/{slug}.md`** (and conditionally `.sisyphus/boulder.json` + matched evidence/notepads on explicit cleanup). + +## 9. Provenance + +Source of truth for the Prometheus Claude-default plan format: + +- `~/dev-projects/pi-setup-hq/reference-repos/oh-my-openagent/src/agents/prometheus/plan-template.ts`: markdown template body +- `~/dev-projects/pi-setup-hq/reference-repos/oh-my-openagent/src/agents/prometheus/identity-constraints.ts`: single-plan mandate, max-parallelism, markdown-only, incremental write protocol +- `~/dev-projects/pi-setup-hq/reference-repos/oh-my-openagent/src/agents/prometheus/plan-generation.ts`: workflow phases (Metis / Oracle / Momus, out of scope for Claude Code use) + +If Prometheus changes its template upstream, refresh this skill against those files. The Claude-default variant is selected by `getPrometheusPrompt()` when the agent's model is not GPT and not Gemini. diff --git a/.changeset/config.json b/.changeset/config.json index 6d0f59e..22be71b 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -16,5 +16,5 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["@libar-dev/architect-spec", "architect-self-host-example"] + "ignore": ["@libar-dev/architect-spec"] } diff --git a/.changeset/retire-pattern-graph-facade.md b/.changeset/retire-pattern-graph-facade.md new file mode 100644 index 0000000..ac4dfbd --- /dev/null +++ b/.changeset/retire-pattern-graph-facade.md @@ -0,0 +1,10 @@ +--- +"@libar-dev/architect": major +"@libar-dev/architect-core": major +"@libar-dev/architect-projection": major +"@libar-dev/architect-guard": major +"@libar-dev/architect-cli": major +"@libar-dev/architect-mcp": major +--- + +Publish the frozen core Graph contract, retire the PatternGraphAPI facade and query envelopes, migrate CLI and MCP consumers to direct graph reads and pure kernels, and reject malformed TypeScript architecture annotations. diff --git a/.claude/hooks/architect-api-first.sh b/.claude/hooks/architect-api-first.sh new file mode 100644 index 0000000..d2581fb --- /dev/null +++ b/.claude/hooks/architect-api-first.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +set -u + +# SessionStart context injection. +# +# Injects durable orientation only: +# 1. the source-first / event-sourced mental model, and +# 2. loading the self-contained `architect-base` skill, with the graph handle +# (`architect-graph-handle`) and `architect-sessions` as on-demand loads. +# The gen-1 verb CLI is retired (ADR-014); the graph handle is THE agent read +# surface. Prior hook versions (API-first contract, live overview exec) live in +# git history. + +MENTAL_MODEL_BLOCK="$(cat <<'EOF' +[Architect mental model — source-first, event-sourced, projected] +Source of truth = annotated production TS (`@architect-*` JSDoc) + executable Gherkin (`tests/features/**`); git-committed annotated code is the immutable event store. +The PatternGraph, generated docs (`docs-live/`), and Studio UI are all PROJECTIONS off that one graph — never hand-author or hand-edit a projection to reconcile it with source. +`docs-live/` regenerates via `pnpm docs:all` and is git-tracked, so `pnpm docs:all && git diff --exit-code docs-live` is a determinism gate; a non-empty diff means a projection drifted from source. +Working state under `architect/` (specs · stubs · decisions) is scaffold, not source: a design spec transfers its invariants to executable Gherkin + its rationale to JSDoc, then is deleted. `architect/decisions/` ADRs are the permanent exception. +EOF +)" + +SKILL_BLOCK="$(cat <<'EOF' +[Load skills] +Before proceeding, load `.agents/skills/architect-base` NOW (canonical repo-root path; self-contained — the vocabulary every other surface assumes; pulls in no other skill). +Load these ON DEMAND, never pre-loaded at startup: +- `.agents/skills/architect-graph-handle` — THE read surface (ADR-014; the verb CLI is retired). Whenever you need graph state (a pattern's status/deps/rules, a file's owner + neighborhood, a symbol's usage, blast radius, what a pattern guarantees / which specs re-verify), or you'd otherwise grep across files to learn the architecture: script cuts over the live graph via `pnpm architect:q`. +- `.agents/skills/architect-sessions` — for spec-driven work (capture/design/implement/review/handoff). +`.codex/skills/` symlinks to `.agents/skills/`; `.claude/skills/` and `.opencode/skills/` mirror it. Use `.agents/skills/` as the canonical path set. +EOF +)" + +ADDITIONAL_CONTEXT="${MENTAL_MODEL_BLOCK}"$'\n\n'"${SKILL_BLOCK}" + +ADDITIONAL_CONTEXT_JSON="$( + ADDITIONAL_CONTEXT="$ADDITIONAL_CONTEXT" python3 - <<'PY' +import json +import os +import sys + +sys.stdout.write(json.dumps(os.environ.get("ADDITIONAL_CONTEXT", ""))) +PY +)" + +printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":%s}}' "$ADDITIONAL_CONTEXT_JSON" diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..4f4b8e4 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/architect-api-first.sh", + "timeout": 30 + } + ] + } + ] + } +} diff --git a/.claude/skills/_shared b/.claude/skills/_shared deleted file mode 120000 index 07fc659..0000000 --- a/.claude/skills/_shared +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/_shared \ No newline at end of file diff --git a/.claude/skills/architect-base b/.claude/skills/architect-base new file mode 120000 index 0000000..b57d263 --- /dev/null +++ b/.claude/skills/architect-base @@ -0,0 +1 @@ +../../.agents/skills/architect-base \ No newline at end of file diff --git a/.claude/skills/architect-data-api b/.claude/skills/architect-data-api deleted file mode 120000 index cec8402..0000000 --- a/.claude/skills/architect-data-api +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/architect-data-api \ No newline at end of file diff --git a/.claude/skills/architect-design-session b/.claude/skills/architect-design-session deleted file mode 120000 index 55ea714..0000000 --- a/.claude/skills/architect-design-session +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/architect-design-session \ No newline at end of file diff --git a/.claude/skills/architect-graph-handle b/.claude/skills/architect-graph-handle new file mode 120000 index 0000000..fa837d4 --- /dev/null +++ b/.claude/skills/architect-graph-handle @@ -0,0 +1 @@ +../../.agents/skills/architect-graph-handle \ No newline at end of file diff --git a/.claude/skills/architect-implement-spec b/.claude/skills/architect-implement-spec deleted file mode 120000 index 80752e8..0000000 --- a/.claude/skills/architect-implement-spec +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/architect-implement-spec \ No newline at end of file diff --git a/.claude/skills/architect-plan-session b/.claude/skills/architect-plan-session deleted file mode 120000 index 3555d65..0000000 --- a/.claude/skills/architect-plan-session +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/architect-plan-session \ No newline at end of file diff --git a/.claude/skills/architect-review-implementation b/.claude/skills/architect-review-implementation deleted file mode 120000 index 19bd637..0000000 --- a/.claude/skills/architect-review-implementation +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/architect-review-implementation \ No newline at end of file diff --git a/.claude/skills/architect-review-spec b/.claude/skills/architect-review-spec deleted file mode 120000 index e367204..0000000 --- a/.claude/skills/architect-review-spec +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/architect-review-spec \ No newline at end of file diff --git a/.claude/skills/architect-session-router b/.claude/skills/architect-session-router deleted file mode 120000 index c158cb3..0000000 --- a/.claude/skills/architect-session-router +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/architect-session-router \ No newline at end of file diff --git a/.claude/skills/architect-sessions b/.claude/skills/architect-sessions new file mode 120000 index 0000000..8990dde --- /dev/null +++ b/.claude/skills/architect-sessions @@ -0,0 +1 @@ +../../.agents/skills/architect-sessions \ No newline at end of file diff --git a/.claude/skills/architect-verify-handoff b/.claude/skills/architect-verify-handoff deleted file mode 120000 index c5c66ba..0000000 --- a/.claude/skills/architect-verify-handoff +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/architect-verify-handoff \ No newline at end of file diff --git a/.claude/skills/omo-plan-author b/.claude/skills/omo-plan-author new file mode 120000 index 0000000..9638c84 --- /dev/null +++ b/.claude/skills/omo-plan-author @@ -0,0 +1 @@ +../../.agents/skills/omo-plan-author \ No newline at end of file diff --git a/.claudeignore b/.claudeignore new file mode 120000 index 0000000..11838b3 --- /dev/null +++ b/.claudeignore @@ -0,0 +1 @@ +.agentignore \ No newline at end of file diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 0000000..741b01a --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1 @@ +sandbox_mode = "workspace-write" diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 0000000..d9d94d3 --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bash '/Users/darkomijic/dev-projects/architect/.codex/hooks/architect-api-first.sh'", + "timeout": 30 + } + ] + } + ] + } +} diff --git a/.codex/hooks/architect-api-first.sh b/.codex/hooks/architect-api-first.sh new file mode 120000 index 0000000..6f662ea --- /dev/null +++ b/.codex/hooks/architect-api-first.sh @@ -0,0 +1 @@ +../../.claude/hooks/architect-api-first.sh \ No newline at end of file diff --git a/.codex/skills b/.codex/skills new file mode 120000 index 0000000..2b7a412 --- /dev/null +++ b/.codex/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.full-review/00-scope.md b/.full-review/00-scope.md deleted file mode 100644 index 8224048..0000000 --- a/.full-review/00-scope.md +++ /dev/null @@ -1,78 +0,0 @@ -# Review Scope - -## Target - -`packages/architect-projection/` — the fragment-based projection pipeline that emits Zod-validated Named Domain Fragments and renders them (compact-text, JSON, markdown, UI) for `architect-generate` and downstream consumers. - -## Why this review now - -The user is preparing a **doc-generation consolidation campaign** (drafted in `.pr-coordination/DEEP-DIVE.md`, `INVENTORY.md`, `PROPOSED-DESIGN.md`). Wave 4 of that campaign will: - -1. Restore the dropped reference-codec capability (13 codec files + 4 generator wrappers lost in W1 lift). -2. Introduce a new `DocDefinition.build(graph)` TS-as-config API to replace the dead `referenceDocConfigs:` field in `architect.config.ts`. -3. Add a **ContentFragment** layer (input-side progressive disclosure) so the same conceptual unit can render at multiple depths in multiple docs. -4. Extend the documentation-composition area to support multi-target output (`docs-live/` + `_claude-md/` + JSON) and a generated-insert directive for hand-authored files. - -This review is therefore scoped to surface issues that would **block, complicate, or invalidate** that incoming work. Generic best-practice nits in unrelated areas are out of scope. - -## Files - -Full package: `packages/architect-projection/src/**` (135 TS files), with extra weight on the areas the campaign touches: - -**Hot zones (campaign will modify these):** -- `src/projections/documentation-composition/` — 14 files, 1,692 LOC; especially `documentation-bundle.internal.ts` (the hardcoded 12-entry dispatch table at line 64 that is the current ceiling on `architect-generate` output), `documentation-types.ts` (517 LOC type definitions), `progressive-disclosure.ts`, `disclosure-spec.ts`. -- `src/blocks/schema.ts` — the 9-block-type catalog + `RenderableDocument` envelope; ContentFragment proposal layers on top of this. -- `src/fragments/**` — 43 projection functions across pattern-relations, governance, operational-insights, delivery-reporting, execution-context, documentation-composition; only 8 reachable through `docs:all` today. -- `src/renderers/**` — `render-markdown.ts`, `render-compact-text.ts`, `render-json.ts`, `render-ui.ts`, plus `markdown-paths.ts` and `_shared/dispatch.ts`; progressive-disclosure output mechanism lives here. - -**Architectural perimeters:** -- `src/index.ts` + sub-entry barrels (`./blocks`, `./fragments`, `./projections`, `./renderers`) — public API surface (`exports` map in `package.json`). -- `src/context/projection-context.ts` — the context type passed to every projection. -- `src/_internal/` — slug + format-utils; trust boundary helpers. - -## Flags - -- Security Focus: **no** (advisory — projection pipeline reads PatternGraph data and renders to text; the only relevant security surface is the markdown trust boundary in `render-markdown.ts`) -- Performance Critical: **yes** (a CI perf gate already enforces `baseline × 1.5` against a 36-pattern / 108-rule fixture; the doc-gen campaign will fan out projection calls 5–10×, so performance headroom is a first-class concern) -- Strict Mode: **no** (review-only; no auto-blocking on Critical findings) -- Framework: TypeScript 5.8 (strict + `verbatimModuleSyntax` + `noUncheckedIndexedAccess` + `noPropertyAccessFromIndexSignature` + `exactOptionalPropertyTypes`) + Zod 4.1 + `@amiceli/vitest-cucumber` + ESM-only (`"type": "module"`, `sideEffects: false`). - -## Repo doctrine reviewers must respect - -These are project-defining constraints — do **not** flag deviations from them as issues, and do flag any code that violates them: - -1. **No-BC.** No `eslint-disable*`, no `@ts-ignore`/`@ts-expect-error`, no `@deprecated` shims, no backward-compatibility aliases. The repo is pre-1.0; shims become permanent cost. -2. **Zod-first boundaries.** Cross-package contracts and CLI/MCP boundaries use `z.strictObject(...)` (never `z.object()`). Types flow from schemas via `z.infer`. Parse once at the boundary, then cheap shape-check internally. -3. **ESM-only, `sideEffects: false`.** Every type-only import uses `import type`. -4. **No circular imports** across packages or within a package's `src/`. -5. **`docs-live/` is regenerated, not committed.** Don't flag missing generated artifacts. -6. **Architect State IS code.** Annotations live with implementation. Generated docs are projections. -7. **Two parsers, don't conflate:** `@cucumber/gherkin` parses `architect/specs/` at doc-gen time; `@amiceli/vitest-cucumber` parses `tests/features/` at test time. - -## Review Phases - -1. Code Quality & Architecture (parallel: code-reviewer + architect-review) -2. Security & Performance (parallel: security-auditor + general-purpose performance analysis) -3. Testing & Documentation (parallel: general-purpose test analysis + general-purpose docs review) -4. Best Practices & Standards (parallel: general-purpose framework review + general-purpose CI/CD review) -5. Consolidated final report - -## Specialized review priorities (per user request) - -Given the campaign context, reviewers should give extra weight to: - -- **Extensibility of `documentation-bundle.internal.ts`** — the 12-entry dispatch table is the bottleneck the campaign explicitly targets. How clean is the replacement path? -- **`DocumentationTypes` (517 LOC)** — will the proposed `DocDefinition` types layer cleanly, or does the current shape force the new API into awkward shapes? -- **Trust boundary in markdown rendering** — `escapeText`, `link-out` schema, `parseMarkdownToBlocks` consumption. The ContentFragment proposal will route MORE markdown through these paths. -- **Progressive-disclosure substrate (output side)** — `RenderMarkdownOptions.disclosureLevel`, `disclosureSpec`, `splitOversizedDocument`. The campaign adds an INPUT side; the OUTPUT side must remain solid. -- **Public API surface (the `exports` map)** — what's currently exported from `./projections`, `./fragments`, `./blocks`, `./renderers` and how disruptive will adding `DocDefinition` / `ContentFragment` be? -- **Perf regression gate** — known to exist with a `baseline × 1.5` ceiling. Confirm it covers the documentation-composition pipeline (not just isolated fragment projection). -- **Test-feature coverage of the documentation-composition area** — vitest-cucumber features that pin the current contract; high-value because the campaign must preserve them. - -## What's out of scope - -- Generic TypeScript/Zod nits in code untouched by the campaign. -- `@libar-dev/architect-core`, `architect-guard`, `architect-cli`, `architect-mcp` — only flag if a finding inside `architect-projection` is symptomatic of a deeper cross-package issue. -- Suggesting to add `// removed for X` comments, parallel implementations, or feature-flag shims (no-BC doctrine). -- The W9 skills consolidation (separate campaign). -- The W7 publish/cutover. diff --git a/.full-review/01-quality-architecture.md b/.full-review/01-quality-architecture.md deleted file mode 100644 index 7e66457..0000000 --- a/.full-review/01-quality-architecture.md +++ /dev/null @@ -1,136 +0,0 @@ -# Phase 1: Code Quality & Architecture Review - -Reviewed: `packages/architect-projection/` against the doc-generation consolidation campaign drafted in `.pr-coordination/`. - -Raw reports: `01a-code-quality-raw.md`, `01b-architecture-raw.md`. - -The two reviews were run independently and **converged on the same structural finding**: a closed dispatch table + entangled documentation types + scattered disclosure ownership form a tightly-coupled subsystem in `src/projections/documentation-composition/` that is precisely what the campaign needs to replace. The convergence is high-signal — not parallel observations of different problems, but two views of the same problem. - -## Headline - -**The campaign cannot land as a layer on top of the current `documentation-composition/` subsystem. It must replace the registry-driven dispatch core. Pre-split that core before W-DOCS-1, do not retrofit.** - -The good news: the layers *around* that core (BlockSchema substrate, ProjectionBundle routing, parseAndProject trust boundary, OUTPUT-side disclosure with `splitOversizedDocument`) are well-positioned to host `DocDefinition` and `ContentFragment` as new peers. - -## Critical issues (campaign blockers) - -### C1 — Closed dispatch core is the campaign's substrate, not an obstacle to route around -**File:** `src/projections/documentation-composition/documentation-bundle.internal.ts:64` -**Convergence:** code-quality C1 + architecture F1. - -`DOCUMENTATION_PROJECTION_FACTORIES` is statically typed against `SupportedDocumentationType`, a union derived via `as const` from the registry literal in `documentation-types.ts`. Adding a doc requires editing the union, the registry, and the dispatch table in lockstep. The campaign's `DocDefinition.build(graph)` API IS the replacement for this core, not a layer on top of it. - -**Action:** delete the registry-driven dispatch when `DocDefinition` lands. Do not parallel-implement (no-BC). Do not extend the union — every new entry deepens the carve-out. - -### C2 — `documentation-types.ts` conflates identity, output routing, disclosure policy, and CLI surface -**File:** `src/projections/documentation-composition/documentation-types.ts:35-47, 140-340` (517 LOC total) -**Convergence:** code-quality C2 + architecture F2. - -One Zod object holds: doc identity, where it writes on disk, disclosure policy, CLI exposure flags, and the now-dead `'dropped'` lifecycle markers. The campaign's "three orthogonal layers" reframe (Extractors / Routing / Composition / Output-routing) cannot land cleanly until each concern owns its own type. - -**Action:** decompose along the campaign's four layer lines. Do this BEFORE introducing `DocDefinition` so the new API consumes orthogonal types from day one. - -## High-priority findings (cause major rework if not addressed pre-campaign) - -### H1 — Types derived from literal, not from schema (Zod-first violation) -**File:** `documentation-types.ts:140-340` (code-quality H1) - -Registry types are produced from the literal via `typeof REGISTRY[number]` instead of via `z.infer`. Inverts the project's Zod-first doctrine. When `DocDefinition` arrives via config, schema/type drift is guaranteed. - -**Action:** schema is canonical; literal is data validated by it. - -### H2 — `status: 'dropped'` registry entries are a no-BC shim -**File:** `documentation-types.ts:49-59, 294-339` (code-quality H2 + architecture F3) - -`'dropped'` entries exist to keep the registry literal type-compatible with vanished generators. Violates the no-BC doctrine directly, and will collide name-for-name with the campaign's restored `reference` doc. - -**Action:** delete the `'dropped'` entries and any code that filters on them. - -### H3 — Renderers reach into `documentation-composition/` for metadata (ADR-005/009 drift) -**File:** `src/renderers/render-markdown.ts:50-52`, `src/renderers/markdown-paths.ts:3-4, 26-49` (code-quality H3 + architecture F4) - -`render-markdown.ts` calls `getDocumentationTypeMetadata()` and consumes `disclosureMatrix` at render time. `markdown-paths.ts` parses `routing.rootRouteId.split(':')[0]` to derive doc-type-aware behavior. Renderers are doc-type-aware — direct violation of ADR-005 (codec/renderer separation) and ADR-009 (projection trust boundary). - -The ContentFragment proposal will route MORE markdown through these paths. The leak gets worse, not better. - -**Action:** push disclosure onto `bundle.routing.disclosureSpec` at projection time; renderer trusts the bundle. No renderer-side lookups into the registry. - -### H4 — Hardcoded doc-type strings leak across modules -**File:** `src/renderers/markdown-paths.ts:26-49`, `src/fragments/delivery-reporting/index.ts` (code-quality H4) - -String literals `'requirements-executable'`, `'milestones'`, etc. appear at routing decision points outside the registry. Symptom of routing-as-data being incompletely realized. - -**Action:** routing decisions belong on the registry entry. Renderers consume `bundle.routing`, period. - -### H5 — `render-markdown.ts` is 2152 lines, 80 top-level functions, ~10 fragment-specific normalizers -**File:** `src/renderers/render-markdown.ts` (code-quality H5) - -ContentFragment will add 6–10 more normalizers. The normalizer table needs to move into fragment-owned modules with a `toMarkdownBlocks(fragment)` contract; render-markdown.ts becomes a thin dispatcher. - -**Action:** move per-fragment markdown normalizers into the fragment modules themselves. Renderer dispatches on `Fragment.kind`, doesn't know fragment internals. - -### H6 — `MarkdownDocument` envelope is unexported and unschema'd -**File:** `render-markdown.ts` (code-quality H6) - -The intermediate envelope is private + structural. The campaign's `composeDoc(title, sections)` returning `RenderableDocument` will compete with it. - -**Action:** schema-fy and export, or replace with `RenderableDocument` when that type lands. Don't ship both. - -### H7 — Disclosure vocabulary lives inside `documentation-composition/` but is package-wide -**File:** `src/renderers/types.ts` imports `DisclosureSpec` + `LogicalRouteId` from `projections/documentation-composition/` (architecture F5 + F17 + F18) - -`DisclosureSpec`, `LogicalRouteId`, and the disclosure enum are conceptually package-level primitives but live inside one projection domain. Layering inversion that the campaign's input-side disclosure axis will exacerbate. - -**Action:** promote to `src/disclosure/` + `src/routing/` as peer concerns before adding the input-side axis. - -### H8 — 43 projections have three inconsistent signature flavors -**File:** various `parseAndProject*` wrappers (architecture F8) - -`DocDefinition.build(graph)` runners cannot call the projections uniformly without an adapter layer. Adapter layers proliferate. - -**Action:** normalize to one signature shape before W-DOCS-2. Variance is technical debt that compounds when the campaign adds 6+ new extractors. - -## Medium-priority findings (should fix before campaign starts) - -### M1 — `Fragment` is a closed 43-variant discriminated union keyed on `kind` -**Reference:** architecture F9 + F19 - -`ContentFragment` and `RenderableDocument` in PROPOSED-DESIGN don't have a `kind` discriminator and shouldn't — they're composition primitives, not domain fragments. Renderer dispatch needs a top-level distinction. - -**Action:** define `RenderInput = ProjectionBundle | RenderableDocument` and have renderers dispatch on input shape first, then on `kind` if it's a `Fragment`. - -### M2 — `_internal/` boundary is naming convention, not enforced -**Reference:** architecture F6 - -`*.internal.ts` files are referenced externally in places. Campaign will introduce a new consumer surface (`DocDefinition` callers) — the boundary needs teeth. - -**Action:** lint rule or barrel discipline to make `_internal/` actually sealed. - -## Welcomes — what to NOT touch - -The architecture review surfaced five places where the current design is well-positioned for the campaign. **Preserve these as-is:** - -1. **`BlockSchema` discriminated union** (`src/blocks/schema.ts`) — the 9-block-type substrate. Hosts ContentFragment-emitted blocks without redesign. -2. **`parseAndProject` trust-boundary helper** — clean ADR-009 implementation; reuse for new extractors. -3. **`ProjectionBundle` / `BundleRouting` / `LogicalRouteId` fan-out machinery** — already does multi-target routing; campaign's `DocTarget[]` layers on top. -4. **The `*.ts` ⟷ `*.internal.ts` paired-module pattern** — uniform convention, just needs enforcement (M2). -5. **OUTPUT-side disclosure already wired through `renderMarkdown`** via `splitOversizedDocument`. The campaign's INPUT-side axis composes orthogonally; don't refactor the output side. - -## Fights — what to address before the campaign starts - -Ranked by campaign impact: - -1. **Closed registry-and-dispatch core** (C1, C2, H2). Pre-split, don't retrofit. -2. **Disclosure ownership scattered across renderer + registry + bundle routing** (H3, H7). Consolidate before adding input-side axis. -3. **`documentation-types.ts` mega-module** (C2, H1). The campaign lands here — decompose first. -4. **Renderer doc-type awareness** (H3, H4, H5). Renderers must trust the bundle, not look things up. -5. **Projection signature variance** (H8). Normalize before `DocDefinition.build()` arrives. - -## Critical issues for Phase 2 context - -The Phase 2 reviewers should give weight to: - -- **Trust boundary erosion in markdown rendering** — `render-markdown.ts` (2152 LOC) has fragment-specific normalizers and consumes registry metadata at render time. Security audit should verify no user-controlled strings reach `escapeText`-bypass paths, and that the `link-out` schema is enforced consistently across all 10+ normalizers. -- **Performance risk in the dispatch core** — every doc-gen run walks the 12-entry table. The campaign will multiply this to 40+ docs. Performance review should confirm the perf gate (`baseline × 1.5`) covers `documentation-composition` end-to-end, not just isolated fragment projection. -- **`render-markdown.ts` size** is a security-review concern (large attack surface for markdown-injection bugs) and a perf concern (cold start + cache pressure). -- **`status: 'dropped'` entries** may be referenced from CI / `docs:all` scripts — verify deletion doesn't silently break the build chain. diff --git a/.full-review/01a-code-quality-raw.md b/.full-review/01a-code-quality-raw.md deleted file mode 100644 index 4e6e483..0000000 --- a/.full-review/01a-code-quality-raw.md +++ /dev/null @@ -1,240 +0,0 @@ -# Code Quality Review — `packages/architect-projection/` - -**Scope:** Code-quality issues that block, complicate, or invalidate the doc-generation consolidation campaign (DocDefinition API, ContentFragment layer, multi-target output, new extractors). Findings strictly prioritized for that campaign — generic nits omitted. - -**Total findings:** 22. **Critical:** 2. **High:** 6. **Medium:** 9. **Low:** 5. - ---- - -## Critical - -### C1. `DOCUMENTATION_PROJECTION_FACTORIES` is statically typed against a closed enum derived from the registry -**File:** `packages/architect-projection/src/projections/documentation-composition/documentation-bundle.internal.ts:64-79` - -The dispatch table is `satisfies Record`. `SupportedDocumentationType` is derived from `Extract<…, { readonly status: 'supported' }>['key']` over `DOCUMENTATION_TYPE_REGISTRY` (`documentation-types.ts:347-357`), which is `as const`. That means *every new doc type is a TypeScript compile error in three places* (registry + factories table + key union flow-through), and the entire registry has to be loaded just to add one factory. The downstream `getSupportedDocumentationTypeMetadata` is also strongly typed against this exhaustive union. - -**Why it matters for the campaign:** The `DocDefinition.build(graph)` API is explicitly designed to let consumers (including per-package `*.doc.ts` files) register new docs without editing a central registry. Today's design forces every new doc to be inserted into a single closed union before it compiles. The campaign cannot land cleanly without either (a) opening this union to `string`-keyed registration at the boundary, or (b) replacing the registry with a `DocDefinition[]` discovered at config time. Plan for (b). - -**Fix recommendation:** Replace the closed-enum dispatch with a `DocDefinition` interface keyed by string id, validated by Zod at the config boundary. The factory becomes `definition.build(context, options)` and the registry is `Map` populated from `architect.config.ts`. The compile-time exhaustiveness check is replaced by a runtime test that every documented type has a registered definition. Worked sketch: - -```ts -export interface DocDefinition { - readonly id: string; - readonly displayTitle: string; - readonly disclosureMatrix: DocumentationDisclosureMatrix; - build(ctx: ProjectionContext, opts: DocDefinitionBuildOptions): ProjectionBundle; -} -// resolve at boundary, no closed union -function assertSupportedDocumentType(id: string, registry: ReadonlyMap) { ... } -``` - -### C2. Disclosure matrix and registry shape conflate four orthogonal concerns -**File:** `packages/architect-projection/src/projections/documentation-composition/documentation-types.ts:35-47` plus `:71-138` - -`SupportedDocumentationTypeRegistryEntry` collapses *(a) identity* (`key`, `displayTitle`, `description`), *(b) output routing* (`rootRouteId`, `markdownRootTarget`, `childDirectory`), *(c) disclosure policy* (`defaultDisclosureLevel`, `disclosureMatrix`), and *(d) CLI surface* (`generatorName`, `generatorAliases`) into one Zod object. The 12 `xxxDisclosureMatrix` constants and 12 registry entries are kept in sync purely by hand — there is no relationship between an entry's `key` and its matrix-constant name beyond convention. - -**Why it matters for the campaign:** The campaign explicitly separates *Extractors / Routing / Composition / Output-routing* (DEEP-DIVE §"Three orthogonal layers"). The current shape forces every new `DocDefinition` to fill all four buckets in one place, and forces `documentation-types.ts` to expand instead of contracting. It also makes multi-target output (`docs-live/` + `_claude-md/` + JSON) hard to express — `markdownRootTarget` is a single string today. - -**Fix recommendation:** Split the registry entry into three composed Zod schemas — `DocIdentity`, `DocOutputTargets` (`Record` so multi-target becomes natural), and `DocDisclosurePolicy`. Make the `disclosureMatrix` an explicit field on the `DocDefinition` so a definition file owns its own policy rather than the central registry. This also unblocks ContentFragment input-side disclosure (which today has nowhere to live). - ---- - -## High - -### H1. Hand-rolled `SUPPORTED_DOCUMENTATION_TYPES`/`DOCUMENTATION_TYPE_REGISTRY` derivations are inverted Zod-first -**File:** `packages/architect-projection/src/projections/documentation-composition/documentation-types.ts:140-340` - -The registry is authored as a hand-written `const` array, then run through `DocumentationTypeRegistryEntrySchema.parse(entry)` *at module top level* (`:342-344`). The exported types are derived from the literal via `(typeof DOCUMENTATION_TYPE_REGISTRY)[number]` rather than from the schema — the schema is only used as a runtime assertion, not as the canonical type source. This is the inverse of the repo's Zod-first doctrine ("types flow from schemas via `z.infer`"). - -**Why it matters for the campaign:** When DocDefinitions arrive from user config (`architect.config.ts`), they must round-trip through Zod at the boundary. If the schema isn't the type source today, the campaign will end up with two parallel definitions of "what is a doc registry entry" — the literal type and the schema — and they will drift. - -**Fix recommendation:** Make `SupportedDocumentationTypeRegistryEntry` (already `z.infer`'d at `:67`) the canonical type, type the array as `readonly SupportedDocumentationTypeRegistryEntry[]`, and lose the literal-derived `InternalDocumentationTypeMetadata`. The compile-time exhaustiveness check is replaced by a Zod refinement that every key is unique. - -### H2. Dropped-type registry exists only to throw — pure dead weight -**File:** `packages/architect-projection/src/projections/documentation-composition/documentation-types.ts:294-339`, `documentation-bundle.internal.ts:82-87` - -The four `status: 'dropped'` entries (`reference`, `product-areas`, `design-review`, `product-requirements`) exist *only* so that `assertSupportedDocumentType` can throw a slightly more helpful error. The "dropped" branch of `DocumentationTypeRegistryEntrySchema` (`:49-59`) carries `markdownRootTarget: z.null()` and `generatorName: z.null()` — Zod gymnastics to model "this is not a thing." This is a no-BC shim (`status: 'dropped'` is a compatibility nudge for callers that haven't migrated). The repo doctrine is explicit: no-BC, no `@deprecated` shims. - -**Why it matters for the campaign:** The campaign restores the `reference` capability under a different shape (codec catalog via `DocDefinition`). Keeping a `status: 'dropped'` entry for `reference` will be actively confusing once the new `reference` doc exists. The whole dropped-type concept must go before the campaign starts. - -**Fix recommendation:** Delete `DroppedDocumentationTypeRegistryEntrySchema`, `DROPPED_DOCUMENTATION_TYPE_REGISTRY`, `isDroppedDocumentationType`, and the dropped-branch error in `assertSupportedDocumentType`. Replace with a single "unknown type" error path — the registry only contains live entries. - -### H3. Renderers reach into projection-internal modules for type and metadata access -**File:** `packages/architect-projection/src/renderers/render-markdown.ts:50-52`, `markdown-paths.ts:3-4`, `renderers/types.ts:2` - -`renderers/` imports `getDocumentationTypeMetadata` from `projections/documentation-composition/documentation-types.js` and `DisclosureSpec` from `projections/documentation-composition/disclosure-spec.js`. The renderer layer is supposed to be document-agnostic — it consumes `Fragment`/`ProjectionBundle` plus a `routeProfile`. Today the markdown renderer special-cases bundle routing by parsing `routing.rootRouteId.split(':')[0]` and looking up the document type's disclosure matrix (`render-markdown.ts:400-420`). That's a layering inversion: routing/disclosure policy lives in the projection layer but is *read* by the renderer. - -**Why it matters for the campaign:** When `DocDefinition` becomes the substrate, disclosure policy and routing move to the definition object. Renderers will need a clean injection point, not a deep import into the documentation-composition module. The current coupling is also a circular-import risk if/when documentation-composition starts depending on renderer-visible types. - -**Fix recommendation:** Have `projectDocumentationBundle` (or `DocDefinition.build`) attach the resolved `DisclosureSpec` directly to the `ProjectionBundle.routing` metadata, so the renderer no longer parses `rootRouteId` strings or looks up metadata. The renderer becomes truly document-agnostic and the projection→renderer dependency edge becomes one-way. - -### H4. Hardcoded doc-type strings leak across files instead of staying in the registry -**File:** `packages/architect-projection/src/renderers/markdown-paths.ts:26-49`, `delivery-reporting/index.ts:121,402` - -`markdown-paths.ts` carries the special case `if (route.documentType === 'requirements-executable') { ...INDEX.md }` (`:26-27`) and `if (documentType === 'milestones') return 'COMPLETED-MILESTONES.md'` (`:48-49`). The `delivery-reporting/index.ts` projection threads a `view === 'milestones'` literal that doesn't appear in the registry at all (`:402`). These are routing decisions that should live as data on the registry entry (e.g., `pathStrategy: 'index-per-entity'`), but instead leak across three files. - -**Why it matters for the campaign:** Every new generated doc the campaign adds will multiply this leakage. The `DocDefinition` API can't replace the registry cleanly if routing rules are scattered through the renderer's path-resolution code. - -**Fix recommendation:** Push the `requirements-executable` index-per-entity behaviour onto the registry entry as a `childPathStrategy: 'index-per-entity'` (or move it into a `DocDefinition.resolvePath()` method). Delete the `'milestones'` upper-case fallback — that code path is for an unregistered doc-type, which should be impossible once `DocDefinition` lands. - -### H5. `render-markdown.ts` is 2152 lines and 80 top-level functions — single-responsibility violation -**File:** `packages/architect-projection/src/renderers/render-markdown.ts` (entire file) - -Ten `normalize*Fragment` functions (`:521-1042`) plus a 90-line generic-fragment fallback plus a markdown-trust-boundary subsystem (`:1855-2052`) plus path-rewriting (`:1482-1547`) plus oversized-document splitting (`:2054-2102`) plus the entry-point bundle/document machinery all share one module. Cyclomatic complexity is high in `normalizeBusinessRuleSet` (`:549-595`), `normalizeRequirementDigest` (`:820-870`), and `splitOversizedDocument` (`:2054-2102`). - -**Why it matters for the campaign:** ContentFragment adds *six to ten more `normalize*Fragment` functions* (stub format, FSM transitions, block-type catalog, Zod schema field tables, CLI catalog, etc.). Bolting those into a 2152-line file is a maintenance landmine. The cohesive way to add them is via a kind→normalizer registry that ContentFragments populate. - -**Fix recommendation:** Move the per-fragment normalizers (`MARKDOWN_NORMALIZERS` table at `:181-192`) out of the renderer module into `fragments//markdown.ts` siblings, so each fragment owns its own normalizer. Renderer becomes the engine, fragments own their rendering. (This is *exactly* the layering ContentFragment will need.) - -### H6. `RenderableDocument` envelope (`MarkdownDocument`) is unexported and unschema'd -**File:** `packages/architect-projection/src/renderers/render-markdown.ts:62-67` - -The intermediate document shape — what every `normalize*Fragment` returns — is an unexported `interface MarkdownDocument { title; purpose?; detailLevel?; sections: MarkdownRenderableBlock[] }`. The `MarkdownRenderableBlock` union (`:132-138`) mixes user-provided `Block` types with five `Trusted*Block` variants that carry the `TRUSTED_MARKDOWN` symbol. There's no Zod schema. - -**Why it matters for the campaign:** The DEEP-DIVE describes `composeDoc(title, sections)` and ContentFragments returning `SectionBlock[]` — these are the same concept that lives unnamed inside the renderer today. Without an exported `RenderableDocument` schema, the campaign has to invent one and reconcile it with `MarkdownDocument`. Two competing envelope types is a guaranteed source of drift. - -**Fix recommendation:** Export `RenderableDocument` (or `MarkdownDocument` renamed) as a Zod schema in `blocks/schema.ts` (or a new `blocks/document.ts`), and reuse it as both the per-fragment normalizer output and the ContentFragment composition target. Trusted-block variants stay internal to the renderer. - ---- - -## Medium - -### M1. `freezeDocumentationTypeMetadata` recursion is manual and brittle -**File:** `packages/architect-projection/src/projections/documentation-composition/documentation-types.ts:411-456` - -Five separate freeze functions hand-walk the metadata tree (entry → matrix → spec → filter → maturity/status arrays). Adding a new field requires editing every freeze step. The pattern exists because TypeScript's `as const satisfies` doesn't deep-freeze, but the manual freeze chain is fragile. - -**Why it matters for the campaign:** `DocDefinition` will add `outputTargets`, `extractors`, and possibly `contentFragments` fields, each of which would need its own freeze function. - -**Fix recommendation:** Replace with a generic `deepFreeze(value: T): T` helper (one function, recursive), or rely on `Object.freeze` plus `readonly` types and skip runtime freezing entirely (the `as const` already prevents mutation at the type level). - -### M2. `disclosureMatrix()` helper silently injects defaults that the spec doesn't see -**File:** `packages/architect-projection/src/projections/documentation-composition/documentation-types.ts:476-493` - -`disclosureMatrix(matrix)` substitutes `DEFAULT_COMMITTED_FILTER` / `DEFAULT_USEFUL_FILTER` for missing filters and strips advanced-level filters via `omitFilter`. The resulting object is then `as const satisfies readonly DocumentationTypeRegistryEntry[]` (`:340`) — but the values inside the matrix are *different* from what the author wrote. - -**Why it matters for the campaign:** ContentFragments will compose at multiple disclosure levels; if the disclosure level the author writes is silently rewritten, fragment-level disclosure won't match doc-level disclosure. This is a sharp gotcha for the new author surface. - -**Fix recommendation:** Make defaults explicit on the schema (`.default(DEFAULT_COMMITTED_FILTER)`), not in a transformation helper. Or drop the helper entirely and require authors to be explicit. - -### M3. `resolveProjectName` is called twice in `buildProjectConfigSnapshot` -**File:** `packages/architect-projection/src/projections/documentation-composition/project-config.internal.ts:58-60` - -```ts -...(resolveProjectName(context, options.projectName) !== undefined - ? { projectName: resolveProjectName(context, options.projectName) } - : {}), -``` - -Cheap function, but the pattern is wrong and recurs in several `Object.assign`-style spreads across the projection code. - -**Fix:** Hoist to a local `const name = resolveProjectName(...)`, then spread `...(name !== undefined ? { projectName: name } : {})`. - -### M4. `MARKDOWN_NORMALIZERS` table is missing the `ProjectConfigSnapshot`, `PrChangeReview`, `ArchitectureNeighborhood`, `PatternCatalog`, `RoleProfile*`, and several other fragment kinds -**File:** `packages/architect-projection/src/renderers/render-markdown.ts:181-192` - -Only 10 of the ~30 fragment kinds have dedicated markdown normalizers. The rest fall through to `normalizeGenericFragment` (`:1042-1133`), which generates a fragile reflection-based table dump. - -**Why it matters for the campaign:** Multi-target generation will route many more fragments through markdown. Pattern-catalog, taxonomy, decision-record, etc. ship structured data that deserves a typed normalizer — generic-fallback markdown for production docs is technical debt the campaign will trip over. - -**Fix recommendation:** Audit `MARKDOWN_NORMALIZERS` against the `Fragment` union; add explicit normalizers for every fragment kind that ships into a documented doc. (Tracks well alongside H5's "move normalizers into fragment-owned modules" refactor.) - -### M5. `RawProjectDocumentationBundleOptionsSchema` duplicates the typed schema -**File:** `packages/architect-projection/src/projections/documentation-composition/documentation-bundle.internal.ts:48-53` - -Two schemas exist for the same input: `ProjectDocumentationBundleOptionsSchema` (typed `documentType`) and `RawProjectDocumentationBundleOptionsSchema` (`documentType: z.string()`). The typed schema is never used at the boundary — `parseAndProject` only invokes the raw one. The typed one only exists for re-export and the inferred `ProjectDocumentationBundleOptions` type. - -**Why it matters for the campaign:** Once the closed `SupportedDocumentationType` union goes away (C1), this raw/typed split becomes meaningless. Cleaning it up unblocks a single uniform schema. - -**Fix recommendation:** Collapse to one schema: `documentType: z.string()` with a `.refine(isRegisteredDocType, ...)` runtime check. The `SupportedDocumentationType` type alias becomes `string`. - -### M6. Generic-fragment markdown fallback reflects on arbitrary objects -**File:** `packages/architect-projection/src/renderers/render-markdown.ts:1042-1133`, `1184-1255` - -`normalizeGenericFragment` walks the fragment with `Object.entries`, dispatching on `isBlockArray`, `isPrimitiveLike`, `toTabularRows`, then `humanizeKey`-ing field names into headings. It's a reflection-based reader that has no relationship to the Zod schema for the fragment. - -**Why it matters for the campaign:** When the campaign adds Zod-schema → field-table extraction (DEEP-DIVE Q1), it will conflict with this generic reflection path. Pick one — and the schema-driven path is correct. - -**Fix recommendation:** Drop the generic fallback in favour of "every fragment kind has a registered normalizer" (M4). For Zod-schema field tables, write a dedicated extractor that walks the schema, not the value. - -### M7. `_internal/format-utils.ts` is shared between renderers and projection support without documented contract -**File:** `packages/architect-projection/src/_internal/format-utils.ts` + four import sites - -`humanizeKey`, `isPrimitive`, `sortValue`, `stableStringify` are imported from `_internal/` by three renderers. `_internal/` is the trust-boundary helper directory per scope. Mixing rendering utilities and trust-boundary helpers in the same namespace risks accidentally exposing the latter. - -**Why it matters for the campaign:** The campaign will add more shared helpers (slug, field-table formatters). Putting them in `_internal/` will further blur the boundary. - -**Fix recommendation:** Move pure formatting utilities into `blocks/format.ts` or `renderers/_shared/format.ts`; keep `_internal/` strictly for trust-boundary helpers (slug, escape, sanitize). - -### M8. `MarkdownDocument` title resolution conflates derivation strategies -**File:** `packages/architect-projection/src/renderers/render-markdown.ts:1182-1276` (`resolveFragmentMetadata`, `deriveTitle`, `getRoadmapViewTitle`) - -The metadata-resolution path tries six different sources in order (`fragment.title`, `fragment.label`, `fragment.name`, `getRoadmapViewTitle`, `humanizeKey(kind)`, …). It's a search-the-haystack approach that works today by virtue of the fragments having consistent shape. - -**Why it matters for the campaign:** ContentFragments will have explicit titles per disclosure level. Routing those through the existing search path is fragile. - -**Fix recommendation:** Each fragment normalizer returns its own `{title, purpose, detailLevel}` (it already mostly does). Delete the generic search path or scope it to the generic-fallback case only. - -### M9. `documentation-types.ts` at 517 LOC is the largest file in the campaign hot zone -**File:** `packages/architect-projection/src/projections/documentation-composition/documentation-types.ts` - -517 lines housing four concerns: Zod schemas, registry data, freeze helpers, filter resolution. Three of those (schemas, freeze helpers, filter resolution) are cross-cutting; only the registry data is doc-specific. - -**Why it matters for the campaign:** When `DocDefinition` replaces the registry, this file must shrink dramatically — the schemas stay, the data goes (to `architect.config.ts` and per-package `*.doc.ts` files). If schemas + helpers stay tangled, the campaign's migration step ends with a file that's still 300+ lines of legacy. - -**Fix recommendation:** Split into `documentation-types.schema.ts` (Zod schemas + types, no data), `documentation-types.registry.ts` (the literal array), `documentation-types.freeze.ts` (or replace with generic deepFreeze per M1), and `disclosure-filter.ts` (resolve-projection-filter). Done before the campaign so the campaign only edits the registry file. - ---- - -## Low - -### L1. `parseLogicalRouteId` returns three different shapes, callers re-discriminate -**File:** `packages/architect-projection/src/renderers/markdown-paths.ts:55-91` - -The function returns a discriminated union but `resolveLogicalRoutePath` (`:12-40`) uses a string of `if (route.kind === 'index')` / `if (route.kind === 'entity')` ladders. Switch-with-exhaustiveness would catch missing cases at compile time. - -**Fix:** Replace `if/if/if` with `switch (route.kind)` so adding a new route kind is a TS error. - -### L2. `isBundle` runtime predicate accepts shapes the type system already guarantees -**File:** `packages/architect-projection/src/fragments/base.ts:21-39` - -`isBundle` re-validates the shape (root is fragment-like, children is plain object, every value is fragment-like) on every call. Used in every renderer entry point. With Zod-first parsing at the projection boundary, this is parse-twice. - -**Why it matters for the campaign:** Per-doc fan-out (5–10× projection calls) per the perf flag in the scope means `isBundle` is on a hot path. - -**Fix:** Replace the deep check with `typeof value === 'object' && value !== null && 'root' in value && 'children' in value && !('kind' in value)` — fragments have `kind`, bundles don't. Or trust the parse-once doctrine and lift this out of renderer entry. - -### L3. `BlockSchema` and `Block` interface are declared independently -**File:** `packages/architect-projection/src/blocks/schema.ts:3-71` (interfaces) vs `:73-152` (schemas) - -The block types are hand-written `interface` declarations *and* hand-written `z.strictObject` schemas. They are not connected by `z.infer`. This is the same Zod-first violation as H1 but in the blocks layer. - -**Why it matters for the campaign:** ContentFragments emit `SectionBlock[]` — exactly these blocks. Two declarations of the same type doubles the risk of drift when new block types are added (the campaign may add `field-table` or `code-with-callouts`). - -**Fix:** Make `Block = z.infer` canonical, delete the parallel interfaces. Block-constructor helpers (`heading()`, `paragraph()`, …) keep their explicit return types. - -### L4. `documentation-bundle.ts` is a 47-line wrapper that only re-exports from `.internal.ts` -**File:** `packages/architect-projection/src/projections/documentation-composition/documentation-bundle.ts` - -Every public bundle function delegates one-to-one to its `.internal.ts` counterpart. The `.internal.ts` distinction is meaningful in some files but here it's pure indirection — the JSDoc lives on the wrapper, the code lives on the internal. - -**Why it matters for the campaign:** Once `DocDefinition` lands, this whole indirection is going away. Worth noting now so the migration doesn't preserve it. - -**Fix recommendation:** Inline `projectDocumentationBundleInternal` into `documentation-bundle.ts`; promote the schema/types from internal. Apply the same simplification once campaign rewrites the dispatch. - -### L5. `documentation-composition-shared.internal.ts` carries only two helpers (`dedupeStrings`, `hasText`) -**File:** `packages/architect-projection/src/projections/documentation-composition/documentation-composition-shared.internal.ts` - -`hasText` is reimplemented at `render-markdown.ts:1555-1557` (different file, same name, same behaviour). `dedupeStrings` is reimplemented at `render-markdown.ts:1559-1574`. - -**Why it matters for the campaign:** Code-search-driven copy is how triplicates start. New extractors will keep reimplementing these. - -**Fix recommendation:** Hoist `hasText` and `dedupeStrings` to `_internal/format-utils.ts` (or a `_internal/strings.ts`), import everywhere. - ---- - -## Summary of campaign impact - -- **Critical** findings (C1, C2) block the `DocDefinition` migration directly — the closed-enum dispatch table and the multi-concern registry shape must be opened up before the new API can replace them. -- **High** findings (H1–H6) describe the structural rework the campaign must perform anyway: Zod-first registry, deletion of the dropped-type shim, renderer/projection decoupling, hardcoded-route-type removal, splitting the 2152-line render-markdown into fragment-owned normalizers, and exporting `RenderableDocument`. Doing them before the campaign converts most of the campaign's "Wave 4" work into pure DocDefinition authoring. -- **Medium** and **Low** are cleanups that get strictly worse as new extractors and fragments arrive — best resolved in the same sweep that fixes C1/C2. diff --git a/.full-review/01b-architecture-raw.md b/.full-review/01b-architecture-raw.md deleted file mode 100644 index 9cb171a..0000000 --- a/.full-review/01b-architecture-raw.md +++ /dev/null @@ -1,212 +0,0 @@ -# `architect-projection` — Architecture Review (Doc-Gen Consolidation Campaign Lens) - -**Reviewer:** Software-architect persona -**Scope:** `packages/architect-projection/src/**` (135 files) -**Lens:** Pre-evaluation of the PROPOSED-DESIGN doc-gen consolidation campaign — flagging structural issues that will block, complicate, or invalidate the incoming `DocDefinition.build(graph)` / `ContentFragment` / multi-target work. - ---- - -## Findings - -### F1. `documentation-bundle.internal.ts` — closed-by-`satisfies` dispatch, no extension point -- **Severity:** Critical -- **Architectural impact:** This is the explicit ceiling the campaign targets. The dispatch is closed at compile time; `DocDefinition.build()` cannot plug in without replacing the file outright. -- **Location:** `src/projections/documentation-composition/documentation-bundle.internal.ts:64-79` -- **Description:** `DOCUMENTATION_PROJECTION_FACTORIES` is a closed object literal typed `satisfies Record`. The set of supported document types comes from the `DOCUMENTATION_TYPE_REGISTRY` enum in `documentation-types.ts`. There is no registry, no plug-in surface, no externally constructible `DocumentationProjectionFactory`. To add a new doc type today you must edit (a) the registry array, (b) the factory map, (c) any renderer that key-maps off documentation type — each is closed shape. -- **Recommendation:** Treat the dispatch table as legacy at the start of the campaign. Author `DocDefinition` as a peer mechanism whose contract is `(ctx: DocBuildContext) => RenderableDocument | Promise<...>`. Wire the runner that iterates `config.docs` directly; delete `DOCUMENTATION_PROJECTION_FACTORIES` once the 12 entries port. Do not try to retrofit a registry into the existing dispatch — the campaign already has a cleaner shape (the `build()` function IS the registration). - -### F2. `documentation-types.ts` couples the registry, disclosure matrix, type aliases, freeze logic, and runtime filter resolution into one 517-LOC module -- **Severity:** High -- **Architectural impact:** This module is the de-facto "doc-gen config" — and it is the file `DocDefinition` is meant to replace. Its overgrowth makes the migration path concretely harder because the four concerns inside it have to be unpicked in lockstep. -- **Location:** `src/projections/documentation-composition/documentation-types.ts` -- **Description:** Single file contains: (1) the supported/dropped enum and registry data (~200 LOC), (2) per-type disclosure-matrix builders (~70 LOC), (3) discriminated-union Zod schemas (`SupportedDocumentationTypeRegistryEntrySchema` + `DroppedDocumentationTypeRegistryEntrySchema`), (4) `resolveProjectionFilter()` — a runtime context-merging function (~25 LOC), (5) freeze helpers (~50 LOC). Half of the surface is consumed only by `documentation-bundle.internal.ts` (and `render-markdown.ts` for `getDocumentationTypeMetadata`); the other half (Zod schemas, the dropped-type enum) is consumed at the public `./projections` boundary. -- **Recommendation:** Before the campaign begins, split this file along the three obvious seams: `documentation-type-registry.ts` (just the data array + lookup), `disclosure-matrix.ts` (the matrix builder + per-type matrices), `projection-filter-resolver.ts` (the merge function). The "freeze" helpers are over-engineered for an `as const` literal — drop them in the split, the `Object.freeze` is redundant given the literal's compile-time readonly-ness. This split is a prerequisite for the campaign to land cleanly because `DocDefinition`s want to own the disclosure choices per doc, not lift them from a centralized matrix. - -### F3. `documentation-types.ts:299-340` — three registry entries hardcoded as `status: 'dropped'` is a backward-compatibility shim -- **Severity:** High (no-BC doctrine violation) -- **Architectural impact:** The `'dropped'` discriminator and `isDroppedDocumentationType()` exist solely to produce a politer error message for callers passing `'reference'`, `'product-areas'`, `'design-review'`, `'product-requirements'`. That's a deprecation shim. -- **Location:** `src/projections/documentation-composition/documentation-types.ts:295-339, 49-64, 383-385`; `documentation-bundle.internal.ts:81-98` -- **Description:** Per `CLAUDE.md`'s no-BC clause: "Backward-compatibility aliases (re-exporting an old name from a new location, parallel implementations behind a feature flag, etc.)" are banned, and "`@deprecated` markers as a way to soften a removal" likewise. The dropped-doc-type registry is exactly the latter — it ships dead entries with metadata only so the error message can say "intentionally dropped" instead of "unknown." A clean error path would just throw `UNKNOWN_DOCUMENT_TYPE` for these strings. -- **Recommendation:** Delete `DroppedDocumentationTypeRegistryEntrySchema`, `DROPPED_DOCUMENTATION_TYPE_REGISTRY`, `DROPPED_DOCUMENTATION_TYPES`, `isDroppedDocumentationType`, and the corresponding branch in `assertSupportedDocumentType`. The `UNKNOWN_DOCUMENT_TYPE` error already lists supported types — that's sufficient. This cleanup is independent of the campaign but blocks the campaign from authoring a `DocDefinition` named e.g. `'design-review'` cleanly. - -### F4. Renderer reaches into `documentation-composition` — codec/renderer line blurred (ADR-005 adherence drift) -- **Severity:** High -- **Architectural impact:** The renderer is supposed to consume fragments by `kind` and trust the shape (ADR-009). Instead it reads the documentation-type registry and disclosure matrix at render-time to decide split strategy, child paths, and emit-children behavior. That makes the renderer doc-type-aware and means new doc types can't be added without renderer changes. -- **Location:** `src/renderers/render-markdown.ts:50` (`getDocumentationTypeMetadata`), `src/renderers/render-markdown.ts:400-421` (`resolveBundleDisclosureSpec`), `src/renderers/markdown-paths.ts:3` (`defaultMarkdownRouteProfile` queries registry) -- **Description:** ADR-005 says codecs produce fragments, renderers consume them. ADR-009 says the projection layer is the trust boundary; renderers downstream of that boundary trust the shape. But `render-markdown.ts` line 400-421 derives the disclosure spec by looking up `rootRouteId` in the documentation-type registry and reading `metadata.disclosureMatrix[level]`. This is logic that belongs in the projection layer — the projection should produce a `ProjectionBundle` whose `routing` already encodes the disclosure-driven split decisions, and the renderer should mechanically follow `routing`. -- **Recommendation:** Move disclosure resolution upstream: the `projectDocumentationBundleInternal` function should set `routing.disclosureSpec` (extend `BundleRouting` if needed) so the renderer can read it off the bundle without consulting the registry. This is the right factoring for the campaign because each `DocDefinition.build()` will set its own disclosure spec — the renderer cannot look up a per-`DocDefinition` registry it doesn't know about. Decouple now, before the campaign multiplies the dependency. - -### F5. `renderers/types.ts` imports from `projections/documentation-composition/*` — directory dependency-direction inversion -- **Severity:** High -- **Architectural impact:** Renderers depend on documentation-composition types (`DisclosureSpec`, `LogicalRouteId`). This breaks the conceptual layering where `renderers/` consumes `fragments/` (and `blocks/`) but not domain-specific `projections/`. The campaign will make this worse — `DocDefinition` will live somewhere that consumes both, and the current cross-link constrains where it can land. -- **Location:** `src/renderers/types.ts:2-3`, `src/renderers/markdown-paths.ts:3-4`, `src/renderers/render-markdown.ts:50,52` -- **Description:** A renderer-side contract type (`RenderMarkdownOptions`) carries `disclosureLevel` and `disclosureSpec`, both sourced from `../projections/documentation-composition/`. This couples the renderer's public contract to a particular projection domain. There is no circular import (the dependency is one-way), but it forces every consumer of `./renderers` to transitively depend on documentation-composition's schemas — including consumers (like `architect-cli`) that render fragments unrelated to documentation bundles. -- **Recommendation:** Promote `DisclosureSpec`, `LogicalRouteId`, and the disclosure-vocabulary enum to a shared module (e.g., `src/disclosure/`) that both `projections/documentation-composition/` and `renderers/` depend on. This is small (just file moves + import-rewrites) but it unlocks the campaign: `DocDefinition` and `ContentFragment` will both consume the disclosure vocabulary without dragging in documentation-composition's full registry. - -### F6. `_internal/` is naming convention only — not enforced -- **Severity:** Medium -- **Architectural impact:** The `_internal/` directory and `*.internal.ts` suffix suggest a sealed boundary, but neither is enforced by linting, package.json `exports`, or ESLint rules. External packages CAN import `dist/projections/documentation-composition/documentation-bundle.internal.js` directly via the `./projections` sub-entry (the barrel re-exports public surface, but tarball contains the internals). -- **Location:** `src/_internal/`, every `*.internal.ts` file -- **Description:** The `package.json` `exports` map exposes `./projections`, `./blocks`, `./fragments`, `./renderers` and points each at a single barrel `index.d.ts/index.js`. Modern bundlers will respect that, but anyone importing the deep path (e.g., via package source if linked, or via TS path-mapping) can reach internals. The campaign will be tempted to import `projectDocumentationBundleInternal` directly from `DocDefinition` runners — the convention won't stop them. -- **Recommendation:** Either add ESLint `import/no-internal-modules` with explicit allowlists, OR rename to `*.unstable.ts` (a stronger social signal), OR add explicit `"./projections/documentation-composition/*.internal": null` entries to `exports`. The campaign should treat `*.internal.ts` as truly closed; that needs reinforcement before W-DOCS-1 begins. - -### F7. Fragment-domain boundary is incoherent — `documentation-composition` fragments are routing primitives, not domain content -- **Severity:** Medium -- **Architectural impact:** `documentation-composition` mixes a content fragment (`ArchitectureDiagram`), a registry fragment (`ProjectConfigSnapshot`), and an aggregator-dispatcher (`projectDocumentationBundle`) in one directory. The campaign will add `ContentFragment` as a layer on top of `Fragment` — that name collision is going to be painful unless this is straightened out first. -- **Location:** `src/fragments/documentation-composition/`, `src/projections/documentation-composition/` -- **Description:** Six fragment domains are listed: `pattern-relations`, `governance`, `operational-insights`, `delivery-reporting`, `execution-context`, `documentation-composition`. The first five are coherent (each groups related domain content). `documentation-composition` is the odd one — its three fragments don't share a domain shape, they share the property "needed by the documentation pipeline." -- **Recommendation:** Move `ArchitectureDiagram` into `pattern-relations/` (it IS pattern-relation visualization). Move `ProjectConfigSnapshot` into a new `meta/` domain or `execution-context/`. Move `PrChangeReview` into `delivery-reporting/`. That leaves `documentation-composition` to be exactly what its name says: the doc-composition machinery (registry, disclosure, routing), not domain content. This pre-cleanup makes `ContentFragment` a clearer addition because there's no naming clash with the residual "documentation-composition fragments" concept. - -### F8. 43 projection functions, signature drift — `parseAndProject*` wrappers come in three flavors -- **Severity:** Medium -- **Architectural impact:** A generic `DocDefinition.build()` cannot call projections uniformly because their option-handling is inconsistent. This is a per-extractor authoring tax that compounds across ~10+ extractor uses per `DocDefinition`. -- **Location:** Survey across `src/projections/**/*.ts` -- **Description:** Three patterns coexist: (a) `project*` takes typed options and returns directly (most common); (b) `parseAndProject*` is a curried function from `parseAndProject(schema, projectFn, name)` wrapping raw-options into typed; (c) some projections only export the parsed variant (e.g., `parseAndProjectSessionContext`), others only the typed variant (e.g., `projectDeliverable`), and most export both. There is no convention for which to use from doc-gen. -- **Recommendation:** Enforce a uniform signature for projections that should be callable from `DocDefinition` runners: `(ctx: ProjectionContext, options?: T) => ProjectionBundle`. The `parseAndProject` wrapper is for CLI/MCP boundaries where raw `unknown` arrives — `DocDefinition` runners get a typed options object compile-checked, so they don't need parse-at-boundary. Document the rule (in the package's `@architect-trust-boundary` annotation if one exists, or `ARCHITECTURE.md`) and grep-audit the 43 functions before extractor-catalog work begins (W-DOCS-2). - -### F9. `Fragment` discriminated union (43 variants) is a closed set — `ContentFragment` proposal will fight this -- **Severity:** Medium -- **Architectural impact:** `ContentFragment.build()` returns `SectionBlock[]`, not a `Fragment`. That means ContentFragments cannot participate in the `ProjectionBundle` model — they bypass it entirely. The proposed design accepts this (it returns blocks directly into `composeDoc`), but it means two parallel "fragment" concepts live in the package. -- **Location:** `src/fragments/fragment-schema.internal.ts:69-113` (closed union), proposed `ContentFragment` in `PROPOSED-DESIGN.md` -- **Description:** Today's `Fragment` is a Zod discriminated union over 43 `kind` literals. Adding a 44th would force a schema and renderer normalizer. The campaign sidesteps this by making `ContentFragment` emit `Block[]` directly — which works, but creates a conceptual schism: a `DocDefinition` will compose `Block[]` from `Fragment`s (via existing projections) AND from `ContentFragment`s (new), with different shape, validation, and trust semantics. -- **Recommendation:** Embrace the schism explicitly. Document the two layers: (1) `Fragment` is for per-pattern domain content with strict schemas (still validated at the projection trust boundary), (2) `ContentFragment` is for reusable composed-block emitters with a typed input but no `kind`-based registry. Add a top-level `src/composition/` (or `src/doc-definition/`) directory for `DocDefinition` + `ContentFragment` + `composeDoc` — not under `fragments/` (would mislead), not under `projections/` (already too crowded), not under `renderers/` (this is upstream of rendering). The package will then have a 7th top-level directory; that's fine. - -### F10. Block schema does not enforce nesting depth — `CollapsibleBlock.content: Block[]` is lazy-recursive -- **Severity:** Medium -- **Architectural impact:** ContentFragments will emit collapsible sections that can themselves contain collapsibles (e.g., per-disclosure-level fan-out). No upper bound on nesting means a pathological ContentFragment can produce a tree the markdown renderer cannot pretty-print or the perf gate cannot bound. -- **Location:** `src/blocks/schema.ts:50-54, 130-134, 142-152` -- **Description:** `CollapsibleBlockSchema` uses `z.lazy()` to allow `Block[]` recursion. There's no `maxDepth`, no validation of leaf-density. Today's projection codecs are well-behaved by convention, but the campaign will hand the pen to many `ContentFragment` authors who will encounter this. -- **Recommendation:** Either add a documented depth limit enforced by a render-time guard (rendererdrops or warns on `depth > N`), OR add a recursion-depth check at the projection trust boundary. The perf-gate fixture (`baseline × 1.5`) should be extended to include a "deeply nested collapsibles" worst case so the campaign's regression bound stays meaningful. - -### F11. `BlockSchema` is the natural target for ContentFragment-emitted blocks — but `parseMarkdownToBlocks` (in core) supports only 6 of the 9 kinds -- **Severity:** Medium -- **Architectural impact:** Preamble loading (`loadPreambleFromMarkdown` in PROPOSED-DESIGN W-DOCS-1) will flow user-authored markdown through `parseMarkdownToBlocks` (lives in `architect-core`). That parser supports `heading | paragraph | separator | table | code | list` per DEEP-DIVE — `collapsible`, `link-out`, `mermaid` cannot survive the round-trip from a hand-authored preamble. -- **Location:** `src/blocks/schema.ts` (9 block kinds); `@libar-dev/architect-core/utils/markdown-parser.ts` (6 supported in parse) -- **Description:** The block catalog defines 9 kinds, but only 6 are reachable through markdown ingestion. Authors of preamble files cannot use HTML `

` (collapsible) or `[text](path)` link-out tagging or fenced mermaid blocks — those will either be flattened or rejected. This is a cross-package observation (the parser lives in core), but its impact lands inside `architect-projection`: every `DocDefinition` that loads a preamble inherits this constraint. -- **Recommendation:** Two paths, pick one in the design session: (a) Extend `parseMarkdownToBlocks` in core to support all 9 block kinds — collapsible via `
...`, mermaid via ` ```mermaid ` fences (the data is already there), link-out via a hint syntax. (b) Document the constraint explicitly in `BlockSchema`'s `@architect-trust-boundary` annotation: "preambles emit a 6-kind subset; the other 3 are projection-emit-only." Option (a) is right because it makes preambles a first-class authoring surface — exactly what the campaign needs. - -### F12. `ProjectionBundle.children` is `Record` — not typed enough to carry per-child disclosure or routing metadata -- **Severity:** Medium -- **Architectural impact:** The OUTPUT-side progressive-disclosure machinery already fans out one bundle into many files via `children`. The INPUT-side disclosure that `ContentFragment` introduces will produce children at varying disclosure levels. There's no way to attach per-child disclosure metadata to the existing `children` map without inventing a side-channel. -- **Location:** `src/fragments/base.ts:15-19` -- **Description:** `children: Record` has only Fragment as the value type. The companion `routing` field has `childRouteIds` but no per-child disclosure or richness. Today's renderer fakes this by re-reading the registry (see F4). When the campaign emits `ProjectionBundle`s with mixed-disclosure children, there's no carrier for "this child was emitted at `useful`, render it inline; that one at `advanced`, split to a separate file." -- **Recommendation:** Promote `children` to `Record` — or add a parallel `childMeta: Record` map keyed by the same child key. Either makes the disclosure/render decision local to the bundle, eliminating the renderer's need to consult the documentation-type registry (fixes F4 too). Touch this in W-DOCS-1 before authoring `DocDefinition`s; touching it later cascades through every projection. - -### F13. The 11 unreachable projections (per INVENTORY) are an architecture symptom, not just routing -- **Severity:** Medium -- **Architectural impact:** Projections like `projectDependencyEdges`, `projectPatternSummary`, `projectDeliverable`, `projectDeliverableManifest` exist with full schemas and tests but no end-user surface. The campaign's "pull-routing extractors" assume projections compose; if 25% of them have never been composed, the composability assumption is unproven. -- **Location:** INVENTORY §1, rows 4, 11, 19, 22, 25, 26, 27, 28, 30, 33, 37 (the ❌-❌ rows) -- **Description:** Eleven projection functions are reachable through neither CLI/MCP nor `docs:all`. They were shipped against design specs but never wired. Some of these (e.g., `projectDeliverable`/`projectDeliverableManifest`) are obvious campaign building blocks; others (e.g., `projectDependencyEdges` vs. `projectDependencyTree`) are duplicative shapes the campaign should pick between. -- **Recommendation:** Before W-DOCS-2 (extractor catalog), audit each of the 11 dead projections: (a) which is the campaign extractor's natural foundation? (b) which is duplicative and can be deleted? Move the chosen ones into the `extractors/` shape proposed in §2 of PROPOSED-DESIGN. Delete the others — per no-BC, dead code is not a future option, it's permanent tax. - -### F14. Aggregation-tag push routing — no projection-layer hook point exists -- **Severity:** Medium -- **Architectural impact:** The campaign's "push model" via aggregation tags with `targetDoc` is documented as already-supported in the registry, but `architect-projection` doesn't expose an extractor for it. To wire it, a new projection has to be added. -- **Location:** No file — absence finding. `src/projections/governance/taxonomy-digest.ts` is the nearest cousin (it surfaces tag registry data); no `projectAggregationMatches` exists. -- **Description:** Aggregation tags live in `PatternGraph.tagRegistry` (per architect-core), but `architect-projection` exposes only the taxonomy digest. The campaign's `extractAggregations(ctx, aggregationTag)` extractor has no current projection to wrap. -- **Recommendation:** Add a `projectAggregationMatches` projection in `src/projections/governance/` (or wherever the tag-registry surface settles) that takes `{ aggregationTag: string; filter?: ... }` and returns `{ entries: Array<{ patternId; sourceFile; jsdoc?: string; ... }> }`. Schema-validate at the boundary like every other projection. The campaign extractor is then a 5-line wrapper. Doing this before W-DOCS-2c (push-routing wiring) shortens the critical path. - -### F15. `RenderMarkdownOptions.disclosureLevel` is renderer-state, not pipeline-state -- **Severity:** Medium -- **Architectural impact:** Two orthogonal disclosure axes (INPUT-side at `ContentFragment.build`, OUTPUT-side at `renderMarkdown(...)`) are supposed to compose. Today's OUTPUT-side option lives on the renderer call, not the `ProjectionBundle`. The `DocDefinition.build()` runner has no way to convey output-disclosure intent forward except by passing it through every layer. -- **Location:** `src/renderers/types.ts:11-19` -- **Description:** A `DocDefinition` wants to declare "this doc should render at output-disclosure `important`" once. But disclosure-level is consumed at render time, not bundle time — so the runner has to thread it through `renderMarkdown(bundle, { disclosureLevel })` per doc. The proposed `DocDefinition` shape in §1 of PROPOSED-DESIGN doesn't show this — it returns `RenderableDocument` and renderer call site is implicit. The thread-through will leak. -- **Recommendation:** Move `disclosureLevel` and `disclosureSpec` from `RenderMarkdownOptions` onto `ProjectionBundle.routing` (or a new `metadata` field on the bundle). The renderer reads it off the bundle. `DocDefinition.build()` sets it once at bundle-build time. This unifies disclosure ownership and resolves F4 and F12 simultaneously — disclosure is a property of the rendered work, not a parameter of the rendering call. - -### F16. Subentry `exports` map omits `/context` — context types leak only through the root barrel -- **Severity:** Low -- **Architectural impact:** Sub-entry partitioning is intentional (per `index.ts` header comment) but consumers wanting `ProjectionContext` must import from the root barrel, which transitively pulls everything else. This is a minor friction point that the campaign will hit because every `DocDefinition.build(ctx: DocBuildContext)` will want `ProjectionContext`. -- **Location:** `package.json:25-46`, `src/index.ts:21-26` -- **Description:** The four sub-entries (`./blocks`, `./fragments`, `./projections`, `./renderers`) intentionally don't include `ProjectionContext`. Per index.ts header, "Context types that are shared across subdomains stay explicitly enumerated below." The result is the root barrel re-exports ~400+ symbols, dominantly schemas, so a consumer that just needs `ProjectionContext` pays the full tree-shake cost. -- **Recommendation:** Add a `./context` sub-entry. Add a `./composition` (or `./doc-definition`) sub-entry as part of W-DOCS-1 — that's where `DocDefinition`, `ContentFragment`, `composeDoc`, and the helpers in PROPOSED-DESIGN §3 will live. This keeps `architect-cli` and `architect-mcp` consumers from pulling in 43 projections when they only want `composeDoc`. - -### F17. `DisclosureSpec` is at `documentation-composition/` but its vocabulary is package-wide -- **Severity:** Low -- **Architectural impact:** The disclosure vocabulary (`essential | important | useful | advanced`) is shared by renderers, projections, and (per the campaign) ContentFragments. It currently lives under one specific projection domain. -- **Location:** `src/projections/documentation-composition/disclosure-spec.ts`, `progressive-disclosure.ts` -- **Description:** `DisclosureSpec` (the rich object) and `ProgressiveDisclosureLevel` (the enum) are project-wide vocabulary, but they're parked inside a single projection domain. This compounds F5 — the rest of the package has to reach into one domain's directory to use a vocabulary that doesn't belong there. -- **Recommendation:** Promote the disclosure vocabulary to a `src/disclosure/` directory: `levels.ts` (enum + policy), `disclosure-spec.ts`, `logical-route-id.ts`. `documentation-composition` then depends on it like everyone else. This is W-DOCS-1 cleanup, ~2 hours of mechanical moves. - -### F18. `progressive-disclosure.ts` couples disclosure levels to logical route IDs -- **Severity:** Low -- **Architectural impact:** Two unrelated concepts (disclosure levels + route-ID format) coexist in one file. The route-ID system is general-purpose routing; disclosure is content-depth selection. Conflating them means a consumer that wants route-IDs (e.g., a fragment-link extractor) drags in the disclosure machinery. -- **Location:** `src/projections/documentation-composition/progressive-disclosure.ts` -- **Description:** The 120-line file mixes `PROGRESSIVE_DISCLOSURE_LEVELS`, `ProgressiveDisclosurePolicySchema`, and `createIndexRouteId`/`createEntityRouteId`/`createChildRouteId`/`isLogicalRouteId`. The route-ID machinery is what `BundleRouting.rootRouteId` and `MarkdownRouteProfile.mapPath` consume — neither knows about disclosure. -- **Recommendation:** Split into `disclosure-levels.ts` and `logical-route-id.ts`. Done as part of F17's promotion to `src/disclosure/` and a sibling `src/routing/`. Trivial mechanical refactor; pays off because the campaign's `linkToCanonical()` helper needs route-IDs but not disclosure. - -### F19. No formal `RenderableDocument` envelope type — PROPOSED-DESIGN references it but it doesn't exist -- **Severity:** Low (campaign-naming gap, not present-day bug) -- **Architectural impact:** PROPOSED-DESIGN refers to `RenderableDocument` as if it exists. The closest current shape is `ProjectionBundle`. `DocDefinition.build()` returning `RenderableDocument` needs a real type definition first. -- **Location:** Absence finding (PROPOSED-DESIGN §1) -- **Description:** `RenderableDocument` is mentioned in the scope file and design doc, but no such Zod schema or TypeScript type exists in `src/blocks/` or `src/fragments/`. Today's renderable substrate is `ProjectionBundle`. A `DocDefinition`'s output type must be something the renderer can consume — either a `ProjectionBundle` of a new top-level fragment kind, or a plain `Block[]` envelope. -- **Recommendation:** In W-DOCS-1, define `RenderableDocument` explicitly: `{ title: string; metadata?: {...}; sections: SectionBlock[]; routing?: BundleRouting }`. Make the renderer accept both `ProjectionBundle` AND `RenderableDocument` via a discriminated union. This avoids creating a synthetic 44th `Fragment.kind` just to make `DocDefinition` outputs flow through the existing pipeline. - -### F20. No CI guard that perf-gate fixture exercises `documentation-bundle` -- **Severity:** Low (verification gap) -- **Architectural impact:** The campaign will multiply doc-gen fan-out 5–10x (per the scope file). The perf gate exists at 36-pattern/108-rule fixture (per scope). If the perf fixture exercises only isolated fragment projections, the campaign's projection multiplication could silently breach budgets at real scale. -- **Location:** `tests/perf/` (presence assumed from scope), `documentation-bundle.internal.ts:64` -- **Description:** The 12-entry dispatch is the natural integration point for fan-out cost. If the perf test only times individual `project*` calls, it misses end-to-end doc-bundle cost. -- **Recommendation:** Add a perf scenario that exercises `projectDocumentationBundle` for all 12 documentation types in one run, including disclosure-level variation. Bake this into the baseline before W-DOCS-1 lands so the campaign's regressions are detectable. The pre-existing `baseline × 1.5` ceiling stays in force. - ---- - -## Welcomes the campaign - -These are places the current architecture is well-positioned for the proposed work — do not touch. - -### W1. `BlockSchema` discriminated union with `z.strictObject` per variant is exactly the right substrate for ContentFragment output -- **Location:** `src/blocks/schema.ts:142-152` -- **Why:** 9 kinds, closed-shape via discriminated union, factory functions (`heading`, `paragraph`, `code`, `mermaid`, `collapsible`, `linkOut`, etc.) are all already in place. `composeDoc()` in PROPOSED-DESIGN §3 will be a thin orchestrator over these existing primitives. The block-emission API is the asset; the campaign builds composition on top, not around. - -### W2. `parseAndProject` is the right trust-boundary abstraction — adopt unchanged for `DocDefinition` runners -- **Location:** `src/projections/_shared/parse-and-project.internal.ts` -- **Why:** This shared helper enforces "parse once at the trust boundary," validates via `parseAtBoundary` from core, and returns a typed function. `DocDefinition` runners can adopt the exact same pattern for the `config.docs[]` entries themselves: parse the `DocDefinition` schema once at runner-load time, then trust the shape. The doctrine (Zod-first + parse-once) propagates cleanly. - -### W3. `ProjectionBundle` + `routing` + `LogicalRouteId` are a working fan-out substrate the campaign extends, not replaces -- **Location:** `src/fragments/base.ts`, `src/projections/documentation-composition/progressive-disclosure.ts` -- **Why:** Multi-target output (DocTarget[]) and per-disclosure-level child documents are already modeled. `BundleRouting.childRouteIds` + `childPathStrategy` + `anchorStrategy` + `MarkdownRouteProfile.mapPath` form a working renderer-side route-resolver. The campaign's "multi-target output" feature plugs into this, it doesn't reinvent it. - -### W4. The `parseAndProject*` boundary pattern is uniformly applied across the package -- **Location:** Every `*.ts` peer to `*.internal.ts` in `src/projections/` -- **Why:** ~20 projection functions consistently use `parseAndProject(Schema, projectFn, name)`. The discipline of `*.ts` for the typed boundary and `*.internal.ts` for the schema + implementation is one of the strongest patterns in the codebase. Extractors (W-DOCS-2) should adopt the same pattern verbatim — no new convention required. - -### W5. `RenderMarkdownOptions.disclosureLevel`/`disclosureSpec` already integrates the output-side disclosure machinery -- **Location:** `src/renderers/types.ts:11-19`, `src/renderers/render-markdown.ts:400-421`, `splitOversizedDocument` machinery -- **Why:** Despite F4 (renderer reads registry), the OUTPUT-side disclosure is fully wired: bundle children flatten or split, h2-boundary splitting works, the renderer-contract feature pins the behavior in tests. The INPUT-side `ContentFragment.build(ctx, { disclosure })` proposal can layer on top without redesigning the output side. Same vocabulary, independent concerns — the substrate is genuinely in place. - ---- - -## Fights the campaign - -These are the highest-value findings — places the current architecture will actively resist the proposed work. - -### Fight1. The closed `DOCUMENTATION_PROJECTION_FACTORIES` + `SUPPORTED_DOCUMENTATION_TYPES` enum are the single biggest blocker -- **Where:** F1 + F2 + F3 -- **Why it fights:** Every `DocDefinition` the campaign wants to ship is conceptually a new "documentation type." The current shape forces each one through a closed enum + a closed dispatch + a closed disclosure-matrix. Three closed mechanisms have to be opened or replaced before W-DOCS-1 can deliver a single doc. -- **Resolution direction:** Treat the registry as legacy at the start of W-DOCS-1, build `DocDefinition` runner as the new pathway, port the 12 existing types as `DocDefinition` instances in W-DOCS-5, then delete the registry. Do not retrofit. - -### Fight2. Disclosure ownership is split across renderer-options, registry, and projection-context — `ContentFragment` cannot route around all three -- **Where:** F4 + F5 + F12 + F15 + F17 -- **Why it fights:** The INPUT-side disclosure that `ContentFragment.build(ctx, { disclosure })` introduces composes with OUTPUT-side disclosure only if both axes share an owner. Today, OUTPUT-side disclosure is read from `RenderMarkdownOptions` (renderer-call argument), the documentation-type registry (lookup at render time), AND from `RenderMarkdownOptions.disclosureSpec` (override). Three sources, no single locus. Adding a fourth (per-fragment INPUT-side) without consolidating will produce inconsistent rendering. -- **Resolution direction:** Move disclosure ownership onto `ProjectionBundle.routing` (or a sibling `metadata`). Renderers and runners read it from one place. Promote the disclosure vocabulary to a shared `src/disclosure/` module. Then INPUT and OUTPUT axes are independent concerns over a single carrier. - -### Fight3. `documentation-types.ts` is the most overgrown file in the package and it is exactly the file the campaign replaces -- **Where:** F2 + F3 -- **Why it fights:** 517 LOC of registry + matrix + filter + freeze + dropped-shim. Half of it has to move (to `DocDefinition`), a quarter has to be deleted (no-BC), and the rest needs splitting. The campaign cannot delete it in one shot because four different consumers read different parts. Each consumer migration is a separate decision. -- **Resolution direction:** Pre-split this file along the three seams (registry / matrix / resolver) BEFORE W-DOCS-1. Then the campaign's deletions land cleanly per file. Trying to delete the monolith in one PR will produce a hairball. - -### Fight4. The `_internal/` boundary is unenforced — the campaign will be tempted to import internals -- **Where:** F6 -- **Why it fights:** `DocDefinition` runners need to consult disclosure resolution, registry lookups, and bundle-shape helpers that today live behind the `.internal.ts` convention with no enforcement. Without a real boundary, the campaign code will reach into `documentation-bundle.internal.ts`, `documentation-types.ts` private exports, etc. Once that happens, the cleanup F1–F3 propose becomes a breaking change for the campaign's own code. -- **Resolution direction:** Add ESLint `import/no-internal-modules` with allow-list for tests + within-domain imports. Or rename `*.internal.ts` to `*.unstable.ts` for stronger social signal. Do this before W-DOCS-1 — pure plumbing fix, ~half a day. - -### Fight5. The `Fragment` discriminated union assumes every renderable output has a `kind` — `RenderableDocument` and `ContentFragment` break that assumption -- **Where:** F9 + F19 -- **Why it fights:** The renderer dispatch (`MARKDOWN_NORMALIZERS: KindTable<...>` in render-markdown.ts line 181) keys off `kind`. ContentFragments emit `SectionBlock[]` directly — they don't have a `kind` and shouldn't. The campaign's `composeDoc(title, sections)` produces a `RenderableDocument`, again no `kind`. Either every new construct gets a synthetic `kind: 'RenderableDocument'` Fragment variant (bad: pollutes the schema, fakes domain content), or the renderer learns a second input shape. -- **Resolution direction:** Define `RenderableDocument` as a sibling to `ProjectionBundle` — a discriminated union over the two: `RenderInput = ProjectionBundle | RenderableDocument`. Renderer dispatches at the top: if `Fragment`-based, use the existing normalizer table; if `RenderableDocument`-based, render the `sections` directly. Two top-level shapes, one renderer entry-point. Document the split in `@architect-trust-boundary` annotations. - ---- - -## Summary - -The package's foundation (blocks schema, `ProjectionBundle`, `parseAndProject` discipline, OUTPUT-side disclosure substrate) is solid and the campaign builds on it cleanly. The friction is concentrated in three places: the closed documentation-type registry/dispatch (F1–F3), disclosure ownership scattered across renderer + registry + projection (F4, F5, F12, F15, F17), and the unclear boundary between domain `Fragment`s and the new `ContentFragment` / `RenderableDocument` concepts (F9, F19). Pre-cleaning these three areas before W-DOCS-1 will make every subsequent wave smaller and the no-BC doctrine sustainable. The renderer's lookups into the documentation-type registry (F4) are the strongest pure-architecture finding — they violate ADR-005/009 today and they actively block the campaign tomorrow. diff --git a/.full-review/02-security-performance.md b/.full-review/02-security-performance.md deleted file mode 100644 index c3d0309..0000000 --- a/.full-review/02-security-performance.md +++ /dev/null @@ -1,95 +0,0 @@ -# Phase 2: Security & Performance Review - -Raw reports: `02a-security-raw.md`, `02b-performance-raw.md`. - -## Headline - -**Security: clean. Performance: structurally healthy, but the gate doesn't measure where the campaign lands.** - -- **Security audit** found no exploitable bugs and documented 5 load-bearing invariants the campaign must preserve. The narrow attack surface (markdown trust boundary) is unusually well-defended for 2152 LOC of renderer. -- **Performance audit** confirmed projection costs are graph-size-bounded, not doc-count-bounded, so 5× doc fan-out doesn't bust the budget by itself. But the perf gate has **zero end-to-end coverage of `renderMarkdown`** and only exercises `documentType: 'patterns'` — exactly the gap the campaign will widen. - -## Security findings - -### Verdict - -No Critical or High findings. Two Low defense-in-depth items + 5 invariants. - -### Low-severity defense-in-depth items - -**L1 — Code-block fence escalation bounded at 4 backticks** -- **File:** `src/renderers/render-markdown.ts:1700-1702` (escalation logic), `:1704` (Mermaid block has no escalation) -- **Why it matters:** ContentFragment will route preamble markdown through this path; user-authored preamble could contain 4+ backtick sequences. Today only `decision-records.internal.ts` feeds external text via a regex that captures triple-backtick boundaries only, so it's not exploitable. Activates if the campaign adds new sources of unconstrained text. -- **Fix:** generalize fence escalation to `max(content_max_run + 1, 3)` and apply uniformly to code + Mermaid blocks. - -**L2 — `CodeBlock.language` is unconstrained `z.string().optional()`** -- **File:** `src/fragments/base.ts` (schema), `render-markdown.ts` (interpolation into fence line) -- **Why it matters:** newline in `language` breaks the fence. Same activation profile as L1. -- **Fix:** `z.string().regex(/^[A-Za-z0-9_+-]*$/).optional()` at the schema layer. - -### Invariants the campaign MUST preserve (highest-value output of the audit) - -| ID | Invariant | Why it's load-bearing | -|---|---|---| -| **I1** | `sanitizeMarkdownLinkTarget` is the single chokepoint for link-href validation (decodes HTML entities before scheme classification, enforces `http`/`https`/`mailto` allowlist, rejects control chars). | Any new link-emitting normalizer that bypasses this opens injection routes. | -| **I2** | URL discipline is split: schema rejects malformed shape; renderer rejects unsafe targets. **The UI renderer does NOT sanitize URLs.** | Campaign-relevant: when multi-target output adds new consumers of `RenderableDocument` (e.g., Studio surfacing UI fragments), the missing UI-side sanitizer becomes exploitable. **Hardening priority when Studio comes online.** | -| **I3** | `TRUSTED_MARKDOWN` symbol is module-private (unexported). All 4 call sites feed pre-escaped substrate. | The campaign's `composeDoc(title, sections)` MUST NOT export or accept `TRUSTED_MARKDOWN`-tagged content from outside the renderer module. | -| **I4** | JSON renderer uses `isPlainObject` prototype check before stringify (anti-prototype-pollution). | If `DocDefinition.build()` returns objects with non-default prototypes, JSON output silently changes shape. Preserve the check. | -| **I5** | `parseAndProject` is the single options-parsing entry point. 113 `z.strictObject` uses, zero `z.object`. | The campaign's `DocDefinition` MUST inherit this discipline — open-shape Zod at the new trust boundary is a regression. | - -## Performance findings - -### Verdict - -**The campaign will NOT bust the current perf gate**, but the gate doesn't measure the path the campaign multiplies. Fix H1 + H2 before W-DOCS-1 and post-campaign regressions become observable; skip them and renderer drift slips through silently. - -### High-priority items - -**H1 — `addRoutedDocument` re-renders each split document 2N+2 times** -- **File:** `src/renderers/render-markdown.ts:308-325, 447-466, 2054` -- **Mechanism:** `shouldSplit` pre-render + per-subdoc line-count render in `splitOversizedDocument` + final parent render + sub-file renders. For a doc that splits into N children, the renderer runs N+2 full passes when 1 would suffice. -- **Campaign impact:** the campaign fans out from ~8 docs to ~40, many of which will exercise the disclosure-split path. Today's wasted rendering becomes a noticeable hot spot. -- **Fix:** render once, cache the block stream, take size/split decisions on the cached output. Memoization keyed on `(fragment, options)`. - -**H2 — Perf gate has zero end-to-end coverage of `renderMarkdown`** -- **Files:** `tests/features/perf/business-rule-set-report.steps.ts`, `tests/perf/compare-baseline.mjs` -- **What's measured today:** `parseAndProjectDocumentationBundle` (projection) and `renderJson` (JSON renderer). -- **What's NOT measured:** `renderMarkdown` end-to-end through the bundle pipeline. The 2152-LOC renderer where the campaign's 5× fan-out lands has no perf gate. -- **Campaign impact:** regressions land silently. -- **Fix:** add a perf test that exercises `parseAndProjectDocumentationBundle → renderMarkdown` for at least 3 representative `documentType` values. Establish baseline before W-DOCS-1 lands. - -### Medium-priority items - -**M1 — `documentationView` perf metric only exercises `documentType: 'patterns'`** -- The other 11 (soon 18+) types have no gate. Campaign adds 25+ docs through new `DocDefinition`s. None will be measured. -- **Fix:** parameterize the perf test over `documentType`; one baseline per type. - -**M2 — Repeated filter passes in `src/projections/_shared/filter.ts`** -- Many projections call into shared filters that walk the graph each invocation. No memoization on filter result by `(graph_version, predicate_signature)`. -- **Campaign impact:** compounds linearly with `DocDefinition` count. -- **Fix:** add `WeakMap>` cache; invalidate on graph rebuild. - -**M3 — Perf baseline is anchored to commit `ee58aac` (initial multi-package split, ~year old)** -- The `× 1.5` ceiling is anchored to year-old numbers. ~50% slack against post-W1.5 reality. -- **Fix:** regenerate baselines on a clean post-W1.5 build before the campaign starts. Don't let the campaign inherit invisible headroom. - -**M4 — `documentation-types.ts:140-340` registry literal is re-evaluated on every module import** -- 200 LOC of object literals; `as const` keeps shape but each registry consumer pays the cost. Negligible alone, but the campaign adds many more consumers. -- **Fix:** part of the C1/C2 decomposition from Phase 1 — registry as data + small accessor functions. - -**M5 — `renderBlock` `default` arm has a silent megabyte-comment trap** -- See raw report. Not a production hazard; flagged for awareness. - -### Low-priority items - -L1–L5 — minor compounding allocations in `format-utils.ts`, `base.ts`, `render-json.ts`. See raw report. - -## Critical issues for Phase 3 context - -Phase 3 reviewers (testing + documentation) should give weight to: - -- **Perf gate coverage is the #1 testing gap** (H2 + M1). Phase 3 testing review must address: should the campaign land with parameterized `documentType` perf tests, and how should new `DocDefinition`s opt into the gate? -- **The 5 security invariants (I1–I5) need test-level enforcement.** Today they're documented-only. A regression test that calls each `parseAndProject*` with an open-shape payload and asserts rejection would lock I5. Test for `TRUSTED_MARKDOWN` import outside the renderer module would lock I3. -- **`render-markdown.ts` (2152 LOC) is under-tested for fragment-specific normalizers.** Phase 3 should map test coverage per normalizer; ContentFragment will add 6–10 more. -- **The 11 unreachable projections from INVENTORY** — Phase 3 doc review should determine whether they have feature-spec coverage (which would prove they're maintained) or are dead code (which the campaign should not preserve). -- **Documentation gap for invariants** — none of the 5 security invariants is captured in ADRs or per-module JSDoc. Phase 3 doc review should propose where to capture them so the campaign cannot accidentally violate them. diff --git a/.full-review/02a-security-raw.md b/.full-review/02a-security-raw.md deleted file mode 100644 index f2de265..0000000 --- a/.full-review/02a-security-raw.md +++ /dev/null @@ -1,159 +0,0 @@ -# Phase 2a — Security Audit: `packages/architect-projection/` - -## Summary verdict - -**No exploitable bugs in the current threat model. The markdown trust boundary is unusually well-implemented for a 2152-LOC renderer.** The package is a library that consumes already-validated PatternGraph data (architect-core trust boundary) and emits formatted output; it has no network surface, no auth, no I/O. Generic OWASP checks do not apply. - -The audit identified **2 low-severity defense-in-depth gaps** and **5 trust-boundary invariants** the doc-gen campaign must preserve as ContentFragment and DocDefinition route more content through these paths. Of those 5 invariants, **3 are load-bearing** — if the campaign relaxes them, dormant injection paths activate. - -## Findings - -### L1 — Code-block fence escalation is bounded at 4 backticks (CWE-1287, CWE-79-adjacent) - -**Severity:** Low (defense-in-depth) -**File:** `src/renderers/render-markdown.ts:1700-1702` - -```ts -case 'code': { - const fence = block.content.includes('```') ? '````' : '```'; - return [`${fence}${block.language ?? ''}`, block.content, fence, '']; -} -``` - -The renderer escalates to a 4-backtick fence only when content contains ` ``` ` (3 backticks). If `content` contains ` ```` ` (4 backticks), the closing fence matches the embedded sequence and downstream text is parsed as markdown. - -**Reproduction:** -```ts -code('````\nMALICIOUS \n````', 'js') -``` -emits: -```` -````js -```` -MALICIOUS -```` -```` - -Renderers interpreting this with a permissive markdown parser may treat `MALICIOUS …` as raw markdown / inline HTML. - -**Current threat model:** the only producer of code blocks from external text is `decision-records.internal.ts:296-313`, which extracts code spans from ADR markdown using regex `/```(\w*)\n([\s\S]*?)```/g`. The regex is non-greedy on the literal triple-backtick boundary, so it cannot itself capture a 4-backtick fence. Other call sites pass `stableStringify(value, 2)` or `buildFsmStateDiagram(fragment)` — internal, non-attacker-controlled. - -**Why it matters for the doc-gen campaign:** ContentFragment proposal routes hand-authored markdown (preambles) through the projection layer. If preamble parsing emits `code` blocks whose content originated from less-trusted sources (e.g., `_claude-md/` includes), the dormant path activates. The fix is to compute the required fence length dynamically. - -**Fix:** -```ts -function pickFence(content: string): string { - const longestRun = (content.match(/`{3,}/g) ?? []) - .reduce((max, run) => Math.max(max, run.length), 0); - return '`'.repeat(Math.max(3, longestRun + 1)); -} - -case 'code': { - const fence = pickFence(block.content); - return [`${fence}${block.language ?? ''}`, block.content, fence, '']; -} -case 'mermaid': { - const fence = pickFence(block.content); - return [`${fence}mermaid`, block.content, fence, '']; -} -``` - -The mermaid branch at line 1704 has the same bug at `\`\`\`` (3 backticks) without any escalation. Apply the same fix. - ---- - -### L2 — `CodeBlock.language` is unconstrained and is interpolated directly into the fence line - -**Severity:** Low (defense-in-depth) -**File:** `src/blocks/schema.ts:121` and `src/renderers/render-markdown.ts:1701` - -`language: z.string().optional()` accepts any string including newlines. The renderer interpolates it as `${fence}${block.language ?? ''}`. A `language` containing `\n` produces a fence line that ends prematurely; the next line of "language" becomes content from the renderer's perspective but the markdown reader sees it as the first content line. - -**Current threat model:** the only caller that supplies a non-static language is `decision-records.internal.ts:312`, where `language` is captured by `/(\w*)/` (alphanumeric + underscore only). Not exploitable today. - -**Why it matters for the doc-gen campaign:** any new caller that lets external text reach `code(content, language)` reopens this. The shape constraint belongs in the schema, not in caller discipline. - -**Fix:** -```ts -language: z - .string() - .regex(/^[A-Za-z0-9_+\-.]*$/u, 'language must be identifier-shaped') - .max(64) - .optional(), -``` - ---- - -## Invariants the campaign MUST preserve - -The following are not bugs — they are load-bearing properties of the current design. The doc-gen campaign cannot relax any of them without re-opening one of the holes audited above. - -### I1 — `sanitizeMarkdownLinkTarget` is the single chokepoint for all markdown link `href` values - -**File:** `src/renderers/render-markdown.ts:1893-1965` - -Every `link-out` → markdown link path is funneled through `toMarkdownLink` → `sanitizeMarkdownLinkTarget`. The sanitizer: - -- Trims and rejects empty / `//`-prefixed targets -- Decodes HTML entities (`:`, `:`, ` `, etc.) *before* scheme classification — defeats entity-encoded `javascript:` payloads -- Rejects control characters (U+0000–U+001F, U+007F) including tab, LF, CR after decoding -- Scheme allowlist: `http`, `https`, `mailto` only — everything else (`javascript:`, `data:`, `vbscript:`, `file:`) is rejected -- `encodeURI` + paren-escaping the accepted target - -`renderLinkOut` falls back to rendering plain text when the path is rejected — no dangling `[text]()` artifact. - -**Campaign action:** every new path that emits a clickable link must route through `toMarkdownLink` (or equivalent) and not template `[text](path)` directly. `ContentFragment` parsers in particular must not bypass this for parsed `[](…)` syntax in preambles — they should re-emit as `linkOut` blocks so the chokepoint applies. - ---- - -### I2 — `LinkOutBlockSchema.path` is `z.string()`; the URL discipline lives in the renderer, not the schema - -**File:** `src/blocks/schema.ts:136-140` - -The Zod schema accepts any string. Producers of link-out blocks rely on the renderer to sanitize. This is consistent with the rest of the pipeline — schemas validate shape, renderers validate format-specific safety. - -**Campaign action:** the `render-ui.ts` consumer in Studio does **not** apply scheme allowlisting (only `isExternalPath` heuristics for path rewriting at line 671). When `RenderableDocument` lands as a new top-level input alongside `Fragment`, ensure the UI renderer either runs the same sanitizer or that Studio's React layer applies its own `href` allowlist. This is the highest-priority campaign hardening — Studio is the only renderer where unsanitized `href` becomes a live DOM attribute. - ---- - -### I3 — Trusted-markdown construction is scoped to four call sites, all of which feed escaped substrate - -**File:** `src/renderers/render-markdown.ts:784, 799, 812, 844, 896, 1398, 1855-1873` - -The `TRUSTED_MARKDOWN` symbol bypasses `escapePlainMarkdownText`. Today every caller wraps content that was itself escaped (`escapePlainMarkdownText(x)` plus static template literals like `**Status:** ${...}`), so the bypass is sound. The symbol is module-private and not exported. - -**Campaign action:** do **not** export `trustedMarkdown` or any wrapper. ContentFragment normalizers must compose with `escapePlainMarkdownText` like the existing fragment-specific normalizers do. If a new normalizer needs the trusted path, it must keep the wrap site adjacent to the escape site in the same function. - ---- - -### I4 — JSON renderer rejects non-plain objects (CWE-1321 mitigation) - -**File:** `src/renderers/render-json.ts:163-166, 203-210` - -`isPlainObject` checks `Object.getPrototypeOf(value) === Object.prototype || null`. Class instances, `Map`/`Set`/`Date`, and prototype-polluted objects throw. Non-finite numbers, `bigint`, `function`, and `symbol` values throw with a JSON path for error attribution. - -**Campaign action:** when JSON becomes a campaign target output (`docs-live/` + `_claude-md/` + JSON multi-target), do not bypass these guards. Pre-stringified payloads — if the campaign adds them — should go through `transformValue` not direct `JSON.stringify`. - ---- - -### I5 — `parseAndProject` is the single Zod entry point for projection options - -**File:** `src/projections/_shared/parse-and-project.internal.ts` - -Every `parseAndProject*` wrapper threads raw caller options through one `parseAtBoundary(schema, …)` call. Inner projection code receives typed options and does no re-parsing. Zero `z.object()` usage in the package (113 `z.strictObject` uses) means cross-package contracts reject unknown fields. - -**Campaign action:** the proposed `DocDefinition.build(graph)` API must define its options/inputs as `z.strictObject` schemas and route through `parseAndProject` rather than introducing a parallel "raw config" path. Phase 1 H1 already flags that `documentation-types.ts` derives types from a literal rather than from the schema; if the campaign perpetuates this for `DocDefinition`, schema-drift is guaranteed and an extra-field smuggling path opens at the new boundary. - ---- - -## Other observations (informational) - -- **Dependency hygiene:** runtime deps are exactly `@libar-dev/architect-core` (workspace) and `zod ^4.1.11`. No drift, no bloat. -- **No `eslint-disable*`, no `@ts-ignore`, no `@ts-expect-error`** in `src/`. The no-BC doctrine is upheld at the lint/type layer. -- **Three `as` casts in source** (`schema.ts:172`, `render-markdown.ts:1716-1717`). All three are interior shape-narrowing that does not cross a trust boundary — the table-cell casts widen `string[][]` to `MarkdownText[][]` (a superset, since `MarkdownText` includes plain `string`), and trusted-vs-plain dispatch happens downstream in `renderMarkdownText`. -- **`markdown-paths.ts` path construction** is fed by `LogicalRouteId` strings that match a regex (`[A-Za-z0-9][A-Za-z0-9_-]*` segments, see `fragments/base.ts:74`). `slugForFilename` collapses anything else to `[a-z0-9-]`. No traversal risk in produced filenames. Phase 1 H4's complaint about hardcoded doc-type strings is an architecture concern, not a security one — the route IDs are still bounded. -- **`
`/`` HTML emission** is the only raw HTML in the markdown pipeline (plus `
` inside table cells). `` content goes through `renderMarkdownText` → `escapeHtml`, so `` injection is blocked. `escapeTable​Cell` replaces `\n` with literal `
` after `escapeHtml` has already neutralized `<`, so attacker-supplied `
` cannot reach the output. - -## Top recommendation - -Apply the two-line fix at L1 (dynamic fence length for code and mermaid blocks) and the regex constraint at L2 (`CodeBlock.language`) before the doc-gen campaign opens the code-block path to less-trusted content. Everything else in this package is already at the right altitude for a renderer of this size — the audit's load-bearing output is the **5 invariants the campaign must preserve**, not new findings. diff --git a/.full-review/02b-performance-raw.md b/.full-review/02b-performance-raw.md deleted file mode 100644 index 8d87c6b..0000000 --- a/.full-review/02b-performance-raw.md +++ /dev/null @@ -1,320 +0,0 @@ -# Phase 2b — Performance review (architect-projection) - -Scope: campaign-readiness perf of `packages/architect-projection/`. Generic web-perf concerns excluded by scope. - -## Top-line verdict - -The projection + JSON-render pipeline is **healthy at 8× load and headroom is large**. Today's baseline (40-iter avg from `tests/perf/baselines/business-rule-set.baseline.json`): - -- `parseAndProjectBusinessRuleSet` end-to-end project: avg 1.17 ms, p50 0.54 ms (budget 1.5 ms) -- `renderJson` (object): avg 0.44 ms (budget 1 ms) -- `renderJson` (pretty): avg 0.76 ms (budget 5 ms) -- All 7 hot-path projections sit at 0.01–0.22 ms avg against an 8 ms budget -- `graphBuild`: 291 ms p50 against a 2000 ms budget (single dominant cost — but fixed per `docs:all` run, not per doc) - -The 36-pattern / 108-rule fixture exercises the projection layer well. **The campaign will not bust these projection budgets even at 5× doc fan-out**, because the projection cost is bounded by graph size (constant), not doc count. - -**However**, the gate has a critical coverage gap (M1) — it does not measure `renderMarkdown` of a documentation bundle end-to-end, and the OUTPUT-side splitter (`splitOversizedDocument`) does redundant rendering (H1) that the campaign's larger doc count will multiply. Two genuine fix-before-campaign items, two should-fix-soon items, the rest is monitoring/scaffold guidance. - -## Measurements I ran - -1. `wc -l` on `render-markdown.ts` → **2152 lines**, with **27 top-level `normalize*` / `render*` functions** (`grep -cE "^function normalize|^function render[A-Z]"`). -2. `JSON.stringify` call sites in `src/**/*.ts`: **5 total** — one in `format-utils.ts:39` (used by `stableStringify`), one in `render-json.ts:59` (the pretty path), one in `render-markdown.ts:1710` (only the "unknown block" diagnostic), and two in places measuring or path-encoding. **No hot-path deep clones via `JSON.parse(JSON.stringify(...))` anywhere.** -3. `findPatternByName` caches via `graph.nameIndex: Map` in core (`packages/architect-core/src/read-api/pattern-helpers.ts:77`); `getCanonicalRelationshipIndex` uses a WeakMap keyed on the graph. **Cross-projection memoization already exists at the core layer.** -4. `filterPatterns(patterns, undefined)` returns `[...patterns]` (`src/projections/_shared/filter.ts:25`) — a full shallow clone on the no-filter path, called 20+ times per `docs:all`. - ---- - -## Findings - -### H1 — `addRoutedDocument` renders each output document 2N+1 times when splitting kicks in -**Severity:** High -**File:** `src/renderers/render-markdown.ts:308–325, 447–466, 2054–2103` - -`addRoutedDocument` first calls `shouldSplit`, which calls `renderDocument(document, options)` (line 464) purely to count newlines. If the doc trips the budget, `splitOversizedDocument` is invoked — that function calls the `renderFn` callback once per H2 sub-section to count its lines (line 2078), THEN `addRoutedDocument` calls `renderDocument` a final time for the parent (line 320) plus once for every kept sub-file (line 323). - -**Estimated impact:** For a single doc that splits into `N` sub-files: `1 (shouldSplit) + N (line 2078 sub-line-count) + 1 (parent) + N (subFiles) = 2N + 2` renders, where ~`N + 1` is the minimum. For a `requirements-executable` bundle with ~10 H2 groups that's ~22 renders vs 11 minimum — **~2× wasted work in the renderer per oversized doc.** The campaign expects more docs to hit size budgets (a 40-doc target with disclosure-driven fan-out), so this scales linearly with the new doc count. - -**Why it matters for the campaign:** The doc-gen step is the only doc-count-sensitive part of the pipeline; this is exactly where 5× will land hardest. - -**Fix:** Track line counts during the first render and reuse them: - -```ts -function addRoutedDocument(entries, basePath, document, options): void { - const rendered = renderDocument(document, options); - const lineCount = rendered.split('\n').length; - if (!shouldSplitFromLineCount(lineCount, basePath, options)) { - addUniqueEntry(entries, basePath, rendered); - return; - } - // splitOversizedDocument receives pre-rendered groups + line counts; - // it no longer needs renderFn for measurement. - const splitResult = splitOversizedDocument(document, options.sizeBudget!, basePath, options); - addUniqueEntry(entries, basePath, renderDocument(splitResult.parent, options)); - for (const [path, sub] of Object.entries(splitResult.subFiles)) { - addUniqueEntry(entries, path, renderDocument(sub, options)); - } -} -``` - -A cheaper alternative: render with a `measureOnly: true` flag returning a precomputed line count without producing the string. Worst-case complexity drops from O(2N+2) to O(N+1). - ---- - -### H2 — Perf gate has zero coverage of `renderMarkdown` + bundle routing + splitter -**Severity:** High -**File:** `tests/features/perf/business-rule-set-report.steps.ts:608–658`, `tests/perf/compare-baseline.mjs:12–28` - -The gate measures `parseAndProjectDocumentationBundle({ documentType: 'patterns' })` (line 628–635, "documentationView" hot path) but **never calls `renderMarkdown` on the bundle**. The Markdown renderer is by far the most complex code in the package (2152 LOC, 27 normalizer/renderer functions) and the place where the campaign's per-doc fan-out lands. The "renderObject"/"renderPretty" budgets cover JSON only. - -**Estimated impact:** Today, `renderMarkdown` of a bundle is unmeasured. If a future refactor regresses a fragment normalizer (e.g., a quadratic table-width pass) by 10×, the gate will not catch it. Pair this with H1 above (2× rendering wasted on splits) and the campaign's larger doc count amplifies whatever regression slips through. - -**Why it matters for the campaign:** The campaign explicitly multiplies the area the gate doesn't cover. - -**Fix:** Add `renderMarkdownDocumentationBundle` as a 4th top-level metric in `business-rule-set-report.steps.ts`, with a budget (suggested 15 ms avg for the 36-pattern fixture): - -```ts -renderMarkdownBundle: measureProjection( - context, - (ctx) => { - const bundle = parseAndProjectDocumentationBundle(ctx, { documentType: 'patterns' }); - return renderMarkdown(bundle, { routeProfile: defaultMarkdownRouteProfile }); - }, - hotPathIterations -), -// And inside the run loop, measure for at least 3 documentationTypes -// ('patterns', 'requirements-executable', 'roadmap') — the three with the -// most fragment variety. Add same budgets to HOT_PATH_BUDGETS in -// tests/perf/compare-baseline.mjs. -``` - -Extend to at least 3 representative documentation types so the table-rendering and bundle-children paths are both exercised. - ---- - -### M1 — Documentation-bundle perf gate uses only one document type ('patterns') -**Severity:** Medium -**File:** `tests/features/perf/business-rule-set-report.steps.ts:628–635` - -The "documentationView" hot path only exercises `documentType: 'patterns'`. The 12 supported documentation types route through different projection compositions; `requirements-executable`/`traceability`/`taxonomy` each touch different fragment combinations. The campaign will add 6+ new doc types — none will land on the gate by default. - -**Estimated impact:** Hidden regression budget. A change that doubles `projectTraceabilityMatrix` time slips by silently. - -**Why it matters for the campaign:** Newly-added doc types ship without perf-gate coverage until someone remembers to wire them in. - -**Fix:** Parameterise the hot-path table over `SUPPORTED_DOCUMENTATION_TYPE_REGISTRY` and bake per-type budgets. Either: -- Loop the 12 (soon 18+) types and store one budget keyed by document-type, or -- Pick 4 representative types (`patterns`, `requirements-executable`, `roadmap`, `taxonomy`) and assert each. - -The second is cheaper and stays representative. - ---- - -### M2 — `filterPatterns(patterns, undefined)` allocates a fresh shallow clone every call -**Severity:** Medium -**File:** `src/projections/_shared/filter.ts:22–28` - -```ts -return filter === undefined - ? [...patterns] - : patterns.filter((pattern) => filterPattern(pattern, filter)); -``` - -The no-filter branch unconditionally clones. The function is invoked from 20+ projection sites (grep above). For a 36-pattern fixture that's <1 µs each, but at the post-campaign scale (~200+ patterns × ~40 docs × ~3 filter calls per projection) it sums to 24k allocations per `docs:all`. - -**Estimated impact:** Negligible today (~1 ms total); a measurable but not gate-busting cost at scale. Mostly a GC-pressure cleanup. - -**Why it matters for the campaign:** Won't bust the gate, but compounds with new extractors each adding their own filter calls. - -**Fix:** Return the original array on the no-filter path. All call sites treat the result as readonly: - -```ts -export function filterPatterns( - patterns: readonly ExtractedPattern[], - filter: ProjectionFilter | undefined -): readonly ExtractedPattern[] { - return filter === undefined ? patterns : patterns.filter((p) => filterPattern(p, filter)); -} -``` - -(Signature change from `ExtractedPattern[]` → `readonly ExtractedPattern[]` will surface any caller that was mutating — code-quality bonus.) - ---- - -### M3 — Perf baseline last refreshed at the initial multi-package split commit -**Severity:** Medium -**File:** `tests/perf/baselines/business-rule-set.baseline.json` - -`git log --oneline -1` on the baseline → `ee58aac chore: initial multi-package layout`. The baseline pre-dates every W1.5 change. The `baseline × 1.5` gate is currently anchored to numbers from the package's first stable state, not the current state. - -**Estimated impact:** Hard-budget HARD_BUDGETS (1.5 ms for `project`, etc.) still bound the gate, so silent drift is limited to `1.5×` of the original baseline. But that 1.5× window has been frozen for the entire post-W1.5 development cycle — any optimisations made since don't tighten the gate, and any regressions within 1.5× went uncaught. - -**Why it matters for the campaign:** Refreshing the baseline before the campaign starts gives the gate a tighter anchor, so post-campaign regression detection has real signal rather than 50% slack from a year-old baseline. - -**Fix:** Run `pnpm --filter @libar-dev/architect-projection test:perf` on a stable build, copy the report → `tests/perf/baselines/business-rule-set.baseline.json`, commit alongside the campaign kickoff. Add a quarterly cadence (or a `scripts/refresh-perf-baseline.mjs` glue) so the baseline doesn't ossify again. - ---- - -### M4 — Documentation-type lookups are O(N) linear scans, called from the renderer hot path -**Severity:** Medium -**File:** `src/projections/documentation-composition/documentation-types.ts:379–385`, `src/renderers/render-markdown.ts:413` - -```ts -export function getDocumentationTypeMetadata(key: string) { - return SUPPORTED_DOCUMENTATION_TYPE_REGISTRY.find((entry) => entry.key === key); -} -``` - -`renderMarkdown` calls this once per bundle (line 413). With 12 entries today, it's ~12 string compares — negligible. But the campaign roadmap adds 6+ new types and ContentFragment composition may call lookups multiple times per document. - -**Estimated impact:** ~0.001 ms today; possibly 0.01 ms with 40 entries × multiple calls per render. Not a gate-buster. - -**Why it matters for the campaign:** Cheap to fix now; expensive once the lookup pattern proliferates. - -**Fix:** Build a `Map` at module load: - -```ts -const SUPPORTED_BY_KEY = new Map(SUPPORTED_DOCUMENTATION_TYPE_REGISTRY.map((e) => [e.key, e])); -export function getDocumentationTypeMetadata(key: string) { return SUPPORTED_BY_KEY.get(key); } -``` - -Same for `DROPPED_DOCUMENTATION_TYPE_REGISTRY`. Bonus: removes the need to filter `'dropped'` entries at lookup time once they're deleted per H2 in Phase 1. - ---- - -### M5 — `splitOversizedDocument` size budget is a line count, not a byte count -**Severity:** Medium -**File:** `src/renderers/render-markdown.ts:447–466`, `2054–2103` - -The split decision is `rendered.split('\n').length > options.sizeBudget`. Tables, mermaid blocks, and code fences emit many lines per "thing" — meaning the policy is line-skewed, not content-skewed. More importantly, the `split('\n')` builds another full array of strings just to count them. - -**Estimated impact:** Minor allocation overhead per oversized doc (one array of `lineCount` strings, discarded). Combined with H1 (2N+2 renders), the wasted `split` arrays add up — but only on the hot oversized path. - -**Why it matters for the campaign:** Same hot path as H1; both fix-together. - -**Fix:** Count newlines without allocating: - -```ts -function countLines(s: string): number { - let n = 1; - for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) n++; - return n; -} -``` - -Folds naturally into the H1 fix (return `{ rendered, lineCount }` from a single helper). - ---- - -### L1 — `stableStringify` deep-clones values before stringifying -**Severity:** Low -**File:** `src/_internal/format-utils.ts:22–40`, used at `render-markdown.ts:1106, 1115` - -`stableStringify` builds a full deep-sorted clone via `sortValue` (which allocates new arrays/objects at every level), then `JSON.stringify`s. Called only from `normalizeGenericFragment` for unknown-shape values landing in fragments. Today's reachable cases are small (config dumps, debug blocks). - -**Estimated impact:** Negligible today; warrants attention only if ContentFragments start emitting larger arbitrary-shape payloads. - -**Why it matters for the campaign:** Worth a watch-item — if ContentFragments emit large embedded JSON blobs via the generic fragment path, this allocates 2× the embedded size. - -**Fix (only if measured):** Use a `replacer` function on `JSON.stringify` that sorts keys at serialization time — single pass, no intermediate clone: - -```ts -export function stableStringify(value: unknown, indent?: number): string { - return JSON.stringify(value, (_, v) => (isPlainObject(v) ? sortKeys(v) : v), indent); -} -``` - -Defer until profiled. - ---- - -### L2 — `isBundle` runs a regex on every child route ID + a full `Object.values` walk -**Severity:** Low -**File:** `src/fragments/base.ts:21–39, 71–76` - -`isBundle` is the runtime discriminator between `Fragment` and `ProjectionBundle`. It walks every child fragment, validates routing if present (regex-test each route ID). Today the perf gate reports p50 ~2.6 µs per call — well under the 50 µs budget. But it's called twice per doc-gen (once by `renderMarkdown`, once by `renderJson`) and the regex `/^([A-Za-z0-9][A-Za-z0-9_-]*)(:([A-Za-z0-9][A-Za-z0-9_-]*)){1,3}$/u` is not cheap. - -**Estimated impact:** Below noise today. Will scale linearly with `children.length`; ContentFragment composition may grow children. - -**Why it matters for the campaign:** Monitoring item only. If `isBundle` p50 starts approaching 25 µs (50% of budget) after the campaign lands, switch to a sentinel: - -```ts -const BUNDLE_TAG = Symbol.for('@libar-dev/architect-projection/bundle'); -// projectSingle and friends set bundle[BUNDLE_TAG] = true -// isBundle becomes: typeof value === 'object' && value !== null && BUNDLE_TAG in value -``` - -Defer. - ---- - -### L3 — `JSON.stringify` deep-walks the entire serialised tree twice in the pretty path -**Severity:** Low -**File:** `src/renderers/render-json.ts:53–60` - -```ts -const payload = isBundle(input) ? serializeBundle(input, opts) : serializeFragment(input, opts, '$'); -return resolvedOptions.pretty ? JSON.stringify(payload, null, 2) : payload; -``` - -The `serialize*` helpers walk the input tree and produce a plain-object copy (which the gate measures at 0.44 ms avg, "renderObject"). The pretty path then `JSON.stringify`s that copy — that's a second full walk, taking the avg to 0.76 ms. - -**Estimated impact:** The current 0.32 ms delta between renderObject and renderPretty is the second walk. Not a hot path; well under the 5 ms budget. - -**Why it matters for the campaign:** Informational only. Worth recording as the largest avoidable cost in the JSON renderer if pretty becomes a default at scale. - -**Fix (only if pretty becomes hot):** Stream stringify during the first walk (small custom serializer), or accept the doubled cost. - ---- - -### L4 — Stable key ordering allocates a fresh sorted array for every object during JSON serialization -**Severity:** Low -**File:** `src/renderers/render-json.ts:178, 189–194` - -`transformObject` calls `orderEntries(Object.entries(value), stableKeyOrder)` per object. `orderEntries` always materialises `[...entries].sort(...)` even when the input is already in stable order (which it typically is — fragment objects have fixed key order from their Zod schemas). - -**Estimated impact:** ~0 cost today (gate baseline is 0.44 ms for full JSON serialization). Compounds linearly with bundle size. - -**Why it matters for the campaign:** Monitoring item; gate covers it. - -**Fix (only if profile shows it):** Skip the sort when the input is already sorted (single linear check). Or accept it as part of stability-by-design. - ---- - -### L5 — `renderBlock` `default` branch JSON-stringifies the block for the diagnostic comment -**Severity:** Low -**File:** `src/renderers/render-markdown.ts:1710` - -```ts -return [``, '']; -``` - -If a future `BlockSchema` extension lands without a corresponding `renderBlock` arm, the fallback `JSON.stringify`s the whole block — which could be a 100KB nested object — every render. Today's discriminated-union shape makes this unreachable, but if ContentFragments emit a block kind that's added to the schema but not the renderer, doc generation will silently embed huge HTML comments. - -**Estimated impact:** Currently unreachable. If triggered: potentially several MB of HTML comments per doc. - -**Why it matters for the campaign:** ContentFragment work will add block types; the asymmetric add (schema-only) becomes a real risk. - -**Fix:** Throw instead of producing a diagnostic comment that's silently shipped to disk: - -```ts -default: { - const exhaustive: never = block; - throw new Error(`renderBlock: unhandled block kind: ${(exhaustive as { type: string }).type}`); -} -``` - -`never`-exhaustiveness check makes this a compile-time error when a new block type is added without a render arm — better than runtime silent megabyte comments. - ---- - -## Summary for parent agent - -The projection pipeline is structurally healthy and will not bust the perf gate on projection time alone. The two real campaign-relevant items are: - -1. **H1 — `addRoutedDocument` does 2N+2 renders when documents split** — fixing this halves rendering work on the exact path the campaign will multiply. -2. **H2 — perf gate has no `renderMarkdown` coverage** — the 2152-LOC renderer is unmonitored; the campaign lands there. Add a bundle-Markdown metric before W-DOCS-1. - -Two MediumPlus follow-ups: refresh the stale baseline (M3) and parameterise the gate over more documentation types (M1). Everything else is informational scaffolding for post-campaign tuning. - -**Will the campaign bust the perf gate?** No — projection cost is graph-size-bounded, not doc-count-bounded. But the gate doesn't measure the multiplied path. Fix H1 + H2 and the campaign's 5× fan-out will be observable and bounded; skip them and any renderer regression introduced during the campaign goes undetected. diff --git a/.full-review/03-testing-documentation.md b/.full-review/03-testing-documentation.md deleted file mode 100644 index 40506b6..0000000 --- a/.full-review/03-testing-documentation.md +++ /dev/null @@ -1,107 +0,0 @@ -# Phase 3: Testing & Documentation Review - -Raw reports: `03a-testing-raw.md`, `03b-documentation-raw.md`. - -## Headline - -**Coverage is broad but campaign-critical paths are unlocked, and the docs that exist are accurate but silent about the invariants the campaign must preserve.** - -Both reviews converged on a single structural finding: the 2152-LOC `render-markdown.ts` has wide *smoke* coverage but narrow *behavioral* coverage, and zero of the Phase 2 security invariants are captured in either tests OR JSDoc. The campaign will land new code through these paths and find them undocumented + under-tested. The fixes are cheap; doing them before W-DOCS-1 is high-leverage. - -Notable positive finding: **the 7 unreachable projections (no doc-gen, no CLI/MCP exposure) all have behavioral feature specs** — they're alive, not dead. The campaign should plan to surface them, not delete them. - -## Testing findings - -### Coverage matrix headline -- **35 / 43 projections** have at least one feature spec -- **10 / 10 markdown normalizers** have smoke-level rendering validation -- **4 / 10 markdown normalizers** have dedicated behavioral scenarios -- **0 / 5 security invariants** have complete test-level enforcement -- **0 perf-gate coverage** of `renderMarkdown` end-to-end -- **1 / 12 document types** measured by the perf gate (`patterns` only) - -### Critical findings - -**T-C1 — No `renderMarkdown` perf gate** -- The 2152-LOC renderer is unmeasured. Campaign multiplies doc-count 5×. -- Regressions in `normalizeBusinessRuleSet`, `normalizeRequirementDigest`, and `splitOversizedDocument` will land silently. -- **Fix:** add `renderMarkdown` hot-path metrics for at least `business-rules`, `requirements-executable`, and `patterns` before W-DOCS-1. -- (Confirms Phase 2 H2 with concrete file evidence.) - -**T-C2 — Security invariants I1–I5 are documented-only, zero test enforcement** -- I1 tests `javascript:` rejection but not `data:` rejection in `sanitizeMarkdownLinkTarget`. -- I2 (UI renderer's intentional URL passthrough) has no test locking the invariant. -- I3 (`TRUSTED_MARKDOWN` module-private) has no lint/test preventing import elsewhere. -- I4 (prototype-pollution guard in `isPlainObject`) has no test. -- I5 (`parseAndProject` rejects extra unknown properties via `z.strictObject`) has no test. -- **Fix:** add rejection tests for I5 (extra-property payload) and I4 (custom-prototype payload) before ContentFragment routes new code through these paths. I3 can be enforced via an ESLint `no-restricted-imports` rule. - -### High-priority findings - -**T-H1 — `SectionedDocumentFixture` test hack hides normalizer omission** -- Many `render-markdown` test scenarios cast `ProjectConfigSnapshot` as a fake Fragment to exercise the canonical-blocks path. A new ContentFragment normalizer accidentally left out of `MARKDOWN_NORMALIZERS` would pass every existing test. -- **Fix:** add a compile-time `satisfies Record` check on the `MARKDOWN_NORMALIZERS` table. Forces TS to flag any missing entry. - -**T-H2 — Perf gate only exercises `documentType: 'patterns'`** -- 11 other types, including the structurally-heavier `traceability` and `requirements-executable`, are unmeasured. Campaign adds 25+ doc types. -- **Fix:** parameterize the `documentationView` perf measurement before W-DOCS-1. One baseline per type. - -**T-H3 — 6 of 10 markdown normalizers have only smoke-level coverage** -- `normalizeArchitectureDiagram`, `normalizeDecisionCatalog`, `normalizeDecisionRecord`, `normalizeTaxonomyDigest`, `normalizeTraceabilityMatrix`, `normalizeValidationRuleDigest` validated only by "no-throw + non-empty output." -- Campaign adds new normalizer peers alongside these. New normalizers will be even less covered if peer signal is "smoke is enough." -- **Fix:** one structural scenario per normalizer (assert specific heading or section content) before W-DOCS-2. - -### Unreachable-projection verdict -**Not dead code.** All 7 `❌❌` projections in INVENTORY have behavioral feature specs. They are alive but unsurfaced; the campaign should treat them as `DocDefinition` targets, not deletion candidates. - -## Documentation findings - -### Critical findings - -**D-C1 — Security invariants I1–I5 documented nowhere in the source** -- `sanitizeMarkdownLinkTarget`, the UI renderer's intentional passthrough, `TRUSTED_MARKDOWN`, `isPlainObject`'s prototype guard, `parseAndProject`'s `z.strictObject` discipline — none of these have JSDoc explaining them. -- Campaign authors writing `composeDoc` and `ContentFragment.build()` will route new content through these paths without knowing the invariants. -- **Fix:** JSDoc blocks on 5 functions/constants in `render-markdown.ts` and `render-json.ts`. Single session of work. - -**D-C2 — Zero `.describe()` calls across all 135 source files** -- DEEP-DIVE's headline worked example (`extractZodSchemaFields('ProgressiveDisclosurePolicySchema')` producing the disclosure table) fails silently — returns empty — until `.describe()` is added to the 13 fields these schemas expose. -- **Highest-impact campaign-readiness finding.** The campaign's most prominent demo doesn't work today. -- **Fix:** add `.describe()` to `ProgressiveDisclosurePolicySchema`, `DisclosureSpecSchema`, and the disclosure enum schemas before W-DOCS-1 ships the new extractor. Otherwise the kitchen-sink demo produces an empty table. - -### High-priority findings - -**D-H1 — All 4 renderer `### When to Use` stubs carry boilerplate copied from contract files** -- Says "As a typed contract / data shape consumed by projection or render layers." Factually wrong for renderers. -- Makes `extractJSDocProse()` + planned `@architect-renderer` tag pattern useless on the 4 entry points. -- **Fix:** lift the accurate "Renderer Overview" section from `docs/MIGRATION.md` (150 lines) into per-renderer JSDoc. - -**D-H2 — `DOCUMENTATION_PROJECTION_FACTORIES` table has no contributor signaling** -- The table the campaign's W-DOCS-1 will DELETE has no "do not add entries here" comment and no pointer to the replacement design. -- Most common campaign-contributor mistake will be extending it. 4-line block comment prevents this. -- **Fix:** add JSDoc citing `.pr-coordination/PROPOSED-DESIGN.md` + a TODO marker. - -**D-H3 — `DisclosureSpec`, `LogicalRouteId`, `ContentRichness` enum values undocumented** -- The three types ContentFragment authors will use on every invocation. No JSDoc anywhere. -- Campaign authors in W-DOCS-2d must trace 2152 LOC of renderer logic to understand `emitChildren`, `richness`, route ID formats. -- **Fix:** JSDoc on each, with a worked example referencing the `RenderMarkdownOptions.disclosureLevel` consumer site. - -### Medium-priority findings (cited from raw report — not duplicated) - -D-M1 through D-M5: incomplete docs for `addRoutedDocument`, missing `@architect-pattern` on `blocks/schema.ts` and `fragments/base.ts`, README disclosure table drift, no root-level v1→v2 `MIGRATION.md`, perf-gate gap not noted in PERF.md. None block campaign start. - -## Cross-cutting observation - -The package documents its **shapes** (types, schemas) but not its **invariants** (what must remain true across changes). Phase 2's security audit derived 5 invariants by reading code, not comments. The doc-gen campaign is the right moment to capture these invariants as JSDoc + executable assertions — both because the campaign needs them, AND because the campaign's own generators will then surface them in the auto-generated docs. - -This is the dogfooding loop: invariants captured in JSDoc → extracted by `extractJSDocProse` → rendered in `docs-live/` reference docs → reviewed by anyone touching the code → corruption detected by the same gate that generates the docs. - -## Critical issues for Phase 4 context - -Phase 4 reviewers (framework practices + CI/CD) should give weight to: - -- **The barrel audit script** (`scripts/options-schema-barrel-audit.mjs`, run as `test:barrel-audit` ahead of typecheck) — what's it enforcing? Is it relevant to the campaign's `DocDefinition` API addition? -- **Perf gate as CI artifact** — Phase 3 confirmed the gate exists but is narrow. Phase 4 should look at how the gate runs in CI: stability of measurement environment, baseline regeneration cadence, failure surfacing. -- **`.describe()` discipline as a build-time check** — the campaign needs Zod schema fields with `.describe()` to generate doc tables. Could this be enforced by an ESLint rule + Zod-schema scanner? -- **JSDoc tag discipline** — `@architect-*` annotations are doctrine but not lint-enforced. Phase 4 should look at whether step-lint or a similar tool checks them; if not, the campaign is one renamed file away from undetected drift. -- **ESM packaging** — the `exports` map has 5 sub-entries. Phase 4 should confirm the build emits matching `.d.ts` + `.js` for each, and that the `prepack` script catches drift. -- **Test parallelism / wall clock** — vitest-cucumber suite size, perf-gate run frequency. The campaign will add tests; Phase 4 should flag any structural test-time bottleneck. diff --git a/.full-review/03a-testing-raw.md b/.full-review/03a-testing-raw.md deleted file mode 100644 index 11c53ac..0000000 --- a/.full-review/03a-testing-raw.md +++ /dev/null @@ -1,382 +0,0 @@ -# Phase 3a: Test Coverage & Test Quality Review - -Reviewed: `packages/architect-projection/` test suite against the doc-generation consolidation campaign. - -## Headline - -**35 / 43 projections (81%) have feature coverage; 6 / 10 fragment-specific markdown normalizers have dedicated behavior scenarios.** The campaign has a solid behavioral floor, but has two campaign-blocking gaps: `renderMarkdown` has no perf gate whatsoever (H2 from Phase 2 confirmed), and the 5 security invariants are documented-only with zero test-level enforcement. A third concern — the `SectionedDocumentFixture` hack in render-markdown steps — will silently corrupt the new-normalizer test strategy the campaign needs. - ---- - -## Coverage Matrix — 43 Projections - -Legend: **Has-Feature** = at least one feature spec file imports/calls the function. **Has-Perf** = measured in `compare-baseline.mjs` gate. - -| # | Function | Has-Feature | Has-Perf | Notes | -|---|---|---|---|---| -| 1 | `projectArchitectureComparison` | Y | N | smoke only (renderer-smoke) | -| 2 | `projectBoundedContext` | Y | N | parity + reporting | -| 3 | `projectArchitectureNeighborhood` | Y | N | architecture-neighborhood.feature | -| 4 | `projectDependencyEdges` | Y | N | dependency-edges.feature | -| 5 | `parseAndProjectPatternBundle` | **N** | N | no direct test; options-validation path untested | -| 6 | `projectPatternBundle` | Y | N | pattern-bundle.feature | -| 7 | `parseAndProjectDependencyTree` | Y | N | dependency-tree.feature | -| 8 | `projectDependencyTree` | **N** | N | only called by #7; raw function untested | -| 9 | `parseAndProjectOpenQuestionList` | **N** | N | no direct test; options-validation path untested | -| 10 | `projectOpenQuestionList` | Y | N | open-question-list.feature | -| 11 | `projectOrphanPatternList` | Y | N | dependency-tree.feature | -| 12 | `parseAndProjectPatternCatalog` | Y | N | pattern-bundle.feature | -| 13 | `projectPatternCatalog` | **N** | N | called only via #12 wrapper | -| 14 | `projectPatternDetail` | Y | N | pattern-detail.feature | -| 15 | `projectPatternSummary` | Y | N | parity + smoke | -| 16 | `parseAndProjectBusinessRuleSet` | Y | Y (JSON only) | 7 feature files | -| 17 | `projectBusinessRule` | Y | N | governance tests | -| 18 | `projectBusinessRuleSet` | Y | N | governance tests | -| 19 | `projectDecisionCatalog` | Y | N | decision-records.feature | -| 20 | `projectDecisionRecord` | Y | N | decision-records.feature | -| 21 | `parseAndProjectTaxonomyDigest` | Y | N | validation-taxonomy.feature | -| 22 | `projectTaxonomyDigest` | Y | N | validation-taxonomy.feature | -| 23 | `projectValidationRuleDigest` | Y | N | validation-taxonomy.feature | -| 24 | `projectAnnotationCoverage` | Y | Y (hot-path) | reporting.feature + perf | -| 25 | `projectOverviewDigest` | Y | N | reporting.feature + smoke | -| 26 | `projectRequirementDigest` | Y | Y (hot-path) | reporting.feature + perf | -| 27 | `projectRequirementExecutableDigest` | Y | Y (hot-path) | reporting + parity + perf | -| 28 | `projectRequirementSpecsDigest` | Y | N | reporting.feature | -| 29 | `projectRoleProfile` | Y | N | reporting.feature | -| 30 | `projectRoleProfiles` | Y | N | reporting.feature | -| 31 | `projectSourceInventoryDigest` | Y | N | reporting.feature | -| 32 | `projectTagUsage` | Y | N | reporting.feature | -| 33 | `projectPhaseProgress` | Y | N | smoke + phase-progress-status | -| 34 | `projectStatusDistribution` | Y | N | smoke + status-distribution | -| 35 | `projectRoadmapTimeline` | Y | N | roadmap-timeline + roadmap-markdown | -| 36 | `projectCompletedMilestones` | Y | N | roadmap-timeline.feature | -| 37 | `projectCurrentWork` | Y | N | roadmap-timeline.feature | -| 38 | `projectReleaseNotesDigest` | Y | N | release-notes.feature | -| 39 | `projectTraceabilityMatrix` | Y | N | traceability-matrix.feature | -| 40 | `projectDeliverable` | Y | N | smoke only (renderer-smoke) | -| 41 | `projectDeliverableManifest` | Y | N | smoke only (renderer-smoke) | -| 42 | `parseAndProjectFileReadingList` | Y | N | context-session.feature | -| 43 | `projectFileReadingList` | **N** | N | called only by #42 wrapper | -| 44 | `parseAndProjectHandoffRecord` | Y | N | context-session.feature | -| 45 | `projectHandoffRecord` | **N** | N | called only by #44 wrapper | -| 46 | `parseAndProjectScopeReadinessReport` | Y | Y (hot-path) | smoke + context-session + perf | -| 47 | `projectScopeReadinessReport` | **N** | N | called only by #46 wrapper | -| 48 | `parseAndProjectSessionContext` | Y | Y (hot-path) | 4 feature files + perf | -| 49 | `projectSessionContextBundle` | Y | N | smoke | -| 50 | `parseAndProjectArchitectureDiagram` | **N** | N | options-validation path untested; `projectArchitectureDiagram` IS tested | -| 51 | `projectArchitectureDiagram` | Y | N | config-documentation.feature | -| 52 | `parseAndProjectConfig` | Y | N | config-documentation.feature | -| 53 | `projectConfig` | Y | N | smoke only | -| 54 | `parseAndProjectDocumentationBundle` | Y | Y (hot-path, patterns-only) | 4 feature files; perf only exercises `patterns` type | -| 55 | `projectDocumentationBundle` | Y | N | smoke + config | -| 56 | `parseAndProjectPrChangeReview` | Y | N | config-documentation.feature | -| 57 | `projectPrChangeReview` | Y | N | smoke only | - -**Totals (INVENTORY's canonical 43):** 35 / 43 have feature coverage. 8 have none. -**Perf gate:** 7 hot-path projections measured. `renderMarkdown` is not measured for any of them. - ---- - -## Invariant Lock Status - -| ID | Invariant | Test Exists? | File / Gap | -|---|---|---|---| -| **I1** | `sanitizeMarkdownLinkTarget` rejects javascript:, data:, control chars | **Partial** | `render-markdown.feature.steps.ts` tests `javascript:` scheme in 10+ places; `data:` and `vbscript:` are NOT tested. The allowlist is enforced but the full rejection surface is not locked. | -| **I2** | UI renderer does NOT sanitize URLs (intentional) | **N** | `render-ui.feature` has zero URL-related scenarios. No test documents or asserts this intentional asymmetry. | -| **I3** | `TRUSTED_MARKDOWN` is module-private; campaign's `composeDoc` must not export it | **Partial** | Two scenarios ("Release notes trusted markdown escapes interpolated fragment values", "Requirement digests escape interpolated trusted markdown values") assert the escape BEHAVIOR. No test asserts the symbol is unexported or that calling code outside `render-markdown.ts` cannot obtain a `TRUSTED_MARKDOWN`-tagged object. | -| **I4** | JSON renderer uses `isPlainObject` prototype check (anti-prototype-pollution) | **N** | `render-json.feature` tests Date/Map/Set class instances ("Forbidden runtime values produce descriptive path errors") but has no test for `Object.create(customProto)` — the prototype-chain check that `isPlainObject` actually enforces. | -| **I5** | `parseAndProject` rejects open-shape Zod input (all schemas use `z.strictObject`) | **Partial** | Three scenarios reject invalid values for known required fields (wrong grouping enum, unknown session type, malformed source-glob groups). None passes an EXTRA unknown property and asserts rejection. The `z.strictObject` strictness is untested at the call boundary. | - ---- - -## Findings (prioritized by campaign risk) - -### F1 — `renderMarkdown` has no perf gate; campaign multiplies this path 5× -**Severity: Critical** - -Confirmed Phase 2 H2: `tests/perf/baselines/business-rule-set.baseline.json` contains no `renderMarkdown` metric. The `compare-baseline.mjs` gate measures `project`, `renderObject` (JSON), `renderPretty` (JSON), and 7 hot-path projections via `renderJson`. The 2152-LOC markdown renderer — where the campaign's 5× doc-count fan-out lands — has no measured budget. - -**Why it matters:** the campaign adds `~25` new `DocDefinition`s, most routing through `renderMarkdown`. Regressions in `normalizeBusinessRuleSet`, `normalizeRequirementDigest`, or `splitOversizedDocument` land silently. - -**Recommendation:** Add `renderMarkdown` measurement to the perf report before W-DOCS-1. Parameterize over at least 3 `documentType` values (`business-rules`, `requirements-executable`, `patterns`) since each exercises a different normalizer. Extend `compare-baseline.mjs` with a `renderMarkdown` block parallel to `renderObject`/`renderPretty`. - -```typescript -// In business-rule-set-report.steps.ts, extend projectionHotPaths: -renderMarkdownBusinessRules: measureProjection( - () => renderMarkdown(parseAndProjectDocumentationBundle(projectionContext, { documentType: 'business-rules' })), -), -renderMarkdownRequirements: measureProjection( - () => renderMarkdown(parseAndProjectDocumentationBundle(projectionContext, { documentType: 'requirements-executable' })), -), -renderMarkdownPatterns: measureProjection( - () => renderMarkdown(parseAndProjectDocumentationBundle(projectionContext, { documentType: 'patterns' })), -), -``` - -```javascript -// In compare-baseline.mjs, add to HOT_PATH_BUDGETS: -renderMarkdownBusinessRules: { field: 'avgMs', budget: 20, unit: 'ms' }, -renderMarkdownRequirements: { field: 'avgMs', budget: 20, unit: 'ms' }, -renderMarkdownPatterns: { field: 'avgMs', budget: 20, unit: 'ms' }, -``` - ---- - -### F2 — Security invariants I1–I5 are documented-only; campaign adds code that violates each -**Severity: Critical** - -All five invariants from Phase 2 lack test-level enforcement. Concretely: - -- **I1 (partial):** `data:` scheme is not tested. `sanitizeMarkdownLinkTarget` allowlists `http/https/mailto`. A test asserting `data:text/html,', - patterns: [ + groupName: 'Roles', + entries: [ { - kind: 'PatternSummary', - patternName: 'Pattern **bold** [trap](javascript:alert(3))', - role: 'Pattern', - file: 'packages/foo.ts', - source: 'typescript', + kind: 'role', + tag: 'projection', + domain: 'Projection', + priority: 1, + description: 'Safe role tag', + aliases: [], }, - ], - deliverables: [ { - name: 'Deliverable [click](javascript:alert(4))', - status: 'active', - tests: [], - location: '', + // A sourced tag value carrying a backtick + link: the backtick would + // close a naive `code` span and let the rest inject a live link. + kind: 'role', + tag: 'evil`[click](javascript:alert(11))', + domain: 'Injected', + priority: 2, + description: 'Hostile tag value', + aliases: [], }, ], - notes: 'Release note [trap](javascript:alert(6))', }, ], + formatTypes: [], } as unknown as Fragment; } @@ -443,20 +448,28 @@ function createBusinessRulesDisclosureBundle(): ProjectionBundle { ], }; - return { - root: documentationFixtureToFragment(root), - children: { - 'business-rules:projection-api': documentationFixtureToFragment(child), - }, - routing: { - rootRouteId: 'business-rules:index', - childRouteIds: { - 'business-rules:projection-api': 'business-rules:projection-api', + return withBundleDisclosureSpec( + { + root: documentationFixtureToFragment(root), + children: { + 'business-rules:projection-api': documentationFixtureToFragment(child), + }, + routing: { + rootRouteId: 'business-rules:index', + childRouteIds: { + 'business-rules:projection-api': 'business-rules:projection-api', + }, + childPathStrategy: 'nested', + anchorStrategy: 'heading-slug', }, - childPathStrategy: 'nested', - anchorStrategy: 'heading-slug', }, - }; + { + grouping: 'flat', + richness: 'full', + emitChildren: true, + committed: true, + }, + ); } function createBusinessRuleSetDisclosureBundle(): ProjectionBundle { @@ -471,7 +484,6 @@ function createBusinessRuleSetDisclosureBundle(): ProjectionBundle { @@ -583,58 +597,79 @@ function createBusinessRuleSetHostileGroupingBundle(): ProjectionBundle { +function createBusinessRuleSetRichnessFixture( + disclosureSpec: DisclosureSpec, +): ProjectionBundle { + return withBundleDisclosureSpec( + { + root: { + kind: 'BusinessRuleSet', + scope: 'all', + rules: [ + { + kind: 'BusinessRule', + feature: 'ProjectionAPI', + ruleName: 'Canonical document types', + package: 'architect-projection', + invariant: 'Business rules expose stable disclosure-driven columns.', + rationale: 'Renderer richness should be explicit and testable.', + verifiedBy: ['BusinessRule table column count per richness'], + scenarioCount: 1, + pattern: 'ProjectionAPI', + productArea: 'Projection Platform', + }, + { + kind: 'BusinessRule', + feature: 'GenerateDocsCli', + ruleName: 'Registry dispatch', + package: 'architect-cli', + invariant: 'CLI business rules render through the same table policy.', + rationale: 'Disclosure richness should not be consumer-specific.', + verifiedBy: ['BusinessRule table column count per richness'], + scenarioCount: 1, + pattern: 'GenerateDocsCli', + productArea: 'CLI', + }, + { + kind: 'BusinessRule', + feature: 'ArchitectMcp', + ruleName: 'Documentation tool parity', + package: 'architect-mcp', + invariant: 'MCP business rules follow the same markdown richness policy.', + rationale: 'Boundary surfaces should share disclosure semantics.', + verifiedBy: ['BusinessRule table column count per richness'], + scenarioCount: 1, + pattern: 'ArchitectMcp', + productArea: 'MCP', + }, + ], + }, + children: {}, + }, + disclosureSpec, + ); +} + +function withBundleDisclosureSpec( + bundle: ProjectionBundle, + disclosureSpec: DisclosureSpec, +): ProjectionBundle { + const routing = bundle.routing; + return { - root: { - kind: 'BusinessRuleSet', - scope: 'all', - rules: [ - { - kind: 'BusinessRule', - feature: 'ProjectionAPI', - ruleName: 'Canonical document types', - package: 'architect-projection', - invariant: 'Business rules expose stable disclosure-driven columns.', - rationale: 'Renderer richness should be explicit and testable.', - verifiedBy: ['BusinessRule table column count per richness'], - scenarioCount: 1, - pattern: 'ProjectionAPI', - phase: 49, - productArea: 'Projection Platform', - }, - { - kind: 'BusinessRule', - feature: 'GenerateDocsCli', - ruleName: 'Registry dispatch', - package: 'architect-cli', - invariant: 'CLI business rules render through the same table policy.', - rationale: 'Disclosure richness should not be consumer-specific.', - verifiedBy: ['BusinessRule table column count per richness'], - scenarioCount: 1, - pattern: 'GenerateDocsCli', - phase: 49, - productArea: 'CLI', - }, - { - kind: 'BusinessRule', - feature: 'ArchitectMcp', - ruleName: 'Documentation tool parity', - package: 'architect-mcp', - invariant: 'MCP business rules follow the same markdown richness policy.', - rationale: 'Boundary surfaces should share disclosure semantics.', - verifiedBy: ['BusinessRule table column count per richness'], - scenarioCount: 1, - pattern: 'ArchitectMcp', - phase: 49, - productArea: 'MCP', - }, - ], + ...bundle, + routing: { + rootRouteId: routing?.rootRouteId ?? 'documentation:index', + childRouteIds: routing?.childRouteIds ?? {}, + childPathStrategy: routing?.childPathStrategy ?? 'nested', + anchorStrategy: routing?.anchorStrategy ?? 'heading-slug', + disclosureSpec, }, - children: {}, }; } @@ -778,7 +813,7 @@ function createRouteIdCollisionBundle(): ProjectionBundle { } function createRequirementsDisclosureBundle( - documentType: 'requirements-executable' | 'requirements-specs' + documentType: 'requirements-executable' | 'requirements-specs', ): ProjectionBundle { const label = documentType === 'requirements-executable' @@ -869,7 +904,7 @@ function createRequirementsDisclosureBundle( } function createRequirementsDisclosureBundleWithRejectedChildren( - documentType: 'requirements-executable' | 'requirements-specs' + documentType: 'requirements-executable' | 'requirements-specs', ): ProjectionBundle { const label = documentType === 'requirements-executable' @@ -1009,6 +1044,56 @@ const expectedAllBlocksMarkdown = [ '', ].join('\n'); +function createDecisionCatalogFixture(): Fragment { + const record = (id: string, title: string) => ({ + kind: 'DecisionRecord', + id, + type: 'ADR', + status: 'accepted', + title, + context: [], + decision: [], + consequences: [], + relatedDecisions: [], + affectedPatterns: [], + }); + return { + kind: 'DecisionCatalog', + decisions: [ + record('ADR-001', 'Taxonomy Canonical Values'), + record('ADR-X **bold**', 'Hostile Identifier'), + ], + } as unknown as Fragment; +} + +function createArchitectureDiagramFixture(): Fragment { + return { + kind: 'ArchitectureDiagram', + scope: 'component', + sections: [ + { + title: 'Context Map', + description: + 'Each node is a group; each arrow is a cross-group dependency (`depends-on` / `uses`).', + diagram: { type: 'mermaid', content: 'graph LR\n a --> b' }, + patterns: [], + }, + { + // Sourced group title carrying hostile markdown — the renderer must escape it while + // keeping the renderer-authored "(N patterns)" suffix live (ADR-009). + title: 'Bounded context: Auth **bold** [trap](javascript:alert(1))', + diagram: { type: 'mermaid', content: 'graph TD\n gamma["Gamma"]' }, + patterns: ['Gamma'], + }, + ], + legend: [ + { type: 'heading', level: 3, text: 'Legend' }, + { type: 'list', ordered: false, items: ['Solid arrow = dependency (depends-on / uses)'] }, + ], + patterns: ['Alpha', 'Beta'], + } as unknown as Fragment; +} + describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { AfterEachScenario(() => { state = null; @@ -1042,7 +1127,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a SectionedDocumentFixture fixture containing hostile markdown text and unsafe links', () => { state!.input = createUnsafeMarkdownFixture(); - } + }, ); When('I render the fragment as markdown', () => { @@ -1052,10 +1137,10 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { Then('the markdown output should escape hostile plain text', () => { const markdown = assertRenderedString(state!.rendered); expect(markdown).toContain( - '<script>alert\\("x"\\)</script> \\[trap\\]\\(javascript:alert\\(1\\)\\) \\*\\*bold\\*\\*' + '<script>alert("x")</script> \\[trap\\](javascript:alert(1)) \\*\\*bold\\*\\*', ); - expect(markdown).toContain('- \\!\\[img\\]\\(https://example.com/x.png\\)'); - expect(markdown).toContain('- \\[link\\]\\(javascript:alert\\(2\\)\\)'); + expect(markdown).toContain('- !\\[img\\](https://example.com/x.png)'); + expect(markdown).toContain('- \\[link\\](javascript:alert(2))'); }); And('the markdown output should neutralize block-level markdown markers', () => { @@ -1070,14 +1155,14 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { And('the markdown output should escape hostile collapsible summaries', () => { const markdown = assertRenderedString(state!.rendered); expect(markdown).toContain( - '\\*\\*Summary\\*\\* \\[trap\\]\\(javascript:alert\\(9\\)\\) <b>tag</b>' + '\\*\\*Summary\\*\\* \\[trap\\](javascript:alert(9)) <b>tag</b>', ); }); And('the markdown output should block unsafe link targets', () => { const markdown = assertRenderedString(state!.rendered); expect(markdown).not.toContain('[Click'); - expect(markdown).toContain('Click\\]\\(javascript:alert\\(3\\)\\)'); + expect(markdown).toContain('Click\\](javascript:alert(3))'); expect(markdown).toContain('[Safe Docs](https://example.com/docs%20path)'); expect(markdown).toContain('Protocol Relative'); expect(markdown).not.toContain('[Protocol Relative]('); @@ -1115,34 +1200,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { expect(markdown).not.toContain('[Trailing Numeric NewLine HTTPS]('); expect(markdown).toContain('[Safe Colonized Path](docs/&colonization-guide.md)'); }); - } - ); - - RuleScenario( - 'Release notes trusted markdown escapes interpolated fragment values', - ({ Given, When, Then }) => { - Given('a ReleaseNotesDigest fixture containing hostile release metadata', () => { - state!.input = createHostileReleaseNotesFixture(); - }); - - When('I render the fragment as markdown', () => { - state!.rendered = renderMarkdown(state!.input!); - }); - - Then('the release notes markdown should escape trusted interpolation values', () => { - const markdown = assertRenderedString(state!.rendered); - expect(markdown).toContain( - '## [v1.0\\]\\(javascript:alert\\(1\\)\\)] - <script>alert\\(2\\)</script>' - ); - expect(markdown).toContain( - '- **Deliverable \\[click\\]\\(javascript:alert\\(4\\)\\)**: <script>alert\\(5\\)</script>' - ); - expect(markdown).toContain( - '- Pattern \\*\\*bold\\*\\* \\[trap\\]\\(javascript:alert\\(3\\)\\)' - ); - expect(markdown).toContain('Release note \\[trap\\]\\(javascript:alert\\(6\\)\\)'); - }); - } + }, ); RuleScenario( @@ -1163,19 +1221,134 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { Then('the requirement markdown should escape trusted interpolation values', () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).toContain( - '[RendererRequirement \\[trap\\]\\(javascript:alert\\(7\\)\\)](requirements-executable/renderer-package/renderer-threat.md)' + '[RendererRequirement \\[trap\\](javascript:alert(7))](requirements-executable/renderer-package/renderer-threat.md)', ); expect(rendered['requirements-executable/renderer-package/renderer-threat.md']).toContain( - '**Status:** active \\*\\*bold\\*\\* \\[trap\\]\\(javascript:alert\\(8\\)\\)' + '**Status:** active \\*\\*bold\\*\\* \\[trap\\](javascript:alert(8))', ); expect(rendered['requirements-executable/renderer-package/renderer-threat.md']).toContain( - 'Requirement body remains plain text.' + 'Requirement body remains plain text.', ); }); - } + }, ); }); + Rule( + 'Renderer-authored markdown renders live while sourced text stays escaped', + ({ RuleScenario }) => { + RuleScenario( + 'Decision catalog renders live ADR links and escapes hostile link text', + ({ Given, When, Then, And }) => { + Given('a DecisionCatalog fixture with a normal and a hostile decision id', () => { + state!.input = createDecisionCatalogFixture(); + }); + + When('I render the fragment as markdown', () => { + state!.rendered = renderMarkdown(state!.input!); + }); + + Then('the decision catalog markdown should render the ADR link as a live link', () => { + const markdown = assertRenderedString(state!.rendered); + expect(markdown).toContain('[ADR-001](decisions/adr-001.md)'); + expect(markdown).not.toContain('\\[ADR-001\\]'); + }); + + And('the decision catalog markdown should escape the hostile decision link text', () => { + const markdown = assertRenderedString(state!.rendered); + // The link STRUCTURE is trusted, but the link TEXT (sourced id) stays escaped. + expect(markdown).toContain('ADR-X \\*\\*bold\\*\\*'); + expect(markdown).not.toContain('ADR-X **bold**'); + }); + }, + ); + + RuleScenario( + 'Architecture diagram trusts renderer-authored description and legend', + ({ Given, When, Then, And }) => { + Given('an ArchitectureDiagram fixture with a code-span description and a legend', () => { + state!.input = createArchitectureDiagramFixture(); + }); + + When('I render the fragment as markdown', () => { + state!.rendered = renderMarkdown(state!.input!); + }); + + Then( + 'the architecture markdown should render the description code spans unescaped', + () => { + const markdown = assertRenderedString(state!.rendered); + expect(markdown).toContain('cross-group dependency (`depends-on` / `uses`)'); + expect(markdown).not.toContain('\\`depends-on\\`'); + }, + ); + + And('the architecture markdown should render the legend parentheses unescaped', () => { + const markdown = assertRenderedString(state!.rendered); + expect(markdown).toContain('Solid arrow = dependency (depends-on / uses)'); + expect(markdown).not.toContain('dependency \\(depends-on / uses\\)'); + }); + }, + ); + + RuleScenario( + 'Architecture diagram escapes sourced section titles but keeps the pattern-count suffix live', + ({ Given, When, Then, And }) => { + Given('an ArchitectureDiagram fixture with a hostile sourced section title', () => { + state!.input = createArchitectureDiagramFixture(); + }); + + When('I render the fragment as markdown', () => { + state!.rendered = renderMarkdown(state!.input!); + }); + + Then('the architecture markdown should escape the sourced section title', () => { + const markdown = assertRenderedString(state!.rendered); + expect(markdown).toContain( + 'Bounded context: Auth \\*\\*bold\\*\\* \\[trap\\](javascript:alert(1))', + ); + expect(markdown).not.toContain('Auth **bold**'); + }); + + And( + 'the architecture markdown should keep the renderer-authored count suffix live', + () => { + const markdown = assertRenderedString(state!.rendered); + expect(markdown).toContain('(1 pattern)'); + expect(markdown).not.toContain('\\(1 pattern\\)'); + }, + ); + }, + ); + + RuleScenario( + 'Taxonomy tag code spans render live but sourced tag text cannot inject', + ({ Given, When, Then, And }) => { + Given( + 'a TaxonomyDigest fixture with a safe tag and a backtick-bearing hostile tag', + () => { + state!.input = createHostileTaxonomyDigestFixture(); + }, + ); + When('I render the fragment as markdown', () => { + state!.rendered = renderMarkdown(state!.input!); + }); + Then('the taxonomy markdown should render the safe tag as a live code span', () => { + const markdown = assertRenderedString(state!.rendered); + expect(markdown).toContain('`projection`'); + expect(markdown).not.toContain('\\`projection\\`'); + }); + And('the taxonomy markdown should not let the hostile tag inject a live link', () => { + const markdown = assertRenderedString(state!.rendered); + // The hostile tag's backtick forces the escaped-plain-text fallback, so its + // `]` is escaped and no `](javascript:…)` link forms (lookbehind = unescaped `]`). + expect(markdown).not.toMatch(/(? { @@ -1186,14 +1359,16 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a routed SectionedDocumentFixture bundle fixture that exceeds the markdown size budget', () => { state!.input = createSplitBundle(); - } + }, ); When('I render the bundle as markdown with an H2 size budget', () => { + state!.renderEvents = []; state!.rendered = renderMarkdown(state!.input!, { includeChildren: true, sizeBudget: 12, splitStrategy: 'h2-boundary', + onRenderDocument: (event) => state!.renderEvents.push(event), }); }); @@ -1226,19 +1401,35 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { '', '[See Gamma Section](gamma-section.md)', '', - ].join('\n') + ].join('\n'), ); expect(rendered['guides/alpha-section.md']).toContain( - '[← Back to Renderer Guide](renderer-guide.md)' + '[← Back to Renderer Guide](renderer-guide.md)', ); expect(rendered['guides/beta-section.md']).toContain('Beta details stay together too.'); expect(rendered['guides/gamma-section.md']).toContain( - 'Gamma details push the file over budget.' + 'Gamma details push the file over budget.', ); }); - } + + And('each split-path routed fragment should render at most twice', () => { + const counts = new Map(); + for (const event of state!.renderEvents) { + counts.set(event.renderKey, (counts.get(event.renderKey) ?? 0) + 1); + } + + expect(Object.fromEntries(counts.entries())).toEqual({ + 'INDEX.md': 1, + 'guides/renderer-guide.md': 2, + 'guides/renderer-guide.md#0:alpha-section': 2, + 'guides/renderer-guide.md#1:beta-section': 2, + 'guides/renderer-guide.md#2:gamma-section': 2, + }); + expect(Math.max(...counts.values())).toBeLessThanOrEqual(2); + }); + }, ); - } + }, ); Rule( @@ -1251,17 +1442,11 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a routed business-rules SectionedDocumentFixture bundle with detailed children', () => { state!.input = createBusinessRulesDisclosureBundle(); - } + }, ); When('I render the bundle as markdown without H2 splitting', () => { state!.rendered = renderMarkdown(state!.input!, { - disclosureSpec: { - grouping: 'flat', - richness: 'full', - emitChildren: true, - committed: true, - }, includeChildren: true, splitStrategy: 'never', }); @@ -1272,16 +1457,16 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['BUSINESS-RULES.md']).toContain('Canonical document types'); - } + }, ); And('the documentation detail child should retain its detail body', () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['business-rules/projection-api.md']).toContain( - 'Full invariant detail stays in the child page.' + 'Full invariant detail stays in the child page.', ); }); - } + }, ); RuleScenario( @@ -1294,8 +1479,6 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { When('I render the bundle as important business-rules markdown disclosure', () => { state!.rendered = renderMarkdown(state!.input!, { disclosureLevel: 'important', - disclosureSpec: - getSupportedDocumentationTypeMetadata('business-rules').disclosureMatrix.important, includeChildren: true, splitStrategy: 'never', }); @@ -1305,7 +1488,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['BUSINESS-RULES.md']).toContain('## Packages'); expect(rendered['BUSINESS-RULES.md']).toContain( - '| Package | Features | Rules | With Invariants |' + '| Package | Features | Rules | With Invariants |', ); }); @@ -1325,7 +1508,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['business-rules/architect-projection.md']).toContain('## Rules'); }); - } + }, ); RuleScenario( @@ -1338,8 +1521,6 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { When('I render the bundle with an unsafe business-rules route profile', () => { state!.rendered = renderMarkdown(state!.input!, { disclosureLevel: 'important', - disclosureSpec: - getSupportedDocumentationTypeMetadata('business-rules').disclosureMatrix.important, includeChildren: true, splitStrategy: 'never', routeProfile: { @@ -1354,18 +1535,24 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['BUSINESS-RULES.md']).toContain( - '| \\[CLI Trap\\]\\(javascript:alert\\(10\\)\\) | 1 | 1 | 1 |' + '| \\[CLI Trap\\](javascript:alert(10)) | 1 | 1 | 1 |', + ); + // The label's brackets are escaped (`\]`), so no live link forms even + // though `](javascript:…)` now appears as a substring — assert there is + // no `](…)` whose `]` is UNescaped (the lookbehind), which is the real + // injection guard now that redundant paren-escaping is gone. + expect(rendered['BUSINESS-RULES.md']).not.toMatch( + /(? { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['javascript:alert(10)']).toBeUndefined(); }); - } + }, ); RuleScenario( @@ -1378,8 +1565,6 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { When('I render the bundle with traversal business-rules route targets', () => { state!.rendered = renderMarkdown(state!.input!, { disclosureLevel: 'important', - disclosureSpec: - getSupportedDocumentationTypeMetadata('business-rules').disclosureMatrix.important, includeChildren: true, splitStrategy: 'never', routeProfile: { @@ -1401,10 +1586,10 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { Then('the business-rules root should render traversal labels as plain text', () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['BUSINESS-RULES.md']).toContain( - '| \\[CLI Trap\\]\\(javascript:alert\\(10\\)\\) | 1 | 1 | 1 |' + '| \\[CLI Trap\\](javascript:alert(10)) | 1 | 1 | 1 |', ); expect(rendered['BUSINESS-RULES.md']).not.toContain( - '[architect-projection](/tmp/absolute.md)' + '[architect-projection](/tmp/absolute.md)', ); expect(rendered['BUSINESS-RULES.md']).not.toContain('## Package Detail'); }); @@ -1414,7 +1599,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { expect(rendered['../outside.md']).toBeUndefined(); expect(rendered['/tmp/absolute.md']).toBeUndefined(); }); - } + }, ); RuleScenario( @@ -1427,15 +1612,21 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { When( 'I render the bundle as important business-rules markdown disclosure without child pages', () => { - const importantDisclosure = - getSupportedDocumentationTypeMetadata('business-rules').disclosureMatrix.important; + const importantDisclosure = withBundleDisclosureSpec( + createBusinessRuleSetDisclosureBundle(), + { + ...getSupportedDocumentationTypeMetadata('business-rules').disclosureMatrix + .important, + emitChildren: false, + }, + ); + state!.input = importantDisclosure; state!.rendered = renderMarkdown(state!.input!, { disclosureLevel: 'important', - disclosureSpec: { ...importantDisclosure, emitChildren: false }, includeChildren: false, splitStrategy: 'never', }); - } + }, ); Then('the business-rules root should contain a Packages counts table', () => { @@ -1443,7 +1634,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { const markdown = state!.rendered as string; expect(markdown).toContain('## Packages'); expect(markdown).toContain( - '| Package | Features | Rules | With Invariants |' + '| Package | Features | Rules | With Invariants |', ); }); @@ -1458,22 +1649,20 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { const markdown = state!.rendered as string; expect(markdown).not.toContain('## Rules'); }); - } + }, ); RuleScenarioOutline( 'BusinessRule table column count per richness', ({ Given, When, Then }, examples: Record) => { - Given('a BusinessRuleSet bundle of 3 rules', () => { - state!.input = createBusinessRuleSetRichnessFixture(); - }); + Given('a BusinessRuleSet bundle of 3 rules', () => void 0); When('I render the bundle to markdown at disclosure {string}', () => { const level = examples['level'] as 'essential' | 'important' | 'useful' | 'advanced'; - const disclosureSpec = - getSupportedDocumentationTypeMetadata('business-rules').disclosureMatrix[level]; + state!.input = createBusinessRuleSetRichnessFixture( + getSupportedDocumentationTypeMetadata('business-rules').disclosureMatrix[level], + ); state!.rendered = renderMarkdown(state!.input!, { - disclosureSpec, disclosureLevel: level, includeChildren: false, splitStrategy: 'never', @@ -1485,7 +1674,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { const markdown = state!.rendered as string; expect(countRuleTableColumns(markdown)).toBe(Number(examples['columns'])); }); - } + }, ); RuleScenario( @@ -1495,7 +1684,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a routed SectionedDocumentFixture bundle whose children request duplicate paths', () => { state!.input = createDuplicatePathBundle(); - } + }, ); When('I render the bundle as markdown without H2 splitting', () => { @@ -1521,10 +1710,10 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['PATTERNS.md']).toContain('[First Pattern](patterns/detail.md)'); expect(rendered['PATTERNS.md']).toContain( - '[Second Pattern](patterns/detail--second-pattern.md)' + '[Second Pattern](patterns/detail--second-pattern.md)', ); }); - } + }, ); RuleScenario( @@ -1534,7 +1723,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a routed SectionedDocumentFixture bundle whose children request duplicate paths', () => { state!.input = createDuplicatePathBundle(); - } + }, ); When('I render the bundle as markdown without H2 splitting', () => { @@ -1550,7 +1739,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { expect(rendered['PATTERNS.md']).toContain('Ambiguous Detail Alias'); expect(rendered['PATTERNS.md']).not.toContain('[Ambiguous Detail Alias]('); }); - } + }, ); RuleScenario( @@ -1560,7 +1749,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a routed SectionedDocumentFixture bundle with a child-key and route-id collision', () => { state!.input = createRouteIdCollisionBundle(); - } + }, ); When('I render the bundle as markdown without H2 splitting', () => { @@ -1576,7 +1765,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { expect(rendered['PATTERNS.md']).toContain('Colliding Alias'); expect(rendered['PATTERNS.md']).not.toContain('[Colliding Alias]('); }); - } + }, ); RuleScenario( @@ -1586,7 +1775,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a routed requirements-executable SectionedDocumentFixture bundle with detailed children', () => { state!.input = createRequirementsDisclosureBundle('requirements-executable'); - } + }, ); When('I render the bundle as markdown without H2 splitting', () => { @@ -1602,26 +1791,26 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).toContain( - '[RendererExecutableRequirement](requirements-executable/renderer-package/renderer-requirement.md)' + '[RendererExecutableRequirement](requirements-executable/renderer-package/renderer-requirement.md)', ); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).toContain( - 'RendererExecutableRequirement' + 'RendererExecutableRequirement', ); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).not.toContain( - 'RendererExecutableRequirement full requirement body is retained in the detail page.' + 'RendererExecutableRequirement full requirement body is retained in the detail page.', ); - } + }, ); And('the requirements-executable detail child should retain its requirement body', () => { const rendered = assertRenderedRecord(state!.rendered); expect( - rendered['requirements-executable/renderer-package/renderer-requirement.md'] + rendered['requirements-executable/renderer-package/renderer-requirement.md'], ).toContain( - 'RendererExecutableRequirement full requirement body is retained in the detail page.' + 'RendererExecutableRequirement full requirement body is retained in the detail page.', ); }); - } + }, ); RuleScenario( @@ -1631,7 +1820,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a routed requirements-executable SectionedDocumentFixture bundle with detailed children', () => { state!.input = createRequirementsDisclosureBundle('requirements-executable'); - } + }, ); When('I render the requirements-executable bundle with traversal route targets', () => { @@ -1653,12 +1842,12 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).toContain( - 'RendererExecutableRequirement' + 'RendererExecutableRequirement', ); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).not.toContain( - '[RendererExecutableRequirement](../outside.md)' + '[RendererExecutableRequirement](../outside.md)', ); - } + }, ); And( @@ -1666,9 +1855,9 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['../outside.md']).toBeUndefined(); - } + }, ); - } + }, ); RuleScenario( @@ -1678,7 +1867,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a routed requirements-executable SectionedDocumentFixture bundle with detailed children', () => { state!.input = createRequirementsDisclosureBundle('requirements-executable'); - } + }, ); When( @@ -1695,7 +1884,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { : '..%2Foutside.md', }, }); - } + }, ); Then( @@ -1703,12 +1892,12 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).toContain( - 'RendererExecutableRequirement' + 'RendererExecutableRequirement', ); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).not.toContain( - '[RendererExecutableRequirement](..%2Foutside.md)' + '[RendererExecutableRequirement](..%2Foutside.md)', ); - } + }, ); And( @@ -1716,9 +1905,9 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['..%2Foutside.md']).toBeUndefined(); - } + }, ); - } + }, ); RuleScenario( @@ -1728,7 +1917,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a routed requirements-executable SectionedDocumentFixture bundle with detailed children', () => { state!.input = createRequirementsDisclosureBundle('requirements-executable'); - } + }, ); When( @@ -1745,7 +1934,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { : '%09renderer.md', }, }); - } + }, ); Then( @@ -1753,12 +1942,12 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).toContain( - 'RendererExecutableRequirement' + 'RendererExecutableRequirement', ); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).not.toContain( - '[RendererExecutableRequirement](%09renderer.md)' + '[RendererExecutableRequirement](%09renderer.md)', ); - } + }, ); And( @@ -1766,9 +1955,9 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['%09renderer.md']).toBeUndefined(); - } + }, ); - } + }, ); RuleScenario( @@ -1779,7 +1968,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { () => { state!.input = createRequirementsDisclosureBundleWithRejectedChildren('requirements-executable'); - } + }, ); When( @@ -1798,7 +1987,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { : 'renderer.txt', }, }); - } + }, ); Then( @@ -1806,18 +1995,18 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).toContain( - 'RendererExecutableRequirement' + 'RendererExecutableRequirement', ); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).toContain( - 'RendererExecutableRequirementTxt' + 'RendererExecutableRequirementTxt', ); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).not.toContain( - '[RendererExecutableRequirement](' + '[RendererExecutableRequirement](', ); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).not.toContain( - '[RendererExecutableRequirementTxt](' + '[RendererExecutableRequirementTxt](', ); - } + }, ); And( @@ -1826,9 +2015,9 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered[' renderer.md ']).toBeUndefined(); expect(rendered['renderer.txt']).toBeUndefined(); - } + }, ); - } + }, ); RuleScenario( @@ -1838,7 +2027,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a routed requirements-executable SectionedDocumentFixture bundle with detailed children', () => { state!.input = createRequirementsDisclosureBundle('requirements-executable'); - } + }, ); When( @@ -1855,7 +2044,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { : 'requirements-executable/renderer-package/renderer-requirement.md', }, }); - } + }, ); Then( @@ -1864,9 +2053,9 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['REQUIREMENTS-EXECUTABLE.md']).toBeDefined(); expect(rendered[' REQUIREMENTS-EXECUTABLE.md ']).toBeUndefined(); - } + }, ); - } + }, ); RuleScenario( @@ -1876,7 +2065,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a routed requirements-specs SectionedDocumentFixture bundle with detailed children', () => { state!.input = createRequirementsDisclosureBundle('requirements-specs'); - } + }, ); When('I render the bundle as markdown without H2 splitting', () => { @@ -1892,23 +2081,158 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['REQUIREMENTS-SPECS.md']).toContain( - '[RendererSpecsRequirement](requirements-specs/renderer-requirement.md)' + '[RendererSpecsRequirement](requirements-specs/renderer-requirement.md)', ); expect(rendered['REQUIREMENTS-SPECS.md']).toContain('RendererSpecsRequirement'); expect(rendered['REQUIREMENTS-SPECS.md']).not.toContain( - 'RendererSpecsRequirement full requirement body is retained in the detail page.' + 'RendererSpecsRequirement full requirement body is retained in the detail page.', ); - } + }, ); And('the requirements-specs detail child should retain its requirement body', () => { const rendered = assertRenderedRecord(state!.rendered); expect(rendered['requirements-specs/renderer-requirement.md']).toContain( - 'RendererSpecsRequirement full requirement body is retained in the detail page.' + 'RendererSpecsRequirement full requirement body is retained in the detail page.', ); }); - } + }, ); - } + }, ); }); + +describe('renderMarkdown adversarial security coverage', () => { + it('uses bundle routing disclosure instead of a per-render-call override', () => { + const bundle = createBusinessRuleSetDisclosureBundle(); + const rendered = renderMarkdown(bundle, { + disclosureLevel: 'important', + disclosureSpec: { + grouping: 'flat', + richness: 'full', + emitChildren: true, + committed: true, + }, + includeChildren: true, + splitStrategy: 'never', + }); + + const markdown = assertRenderedRecord(rendered)['BUSINESS-RULES.md']; + expect(markdown).toContain('## Package Detail'); + expect(markdown).not.toContain('## Rules'); + }); + + it('uses five-backtick fences when code and mermaid content contains four-backtick runs', () => { + const rendered = renderMarkdown( + documentationFixtureToFragment({ + kind: 'SectionedDocumentFixture', + documentType: 'security', + title: 'Fence Security', + sections: [ + { + id: 'fences', + title: 'Fences', + blocks: [ + { type: 'code', language: 'ts', content: 'const nested = "````";' }, + { type: 'mermaid', content: 'graph TD; A[````] --> B[ok]' }, + ], + }, + ], + }), + ); + + const markdown = assertRenderedString(rendered); + expect(markdown).toContain('`````ts\nconst nested = "````";\n`````'); + expect(markdown).toContain('`````mermaid\ngraph TD; A[````] --> B[ok]\n`````'); + }); + + it('renders data URL link targets as plain text', () => { + const rendered = renderMarkdown( + documentationFixtureToFragment({ + kind: 'SectionedDocumentFixture', + documentType: 'security', + title: 'Link Security', + sections: [ + { + id: 'links', + title: 'Links', + blocks: [ + { type: 'link-out', text: 'Data URL', path: 'data:text/html,' }, + ], + }, + ], + }), + ); + + const markdown = assertRenderedString(rendered); + expect(markdown).toContain('Data URL'); + expect(markdown).not.toContain('[Data URL]('); + }); + + it('renders file URL link targets as plain text', () => { + const rendered = renderMarkdown( + documentationFixtureToFragment({ + kind: 'SectionedDocumentFixture', + documentType: 'security', + title: 'Link Security', + sections: [ + { + id: 'links', + title: 'Links', + blocks: [{ type: 'link-out', text: 'File URL', path: 'file:///etc/passwd' }], + }, + ], + }), + ); + + const markdown = assertRenderedString(rendered); + expect(markdown).toContain('File URL'); + expect(markdown).not.toContain('[File URL]('); + }); + + it('renders entity-encoded javascript URL link targets as plain text', () => { + const rendered = renderMarkdown( + documentationFixtureToFragment({ + kind: 'SectionedDocumentFixture', + documentType: 'security', + title: 'Link Security', + sections: [ + { + id: 'links', + title: 'Links', + blocks: [ + { type: 'link-out', text: 'Encoded JavaScript', path: 'javascript:alert(1)' }, + ], + }, + ], + }), + ); + + const markdown = assertRenderedString(rendered); + expect(markdown).toContain('Encoded JavaScript'); + expect(markdown).not.toContain('[Encoded JavaScript]('); + }); + + it('renders control-character link targets as plain text', () => { + const rendered = renderMarkdown( + documentationFixtureToFragment({ + kind: 'SectionedDocumentFixture', + documentType: 'security', + title: 'Link Security', + sections: [ + { + id: 'links', + title: 'Links', + blocks: [ + { type: 'link-out', text: 'Control Target', path: 'https://example.com/\u0000x' }, + ], + }, + ], + }), + ); + + const markdown = assertRenderedString(rendered); + expect(markdown).toContain('Control Target'); + expect(markdown).not.toContain('[Control Target]('); + }); +}); diff --git a/packages/architect-projection/tests/features/renderers/render-ui.feature b/packages/architect-projection/tests/features/renderers/render-ui.feature index d6f763e..e3419a3 100644 --- a/packages/architect-projection/tests/features/renderers/render-ui.feature +++ b/packages/architect-projection/tests/features/renderers/render-ui.feature @@ -1,3 +1,8 @@ +@architect +@architect-pattern:UiRendererExecutableTests +@architect-implements:UiRenderer +@architect-status:active +@architect-role:projection @projection Feature: renderUi returns Studio-oriented structured UI documents The UI renderer should keep bundle structure intact while reshaping fragments into predictable pure-data sections for Studio consumption. diff --git a/packages/architect-projection/tests/features/renderers/render-ui.steps.ts b/packages/architect-projection/tests/features/renderers/render-ui.steps.ts index e036512..93f0510 100644 --- a/packages/architect-projection/tests/features/renderers/render-ui.steps.ts +++ b/packages/architect-projection/tests/features/renderers/render-ui.steps.ts @@ -41,7 +41,6 @@ function createPatternDetailFixture(patternName = 'RenderUiProjection'): Pattern patternName, status: 'active', role: 'projection', - phase: 14, file: 'packages/architect-projection/src/renderers/render-ui.ts', source: 'typescript', description: 'Render UI data in a stable order for Studio consumers.', @@ -58,7 +57,7 @@ function createPatternDetailFixture(patternName = 'RenderUiProjection'): Pattern enables: ['StudioProjectionConsumption'], uses: ['BlockSchema'], usedBy: ['StudioProjectionConsumption'], - implementsPatterns: ['PerspectiveAwareProjections'], + implementsPatterns: ['SessionContextProjection'], implementedBy: [ { name: 'renderUi', @@ -174,7 +173,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'stubs', ]); }); - } + }, ); }); @@ -196,13 +195,13 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'pattern-detail-copy', ]); expect(state!.rendered?.children?.['documentation-child']?.heading).toBe( - 'DocumentationChild' + 'DocumentationChild', ); expect(state!.rendered?.children?.['pattern-detail-copy']?.heading).toBe( - 'RenderUiProjectionChild' + 'RenderUiProjectionChild', ); }); - } + }, ); }); }); diff --git a/packages/architect-projection/tests/features/renderers/renderer-smoke.feature b/packages/architect-projection/tests/features/renderers/renderer-smoke.feature index f7ae06d..16bba83 100644 --- a/packages/architect-projection/tests/features/renderers/renderer-smoke.feature +++ b/packages/architect-projection/tests/features/renderers/renderer-smoke.feature @@ -1,3 +1,8 @@ +@architect +@architect-pattern:RendererDispatchSmokeExecutableTests +@architect-implements:FragmentRendererDispatch +@architect-status:active +@architect-role:projection @projection Feature: Every renderer accepts every fragment kind without throwing New FragmentKinds must be accepted by each of the four renderers as a valid @@ -19,9 +24,7 @@ Feature: Every renderer accepts every fragment kind without throwing Examples: | kind | - | PhaseProgress | | StatusDistribution | - | ReleaseNotesDigest | | TraceabilityMatrix | | ProjectConfigSnapshot | | ArchitectureDiagram | @@ -55,6 +58,6 @@ Feature: Every renderer accepts every fragment kind without throwing | PatternDetail | | DependencyEdge | | DependencyEdgeSet | - | DependencyTree | + | DependencyContext | | ArchitectureNeighborhood | | OrphanPatternList | diff --git a/packages/architect-projection/tests/features/renderers/renderer-smoke.feature.steps.ts b/packages/architect-projection/tests/features/renderers/renderer-smoke.feature.steps.ts index cf31feb..9c17f5a 100644 --- a/packages/architect-projection/tests/features/renderers/renderer-smoke.feature.steps.ts +++ b/packages/architect-projection/tests/features/renderers/renderer-smoke.feature.steps.ts @@ -84,7 +84,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { runRenderer('renderJson', () => renderJson(fixture)); runRenderer('renderMarkdown', () => renderMarkdown(fixture)); runRenderer('renderUi', () => renderUi(fixture)); - } + }, ); Then('no renderer throws', () => { @@ -103,7 +103,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { expect(output, `${name} output for ${kind}`).toBeDefined(); expect( isNonEmptyProjection(output), - `${name} output for ${kind} should be non-empty` + `${name} output for ${kind} should be non-empty`, ).toBe(true); } @@ -120,8 +120,8 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { expect(compactText).not.toContain('Date: unknown'); } }); - } + }, ); - } + }, ); }); diff --git a/packages/architect-projection/tests/features/renderers/roadmap-markdown.feature b/packages/architect-projection/tests/features/renderers/roadmap-markdown.feature index 80f2b8c..3e5cb75 100644 --- a/packages/architect-projection/tests/features/renderers/roadmap-markdown.feature +++ b/packages/architect-projection/tests/features/renderers/roadmap-markdown.feature @@ -5,12 +5,11 @@ Feature: renderMarkdown renders roadmap timeline bundles Background: Given the roadmap markdown renderer state is initialized - Rule: Roadmap documentation bundles stay routed and quarter-grouped + Rule: Roadmap documentation bundles stay routed as a flat pattern list @routing Scenario: roadmap documentation bundle renders routed markdown files - Given a documentation projection context with roadmap and deferred quarter entries + Given a documentation projection context with roadmap and deferred patterns When I project and render the roadmap documentation bundle as markdown - Then the routed markdown output should include the roadmap root and quarter child files - And the roadmap root markdown should summarize the roadmap quarters - And the roadmap child markdown should retain the quarter pattern details + Then the routed markdown output should include the roadmap root file + And the roadmap root markdown should summarize the roadmap patterns diff --git a/packages/architect-projection/tests/features/renderers/roadmap-markdown.feature.steps.ts b/packages/architect-projection/tests/features/renderers/roadmap-markdown.feature.steps.ts index b1d474e..946718f 100644 --- a/packages/architect-projection/tests/features/renderers/roadmap-markdown.feature.steps.ts +++ b/packages/architect-projection/tests/features/renderers/roadmap-markdown.feature.steps.ts @@ -16,7 +16,7 @@ import { interface RoadmapMarkdownState { context: ProjectionContext | null; bundle: ProjectionBundle | null; - rendered: Record | null; + rendered: string | null; } const feature = await loadFeature('tests/features/renderers/roadmap-markdown.feature'); @@ -42,80 +42,54 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { }); }); - Rule('Roadmap documentation bundles stay routed and quarter-grouped', ({ RuleScenario }) => { + Rule('Roadmap documentation bundles stay routed as a flat pattern list', ({ RuleScenario }) => { RuleScenario( 'roadmap documentation bundle renders routed markdown files', ({ Given, When, Then, And }) => { - Given( - 'a documentation projection context with roadmap and deferred quarter entries', - () => { - state!.context = createProjectionContext({ - patterns: [ - createPattern('RoadmapAlpha', { - status: 'roadmap', - phase: 16, - quarter: 'Q1 2026', - }), - createPattern('RoadmapBeta', { - status: 'deferred', - phase: 17, - quarter: 'Q2 2026', - }), - createPattern('ActiveNoise', { - status: 'active', - phase: 18, - quarter: 'Q3 2026', - }), - ], - }); - } - ); + Given('a documentation projection context with roadmap and deferred patterns', () => { + state!.context = createProjectionContext({ + patterns: [ + createPattern('RoadmapAlpha', { + status: 'roadmap', + }), + createPattern('RoadmapBeta', { + status: 'deferred', + }), + createPattern('ActiveNoise', { + status: 'active', + }), + ], + }); + }); When('I project and render the roadmap documentation bundle as markdown', () => { state!.bundle = parseAndProjectDocumentationBundle(state!.context!, { documentType: 'roadmap', }); const rendered = renderMarkdown(state!.bundle!); - expect(typeof rendered).toBe('object'); - expect(rendered).not.toBeNull(); + expect(typeof rendered).toBe('string'); - if (typeof rendered === 'string') { - throw new Error('Expected roadmap markdown rendering to return routed files.'); + if (typeof rendered !== 'string') { + throw new Error('Expected roadmap markdown rendering to return a single document.'); } state!.rendered = rendered; }); - Then( - 'the routed markdown output should include the roadmap root and quarter child files', - () => { - expect(Object.keys(state!.rendered ?? {})).toEqual([ - 'ROADMAP.md', - 'roadmap/q1-2026.md', - 'roadmap/q2-2026.md', - ]); - } - ); + Then('the routed markdown output should include the roadmap root file', () => { + expect(typeof state!.rendered).toBe('string'); + expect(state!.rendered).toContain('# Roadmap'); + }); - And('the roadmap root markdown should summarize the roadmap quarters', () => { - const root = state!.rendered?.['ROADMAP.md']; - expect(root).toContain('# Roadmap'); - expect(root).toContain('Quarter-grouped roadmap timeline covering 2 quarters.'); - expect(root).toContain('## Q1 2026'); - expect(root).toContain('## Q2 2026'); + And('the roadmap root markdown should summarize the roadmap patterns', () => { + const root = state!.rendered ?? ''; + expect(root).toContain('Roadmap timeline covering 2 patterns.'); expect(root).toContain('RoadmapAlpha'); expect(root).toContain('RoadmapBeta'); expect(root).not.toContain('ActiveNoise'); + expect(root).toContain('packages/architect-projection/fixtures/RoadmapAlpha.ts'); }); - - And('the roadmap child markdown should retain the quarter pattern details', () => { - const child = state!.rendered?.['roadmap/q1-2026.md']; - expect(child).toContain('# Roadmap'); - expect(child).toContain('## Q1 2026'); - expect(child).toContain('RoadmapAlpha'); - expect(child).toContain('packages/architect-projection/fixtures/RoadmapAlpha.ts'); - }); - } + }, ); }); }); diff --git a/packages/architect-projection/tests/features/scaffold.steps.ts b/packages/architect-projection/tests/features/scaffold.steps.ts index 56ed3a5..4302f48 100644 --- a/packages/architect-projection/tests/features/scaffold.steps.ts +++ b/packages/architect-projection/tests/features/scaffold.steps.ts @@ -14,10 +14,8 @@ import { separator, table, type Block, - type Fragment, - type ProjectionContext, - type TagExampleOverrides, -} from '../../src/index.js'; +} from '@libar-dev/architect-core'; +import type { Fragment, ProjectionContext, TagExampleOverrides } from '../../src/index.js'; import { createTestPackageResolver } from '../support/test-package-resolver.js'; interface ScaffoldState { @@ -66,7 +64,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { ['blocks/schema.ts', 'scaffolded'], ['context/projection-context.ts', 'scaffolded'], ], - ['left', 'left'] + ['left', 'left'], ), list(['heading', { text: 'collapsible', checked: true }]), code('export type Fragment = never;', 'ts'), @@ -74,7 +72,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { collapsible('Future work', [paragraph('Wave 2 will extend the fragment union.')]), linkOut( 'Projection plan', - '.sisyphus/plans/ddd-projections-refactoring-opus-4.7-bkp-rtry.md' + '.sisyphus/plans/ddd-projections-refactoring-opus-4.7-bkp-rtry.md', ), ]; }); @@ -103,7 +101,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { }); expect(state!.emptyChildren).toEqual({}); }); - } + }, ); }); }); diff --git a/packages/architect-projection/tests/fixtures/documentation-composition/documentation-types.md b/packages/architect-projection/tests/fixtures/documentation-composition/documentation-types.md deleted file mode 100644 index e77e6b7..0000000 --- a/packages/architect-projection/tests/fixtures/documentation-composition/documentation-types.md +++ /dev/null @@ -1,28 +0,0 @@ -# Documentation composition documentation types - -`projectDocumentationBundle(context, options)` accepts exactly these document types: - -| Type | Source projection/composition | Notes | -| ------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| `architecture` | `parseAndProjectArchitectureDiagram()` + documentation composition adapter | Defaults to `component` scope unless explicit architecture scope options are supplied. | -| `decisions` | `projectDecisionCatalog()` | Preserves decision child fragments through bundle children. | -| `business-rules` | `projectBusinessRuleSet({ groupedBy: 'feature' })` | Uses grouped child fragments for feature-specific drill-down. | -| `patterns` | projectPatternCatalog() | Returns the domain pattern catalog bundle directly. | -| `roadmap` | `parseAndProjectDocumentationBundle({ documentType: 'roadmap' })` | Internal roadmap quarter children are normalized into routed bundle children. | -| `requirements-executable` | `projectRequirementExecutableDigest()` | Lists value-transfer-complete requirement coverage with routed package index and pattern-detail files. | -| `requirements-specs` | `projectRequirementSpecsDigest()` | Lists design/spec-tier requirement coverage with flat routed pattern-detail files. | -| `validation-rules` | `projectValidationRuleDigest()` | Returns the domain validation-rule digest bundle directly. | -| `taxonomy` | `projectTaxonomyDigest()` | Supports explicit example overrides through projection options. | -| `changelog` | `projectReleaseNotesDigest()` | Release buckets remain domain child fragments. | -| `traceability` | `projectTraceabilityMatrix()` | Row children are preserved as routed domain bundle children. | -| `current-work` | `projectCurrentWork()` | Current-quarter children are preserved as routed domain bundle children. | - -## Explicitly rejected - -These strings must throw `UnknownDocumentType` and must not be silently accepted or reintroduced: - -- `reference` -- `product-areas` -- `design-review` -- `product-requirements` -- any arbitrary unsupported value diff --git a/packages/architect-projection/tests/fixtures/fragments.ts b/packages/architect-projection/tests/fixtures/fragments.ts index bb80422..5606a4c 100644 --- a/packages/architect-projection/tests/fixtures/fragments.ts +++ b/packages/architect-projection/tests/fixtures/fragments.ts @@ -15,7 +15,7 @@ import { DeliverableSchema, DependencyEdgeSchema, DependencyEdgeSetSchema, - DependencyTreeSchema, + DependencyContextSchema, FileReadingListSchema, HandoffRecordSchema, OpenQuestionListSchema, @@ -24,10 +24,8 @@ import { PatternCatalogSchema, PatternDetailSchema, PatternSummarySchema, - PhaseProgressSchema, PrChangeReviewSchema, ProjectConfigSnapshotSchema, - ReleaseNotesDigestSchema, RequirementDigestSchema, RoleProfileCollectionSchema, RoleProfileSchema, @@ -46,9 +44,7 @@ import { } from '../../src/index.js'; export type PublicFragmentKind = - | 'PhaseProgress' | 'StatusDistribution' - | 'ReleaseNotesDigest' | 'TraceabilityMatrix' | 'ProjectConfigSnapshot' | 'BusinessRuleReference' @@ -83,7 +79,7 @@ export type PublicFragmentKind = | 'PatternDetail' | 'DependencyEdge' | 'DependencyEdgeSet' - | 'DependencyTree' + | 'DependencyContext' | 'ArchitectureNeighborhood' | 'OpenQuestionList' | 'OrphanPatternList'; @@ -98,7 +94,6 @@ const validDeliverable: Fragment = { location: 'packages/architect-projection/src/fragments/execution-context/session-context-bundle.ts', finding: 'Keeps context/session projection contracts strict and JSON-safe.', - release: '2026-Q2', }; const validScopeReadinessCheck: Fragment = { @@ -121,7 +116,6 @@ const validBusinessRule: Fragment = { verifiedBy: ['governance schema feature', 'package typecheck'], scenarioCount: 2, pattern: 'ProjectionMigration', - phase: 5, productArea: 'DeliveryProcess', }; @@ -147,10 +141,16 @@ const validArchitectureDiagramFixture: Fragment = { kind: 'ArchitectureDiagram', scope: 'bounded-context', scopeValue: 'projection', - diagram: { - type: 'mermaid', - content: 'graph TD; A[PatternGraph] --> B[ProjectionContext]; B --> C[ArchitectureDiagram]', - }, + sections: [ + { + title: 'Bounded context: projection (3 patterns)', + diagram: { + type: 'mermaid', + content: 'graph TD; A[PatternGraph] --> B[ProjectionContext]; B --> C[ArchitectureDiagram]', + }, + patterns: ['WidgetService', 'ProjectionContext', 'ArchitectureDiagramProjection'], + }, + ], legend: [ { type: 'heading', @@ -163,21 +163,10 @@ const validArchitectureDiagramFixture: Fragment = { ordered: false, }, ], - patterns: ['PatternGraphAPI', 'ProjectionContext', 'ArchitectureDiagramProjection'], + patterns: ['WidgetService', 'ProjectionContext', 'ArchitectureDiagramProjection'], }; export const FRAGMENT_VALID_FIXTURES: Record = { - PhaseProgress: { - kind: 'PhaseProgress', - phaseNumber: 4, - phaseName: 'Projection Cutover', - completed: 6, - active: 2, - planned: 3, - candidate: 1, - total: 12, - completionPercentage: 50, - }, StatusDistribution: { kind: 'StatusDistribution', counts: { @@ -194,39 +183,6 @@ export const FRAGMENT_VALID_FIXTURES: Record = { candidate: 5.5, }, }, - ReleaseNotesDigest: { - kind: 'ReleaseNotesDigest', - releases: [ - { - release: 'v4.7.0', - date: '2026-04-19', - patterns: [ - { - kind: 'PatternSummary', - patternName: 'ProjectionMigration', - status: 'completed', - role: 'service', - phase: 4, - file: 'packages/architect-projection/src/index.ts', - source: 'typescript', - }, - ], - deliverables: [ - { - name: 'Projection package', - status: 'completed', - tests: [ - 'packages/architect-projection/tests/features/fragments/delivery-reporting-schemas.feature', - ], - location: 'packages/architect-projection/src/index.ts', - finding: 'Consolidates fragment schemas behind one package boundary.', - release: 'v4.7.0', - }, - ], - notes: 'Introduces strict Delivery Reporting fragments for timeline and reporting outputs.', - }, - ], - }, TraceabilityMatrix: { kind: 'TraceabilityMatrix', rows: [ @@ -250,7 +206,6 @@ export const FRAGMENT_VALID_FIXTURES: Record = { sourceGlobs: ['src/**/*.ts', 'tests/features/**/*.feature', '!dist/**'], buildTimeMs: 184, patternCount: 47, - phaseCount: 7, roleCount: 6, projectName: 'architect-studio', }, @@ -278,81 +233,79 @@ export const FRAGMENT_VALID_FIXTURES: Record = { }, SessionContextBundle: { kind: 'SessionContextBundle', - patterns: ['PerspectiveAwareProjections'], + patterns: ['SessionContextProjection'], sessionType: 'implement', metadata: [ { - name: 'PerspectiveAwareProjections', - status: 'active', - phase: 49, - role: 'service', - file: 'packages/architect-query/src/api/context-assembler.ts', - summary: 'Builds session-oriented context for implementation work.', + name: 'SessionContextProjection', + status: 'completed', + role: 'projection', + file: 'packages/architect-projection/src/projections/execution-context/session-context.ts', + summary: + 'Builds session-oriented context bundles for planning, design, and implement sessions.', }, ], - specFiles: ['architect/specs/perspective-aware-projections.feature'], - stubs: [ - { - stubFile: 'architect/stubs/perspectives.stub.ts', - targetPath: 'packages/architect-query/src/api/context-assembler.ts', - name: 'PerspectiveAwareProjectionsStub', - }, + specFiles: [ + 'packages/architect-projection/tests/features/projections/execution-context/context-session.feature', ], + stubs: [], dependencies: [ { - name: 'EnforcementConfiguration', + name: 'ExecutionContextProjectionSupport', status: 'completed', - file: 'packages/architect-core/src/config/enforcement.ts', - kind: 'planning', + file: 'packages/architect-projection/src/projections/execution-context/execution-context-shared.internal.ts', + kind: 'implementation', }, ], sharedDependencies: [ { - name: 'EnforcementConfiguration', - status: 'completed', - file: 'packages/architect-core/src/config/enforcement.ts', - kind: 'planning', + name: 'ProjectionFragmentContracts', + status: 'active', + file: 'packages/architect-projection/src/fragments/index.ts', + kind: 'implementation', }, ], consumers: [ { - name: 'ArchitectMcpServer', - status: 'active', - file: 'packages/architect-mcp/src/tool-registry.ts', - kind: 'implementation', + name: 'ArchitectBriefDeterministicBundle', + status: 'candidate', + file: 'architect/specs/architect-brief-deterministic-bundle.feature', + kind: 'planning', }, ], architectureNeighbors: [ { - name: 'ContextAssemblerImpl', - status: 'active', - role: 'service', - archContext: 'api', - file: 'packages/architect-query/src/api/context-assembler.ts', + name: 'ScopeReadinessProjection', + status: 'completed', + role: 'projection', + archContext: 'execution-context', + file: 'packages/architect-projection/src/projections/execution-context/scope-readiness.ts', }, ], deliverables: [validDeliverable], fsm: { - currentStatus: 'active', - validTransitions: ['completed', 'deferred'], - protectionLevel: 'scope', + currentStatus: 'completed', + validTransitions: [], + protectionLevel: 'hard', }, fsmByPattern: [ { - pattern: 'PerspectiveAwareProjections', + pattern: 'SessionContextProjection', fsm: { - currentStatus: 'active', - validTransitions: ['completed', 'deferred'], - protectionLevel: 'scope', + currentStatus: 'completed', + validTransitions: [], + protectionLevel: 'hard', }, }, ], - testFiles: ['tests/features/query/context.feature'], + testFiles: [ + 'packages/architect-projection/tests/features/projections/execution-context/context-session.feature', + ], }, ScopeReadinessCheck: validScopeReadinessCheck, ScopeReadinessReport: { kind: 'ScopeReadinessReport', - pattern: 'PerspectiveAwareProjections', + pattern: 'ScopeReadinessProjection', sessionType: 'implement', checks: [ validScopeReadinessCheck, @@ -377,18 +330,18 @@ export const FRAGMENT_VALID_FIXTURES: Record = { }, HandoffRecord: { kind: 'HandoffRecord', - pattern: 'PerspectiveAwareProjections', + pattern: 'HandoffProjection', status: 'active', sessionType: 'review', completed: [ - 'Projection schema bundle (packages/architect-projection/src/fragments/execution-context)', + 'Execution-context projection bundle (packages/architect-projection/src/projections/execution-context)', ], inProgress: [ - 'Projection schema tests (packages/architect-projection/tests/features/fragments/execution-context-schemas.feature)', + 'Execution-context projection tests (packages/architect-projection/tests/features/projections/execution-context/context-session.feature)', ], filesModified: [ - 'packages/architect-projection/src/fragments/execution-context/handoff-record.ts', - 'packages/architect-projection/tests/features/fragments/execution-context-schemas.feature.steps.ts', + 'packages/architect-projection/src/projections/execution-context/handoff.ts', + 'packages/architect-projection/tests/features/projections/execution-context/context-session.steps.ts', ], discovered: [ 'Scope readiness and handoff contracts must align to the plan, not the legacy formatter shape.', @@ -399,19 +352,23 @@ export const FRAGMENT_VALID_FIXTURES: Record = { }, FileReadingList: { kind: 'FileReadingList', - pattern: 'PerspectiveAwareProjections', + pattern: 'FileReadingListProjection', primary: [ - 'packages/architect-query/src/api/context-assembler.ts', - 'packages/architect-query/src/api/scope-validator.ts', + 'packages/architect-projection/src/projections/execution-context/session-context.ts', + 'packages/architect-projection/src/projections/execution-context/scope-readiness.ts', + ], + completedDeps: [ + 'packages/architect-projection/src/projections/execution-context/execution-context-shared.internal.ts', + ], + roadmapDeps: ['packages/architect-projection/src/fragments/index.ts'], + architectureNeighbors: [ + 'packages/architect-projection/src/projections/execution-context/handoff.ts', ], - completedDeps: ['packages/architect-core/src/config/enforcement.ts'], - roadmapDeps: ['architect/specs/enforcement-configuration.feature'], - architectureNeighbors: ['packages/architect-query/src/api/handoff-generator.ts'], }, Deliverable: validDeliverable, DeliverableManifest: { kind: 'DeliverableManifest', - pattern: 'PerspectiveAwareProjections', + pattern: 'SessionContextProjection', items: [ validDeliverable, { @@ -463,7 +420,7 @@ export const FRAGMENT_VALID_FIXTURES: Record = { }, ], relatedDecisions: ['ADR-003', 'ADR-005'], - affectedPatterns: ['PerspectiveAwareProjections', 'McpOutputSchemaValidation'], + affectedPatterns: ['ProjectionFragmentContracts', 'McpOutputSchemaValidation'], }, DecisionCatalog: { kind: 'DecisionCatalog', @@ -494,7 +451,7 @@ export const FRAGMENT_VALID_FIXTURES: Record = { }, ], relatedDecisions: ['ADR-003'], - affectedPatterns: ['PerspectiveAwareProjections'], + affectedPatterns: ['ProjectionFragmentContracts'], }, ], }, @@ -555,14 +512,15 @@ export const FRAGMENT_VALID_FIXTURES: Record = { statuses: ['roadmap', 'deferred'], meaning: 'Planning statuses remain editable.', canAddDeliverables: true, - needsUnlock: false, + unlockSuppressesWarning: false, }, { level: 'hard', statuses: ['completed'], - meaning: 'Completed work is locked without an explicit unlock reason.', + meaning: + 'Completed work is hard-locked; editing or reopening warns, unlock reason is optional (advisory).', canAddDeliverables: false, - needsUnlock: true, + unlockSuppressesWarning: true, }, ], }, @@ -620,7 +578,7 @@ export const FRAGMENT_VALID_FIXTURES: Record = { ], exampleOverrides: { enum: '@architect-status active', - csv: '@architect-uses PatternGraphAPI, ProjectionBundle', + csv: '@architect-uses WidgetService, ProjectionBundle', }, }, OverviewDigest: { @@ -633,19 +591,6 @@ export const FRAGMENT_VALID_FIXTURES: Record = { candidate: 1, percentage: 46, }, - activePhases: [ - { - phase: 4, - name: 'Projection Cutover', - patternCount: 5, - activeCount: 2, - }, - { - phase: 5, - patternCount: 3, - activeCount: 2, - }, - ], blocking: [ { pattern: 'OperationalInsightsProjectionBodies', @@ -685,7 +630,7 @@ export const FRAGMENT_VALID_FIXTURES: Record = { }, { kind: 'TagUsageEntry', - tag: 'quarter', + tag: 'bounded-context', count: 0, values: null, }, @@ -728,7 +673,7 @@ export const FRAGMENT_VALID_FIXTURES: Record = { priority: 20, count: 5, description: 'Coordinates use cases and delegates to lower layers.', - examples: ['PatternGraphAPI', 'ContextAssemblerImpl'], + examples: ['WidgetService', 'ContextAssemblerImpl'], }, RoleProfileCollection: { kind: 'RoleProfileCollection', @@ -740,7 +685,7 @@ export const FRAGMENT_VALID_FIXTURES: Record = { priority: 20, count: 5, description: 'Coordinates use cases and delegates to lower layers.', - examples: ['PatternGraphAPI', 'ContextAssemblerImpl'], + examples: ['WidgetService', 'ContextAssemblerImpl'], }, ], }, @@ -799,15 +744,14 @@ export const FRAGMENT_VALID_FIXTURES: Record = { count: false, }, count: 1, - names: ['PatternGraphAPI'], + names: ['WidgetService'], items: [ { kind: 'PatternSummary', - patternName: 'PatternGraphAPI', + patternName: 'WidgetService', status: 'active', role: 'infra', - phase: 2, - file: 'packages/architect-query/src/pattern-graph-api.ts', + file: 'packages/architect-query/src/graph-handle.ts', source: 'typescript', }, ], @@ -818,7 +762,7 @@ export const FRAGMENT_VALID_FIXTURES: Record = { { name: 'api', patternCount: 2, - patterns: ['ContextAssemblerImpl', 'PatternGraphAPI'], + patterns: ['ContextAssemblerImpl', 'WidgetService'], layers: ['application'], roles: ['service'], }, @@ -853,20 +797,18 @@ export const FRAGMENT_VALID_FIXTURES: Record = { }, PatternSummary: { kind: 'PatternSummary', - patternName: 'PatternGraphAPI', + patternName: 'WidgetService', status: 'active', role: 'service', - phase: 2, - file: 'packages/architect-query/src/pattern-graph-api.ts', + file: 'packages/architect-query/src/graph-handle.ts', source: 'typescript', }, PatternDetail: { kind: 'PatternDetail', - patternName: 'PatternGraphAPI', + patternName: 'WidgetService', status: 'active', role: 'service', - phase: 2, - file: 'packages/architect-query/src/pattern-graph-api.ts', + file: 'packages/architect-query/src/graph-handle.ts', source: 'typescript', description: 'Primary query facade over the PatternGraph read model.', deliverables: [ @@ -874,9 +816,8 @@ export const FRAGMENT_VALID_FIXTURES: Record = { name: 'PatternGraph API module', status: 'active', tests: ['tests/features/query/pattern-graph.feature'], - location: 'packages/architect-query/src/pattern-graph-api.ts', + location: 'packages/architect-query/src/graph-handle.ts', finding: 'Keeps read operations centralized.', - release: '2026-Q2', }, ], relationships: { @@ -887,8 +828,8 @@ export const FRAGMENT_VALID_FIXTURES: Record = { implementsPatterns: ['PatternGraphReadModel'], implementedBy: [ { - name: 'PatternGraphAPIImpl', - file: 'packages/architect-query/src/pattern-graph-api.ts', + name: 'WidgetServiceImpl', + file: 'packages/architect-query/src/graph-handle.ts', description: 'Concrete API adapter', }, ], @@ -908,73 +849,81 @@ export const FRAGMENT_VALID_FIXTURES: Record = { ], stubs: [ { - stubFile: 'architect/stubs/query/pattern-graph-api.stub.ts', - targetPath: 'packages/architect-query/src/pattern-graph-api.ts', - name: 'PatternGraphAPIStub', + stubFile: 'architect/stubs/query/graph-handle.stub.ts', + targetPath: 'packages/architect-query/src/graph-handle.ts', + name: 'WidgetServiceStub', }, ], deliverableManifest: { - pattern: 'PatternGraphAPI', + pattern: 'WidgetService', items: [ { name: 'PatternGraph API module', status: 'active', tests: ['tests/features/query/pattern-graph.feature'], - location: 'packages/architect-query/src/pattern-graph-api.ts', + location: 'packages/architect-query/src/graph-handle.ts', finding: 'Keeps read operations centralized.', - release: '2026-Q2', }, ], }, }, DependencyEdge: { kind: 'DependencyEdge', - from: 'PatternGraphAPI', + from: 'WidgetService', to: 'PatternGraph', relationKind: 'depends-on', }, DependencyEdgeSet: { kind: 'DependencyEdgeSet', - from: 'PatternGraphAPI', + from: 'WidgetService', items: [ { kind: 'DependencyEdge', - from: 'PatternGraphAPI', + from: 'WidgetService', to: 'PatternGraph', relationKind: 'depends-on', }, ], }, - DependencyTree: { - kind: 'DependencyTree', - root: 'PatternGraph', - nodes: [ + DependencyContext: { + kind: 'DependencyContext', + focal: 'WidgetService', + upstream: [ { name: 'PatternGraph', status: 'completed', - phase: 1, - isFocal: false, truncated: false, children: [ { - name: 'PatternGraphAPI', + name: 'PatternHelpers', status: 'active', - phase: 2, - isFocal: true, - truncated: false, + truncated: true, children: [], }, ], }, ], + downstream: [ + { + name: 'ApiReferenceProjection', + status: 'active', + truncated: false, + children: [], + }, + ], + summary: { + upstreamDirect: 1, + upstreamTransitive: 2, + downstreamDirect: 1, + downstreamTransitive: 1, + }, options: { maxDepth: 3, - includeImplementationDeps: true, }, }, ArchitectureNeighborhood: { kind: 'ArchitectureNeighborhood', - pattern: 'PatternGraphAPI', + pattern: 'WidgetService', context: 'api', role: 'service', layer: 'application', @@ -982,12 +931,14 @@ export const FRAGMENT_VALID_FIXTURES: Record = { usedBy: ['PatternBrowserView'], dependsOn: ['PatternGraph'], enables: ['ArchitectMcpServer'], + seeAlso: [], + enforcedBy: [], sameContext: ['ContextAssemblerImpl'], implements: ['PatternGraphReadModel'], implementedBy: [ { - name: 'PatternGraphAPIImpl', - file: 'packages/architect-query/src/pattern-graph-api.ts', + name: 'WidgetServiceImpl', + file: 'packages/architect-query/src/graph-handle.ts', description: 'Concrete API adapter', }, ], @@ -1018,18 +969,6 @@ export const FRAGMENT_VALID_FIXTURES: Record = { }; export const FRAGMENT_INVALID_FIXTURES: Record = { - PhaseProgress: { - kind: 'PhaseProgress', - phaseNumber: 4, - phaseName: 'Projection Cutover', - completed: 6, - active: 2, - planned: 3, - candidate: 1, - total: 12, - completionPercentage: 50, - extraField: true, - }, StatusDistribution: { kind: 'StatusDistribution', counts: { @@ -1047,18 +986,6 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { total: 100, }, }, - ReleaseNotesDigest: { - kind: 'ReleaseNotesDigest', - releases: [ - { - release: 'v4.7.0', - patterns: [], - deliverables: [], - notes: 'strict schema should reject unknown properties', - markdown: '### forbidden presentation field', - }, - ], - }, TraceabilityMatrix: { kind: 'TraceabilityMatrix', rows: [ @@ -1083,7 +1010,6 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { }, buildTimeMs: 184, patternCount: 47, - phaseCount: 7, roleCount: 6, }, BusinessRuleReference: { @@ -1095,11 +1021,17 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { ArchitectureDiagram: { kind: 'ArchitectureDiagram', scope: 'component', - diagram: { - type: 'paragraph', - text: 'This must be a mermaid block.', - }, - patterns: ['PatternGraphAPI'], + sections: [ + { + title: 'Context Map', + diagram: { + type: 'paragraph', + text: 'This must be a mermaid block.', + }, + patterns: ['WidgetService'], + }, + ], + patterns: ['WidgetService'], }, PrChangeReview: { kind: 'PrChangeReview', @@ -1112,20 +1044,20 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { }, SessionContextBundle: { kind: 'SessionContextBundle', - patterns: ['PerspectiveAwareProjections'], + patterns: ['SessionContextProjection'], sessionType: 'implement', metadata: [ { - name: 'PerspectiveAwareProjections', + name: 'SessionContextProjection', role: 'service', - file: 'packages/architect-query/src/api/context-assembler.ts', + file: 'packages/architect-projection/src/projections/execution-context/session-context.ts', summary: 'Builds session-oriented context for implementation work.', }, ], specFiles: [], stubs: [], dependencies: [], - sharedDependencies: ['EnforcementConfiguration'], + sharedDependencies: ['ProjectionFragmentContracts'], consumers: [], architectureNeighbors: [], deliverables: [], @@ -1143,7 +1075,7 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { }, ScopeReadinessReport: { kind: 'ScopeReadinessReport', - pattern: 'PerspectiveAwareProjections', + pattern: 'ScopeReadinessProjection', sessionType: 'planning', checks: [ { @@ -1160,7 +1092,7 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { }, HandoffRecord: { kind: 'HandoffRecord', - pattern: 'PerspectiveAwareProjections', + pattern: 'HandoffProjection', sessionType: 'review', completed: [], inProgress: [], @@ -1170,7 +1102,7 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { }, FileReadingList: { kind: 'FileReadingList', - pattern: 'PerspectiveAwareProjections', + pattern: 'FileReadingListProjection', primary: [], completedDeps: [], roadmapDeps: [], @@ -1188,7 +1120,7 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { }, DeliverableManifest: { kind: 'DeliverableManifest', - pattern: 'PerspectiveAwareProjections', + pattern: 'SessionContextProjection', items: [ { kind: 'Deliverable', @@ -1290,7 +1222,6 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { candidate: 1, percentage: 101, }, - activePhases: [], blocking: [], }, AnnotationCoverage: { @@ -1361,7 +1292,7 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { count: false, }, count: 1, - names: ['PatternGraphAPI'], + names: ['WidgetService'], items: [], unexpected: true, }, @@ -1371,7 +1302,7 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { { name: 'api', patternCount: '2', - patterns: ['PatternGraphAPI'], + patterns: ['WidgetService'], layers: ['application'], roles: ['service'], }, @@ -1406,17 +1337,17 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { }, PatternSummary: { kind: 'PatternSummary', - patternName: 'PatternGraphAPI', + patternName: 'WidgetService', role: 'service', - file: 'packages/architect-query/src/pattern-graph-api.ts', + file: 'packages/architect-query/src/graph-handle.ts', source: 'typescript', extraField: true, }, PatternDetail: { kind: 'PatternDetail', - patternName: 'PatternGraphAPI', + patternName: 'WidgetService', role: 'service', - file: 'packages/architect-query/src/pattern-graph-api.ts', + file: 'packages/architect-query/src/graph-handle.ts', source: 'typescript', deliverables: [], relationships: { @@ -1437,37 +1368,41 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { }, DependencyEdge: { kind: 'DependencyEdge', - from: 'PatternGraphAPI', + from: 'WidgetService', to: 'PatternGraph', relationKind: 'blocked-by', }, DependencyEdgeSet: { kind: 'DependencyEdgeSet', - from: 'PatternGraphAPI', + from: 'WidgetService', items: 'not-an-array', }, - DependencyTree: { - kind: 'DependencyTree', - root: 'PatternGraph', - nodes: [ + DependencyContext: { + kind: 'DependencyContext', + focal: 'WidgetService', + upstream: [ { - name: 'PatternGraphAPI', + name: 'PatternGraph', status: 'active', - phase: 2, - isFocal: true, truncated: false, children: [], extraField: 'not allowed', }, ], + downstream: [], + summary: { + upstreamDirect: 1, + upstreamTransitive: 1, + downstreamDirect: 0, + downstreamTransitive: 0, + }, options: { maxDepth: 3, - includeImplementationDeps: true, }, }, ArchitectureNeighborhood: { kind: 'ArchitectureNeighborhood', - pattern: 'PatternGraphAPI', + pattern: 'WidgetService', context: 'api', role: 'service', layer: 'application', @@ -1475,9 +1410,11 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { usedBy: ['PatternBrowserView'], dependsOn: ['PatternGraph'], enables: ['ArchitectMcpServer'], + seeAlso: [], + enforcedBy: [], sameContext: ['ContextAssemblerImpl'], implements: ['PatternGraphReadModel'], - implementedBy: ['PatternGraphAPIImpl'], + implementedBy: ['WidgetServiceImpl'], }, OpenQuestionList: { kind: 'OpenQuestionList', @@ -1503,9 +1440,7 @@ export const FRAGMENT_INVALID_FIXTURES: Record = { }; export const FRAGMENT_SCHEMAS: Record> = { - PhaseProgress: PhaseProgressSchema, StatusDistribution: StatusDistributionSchema, - ReleaseNotesDigest: ReleaseNotesDigestSchema, TraceabilityMatrix: TraceabilityMatrixSchema, ProjectConfigSnapshot: ProjectConfigSnapshotSchema, ArchitectureDiagram: ArchitectureDiagramSchema, @@ -1540,14 +1475,14 @@ export const FRAGMENT_SCHEMAS: Record> = { PatternDetail: PatternDetailSchema, DependencyEdge: DependencyEdgeSchema, DependencyEdgeSet: DependencyEdgeSetSchema, - DependencyTree: DependencyTreeSchema, + DependencyContext: DependencyContextSchema, ArchitectureNeighborhood: ArchitectureNeighborhoodSchema, OpenQuestionList: OpenQuestionListSchema, OrphanPatternList: OrphanPatternListSchema, }; export const FRAGMENT_KINDS: readonly PublicFragmentKind[] = Object.keys( - FRAGMENT_SCHEMAS + FRAGMENT_SCHEMAS, ) as PublicFragmentKind[]; export const INVALID_ARCHITECTURE_DIAGRAM_SCOPE_FIXTURE: unknown = { diff --git a/packages/architect-projection/tests/fixtures/renderers/progressive-disclosure.md b/packages/architect-projection/tests/fixtures/renderers/progressive-disclosure.md index dc12782..b39fffe 100644 --- a/packages/architect-projection/tests/fixtures/renderers/progressive-disclosure.md +++ b/packages/architect-projection/tests/fixtures/renderers/progressive-disclosure.md @@ -33,7 +33,7 @@ type BundleRouting = { ## Decision 1: Delivery-reporting view splitting stays at the projection layer -`projectCompletedMilestones` and `projectCurrentWork` stay explicit public projection entrypoints because their retained delivery-reporting views must remain deterministic. The roadmap view stays inside `parseAndProjectDocumentationBundle({ documentType: 'roadmap' })`, where the package can keep the internal timeline helper without re-exposing it as a public projector. +`projectChangelog` and `projectCurrentWork` stay explicit public projection entrypoints because their retained delivery-reporting views must remain deterministic. The roadmap view stays inside `parseAndProjectDocumentationBundle({ documentType: 'roadmap' })`, where the package can keep the internal timeline helper without re-exposing it as a public projector. We do not create a single projection function that switches behavior from a runtime `view` option. That keeps routing, naming, and downstream renderer expectations deterministic. diff --git a/packages/architect-projection/tests/perf/baselines/business-rule-set.baseline.json b/packages/architect-projection/tests/perf/baselines/business-rule-set.baseline.json index b5943d0..99c1736 100644 --- a/packages/architect-projection/tests/perf/baselines/business-rule-set.baseline.json +++ b/packages/architect-projection/tests/perf/baselines/business-rule-set.baseline.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-05-08T15:24:38.282Z", + "generatedAt": "2026-05-29T04:15:34.472Z", "fixture": { "name": "BusinessRuleSet grouped-by-product-area bundle", "patterns": 36, @@ -11,61 +11,360 @@ "warmupIterations": 5 }, "project": { - "avgMs": 1.1706323000000054, - "p50Ms": 0.5370419999999285, + "avgMs": 0.5962738750000028, + "p50Ms": 0.5401249999999891, "iterations": 40 }, "renderObject": { - "avgMs": 0.4403969249999989, - "p50Ms": 0.39545799999996234, + "avgMs": 0.37305319999999254, + "p50Ms": 0.371167000000014, "iterations": 40 }, "renderPretty": { - "avgMs": 0.7603312500000016, - "p50Ms": 0.5768330000000788, + "avgMs": 0.5416459499999917, + "p50Ms": 0.5236669999999322, "iterations": 40 }, "projectionHotPaths": { "sessionContextBundle": { - "avgMs": 0.013877766666693485, - "p50Ms": 0.009500000000116415, - "iterations": 30 + "avgMs": 0.006313667999994323, + "p50Ms": 0.0054169999998521234, + "iterations": 250 }, "scopeReadinessReport": { - "avgMs": 0.011734699999995732, - "p50Ms": 0.008333999999877051, - "iterations": 30 + "avgMs": 0.00623716400000103, + "p50Ms": 0.005374999999958163, + "iterations": 250 }, "documentationView": { - "avgMs": 0.019727866666645846, - "p50Ms": 0.01683299999990595, - "iterations": 30 + "avgMs": 0.009942147999992813, + "p50Ms": 0.008125000000063665, + "iterations": 250 }, "requirementDigestAllAreas": { - "avgMs": 0.15573196666667474, - "p50Ms": 0.15658300000018244, - "iterations": 30 + "avgMs": 0.0966245200000003, + "p50Ms": 0.09429099999988466, + "iterations": 250 }, "requirementDigestExecutable": { - "avgMs": 0.22038616666667016, - "p50Ms": 0.21408400000018446, - "iterations": 30 + "avgMs": 0.14814464799999041, + "p50Ms": 0.14483400000017355, + "iterations": 250 }, "patternSatisfiesTag": { - "avgMs": 0.07620699999999185, - "p50Ms": 0.08029099999998834, - "iterations": 30 + "avgMs": 0.04117066000000705, + "p50Ms": 0.0359169999999267, + "iterations": 250 }, "buildBoundedContext": { - "avgMs": 0.03054456666666283, - "p50Ms": 0.027166999999963082, - "iterations": 30 + "avgMs": 0.016804299999999786, + "p50Ms": 0.014833000000180618, + "iterations": 250 }, "graphBuild": { - "avgMs": 290.94890829999997, - "p50Ms": 283.45437500000025, + "avgMs": 376.2606917999998, + "p50Ms": 371.7694589999992, "iterations": 10 } }, - "isBundleP50Micros": 2.6250000000800355 + "renderMarkdownBundles": { + "patterns": { + "avgMs": 0.23772281200000725, + "p50Ms": 0.21970799999962765, + "iterations": 250 + }, + "decisions": { + "avgMs": 0.25274582399998324, + "p50Ms": 0.24049999999988358, + "iterations": 250 + }, + "requirements-executable": { + "avgMs": 0.19015747599999305, + "p50Ms": 0.1800839999996242, + "iterations": 250 + } + }, + "isBundleP50Micros": 4.583000000138782, + "samples": [ + { + "iteration": 1, + "projectMs": 0.5788330000000315, + "renderObjectMs": 0.4869170000001759, + "renderPrettyMs": 0.6763750000000073, + "isBundleMicros": 4.790999999841006 + }, + { + "iteration": 2, + "projectMs": 0.6522500000000946, + "renderObjectMs": 0.4344579999999496, + "renderPrettyMs": 0.5983750000000327, + "isBundleMicros": 4.707999999936874 + }, + { + "iteration": 3, + "projectMs": 0.6102500000001783, + "renderObjectMs": 0.4152080000001206, + "renderPrettyMs": 0.5824170000000777, + "isBundleMicros": 4.58400000002257 + }, + { + "iteration": 4, + "projectMs": 0.5974169999999503, + "renderObjectMs": 0.4043749999998454, + "renderPrettyMs": 0.5897499999998672, + "isBundleMicros": 4.666000000042914 + }, + { + "iteration": 5, + "projectMs": 3.1422079999999823, + "renderObjectMs": 0.3427910000000338, + "renderPrettyMs": 0.5236669999999322, + "isBundleMicros": 5.625000000009095 + }, + { + "iteration": 6, + "projectMs": 0.6212080000000242, + "renderObjectMs": 0.42325000000005275, + "renderPrettyMs": 0.6050000000000182, + "isBundleMicros": 8.95900000000438 + }, + { + "iteration": 7, + "projectMs": 0.6265830000002097, + "renderObjectMs": 0.4001670000000104, + "renderPrettyMs": 0.5470840000000408, + "isBundleMicros": 4.249999999956344 + }, + { + "iteration": 8, + "projectMs": 0.5467499999999745, + "renderObjectMs": 0.3861669999998867, + "renderPrettyMs": 0.5418340000001081, + "isBundleMicros": 3.9580000000114524 + }, + { + "iteration": 9, + "projectMs": 0.5632080000000315, + "renderObjectMs": 0.3968330000000151, + "renderPrettyMs": 0.5638330000001588, + "isBundleMicros": 4.20799999983501 + }, + { + "iteration": 10, + "projectMs": 0.5791250000002037, + "renderObjectMs": 0.3897500000000491, + "renderPrettyMs": 0.6433329999999842, + "isBundleMicros": 10.33400000005713 + }, + { + "iteration": 11, + "projectMs": 0.572458000000097, + "renderObjectMs": 0.4042500000000473, + "renderPrettyMs": 0.566332999999986, + "isBundleMicros": 4.83299999996234 + }, + { + "iteration": 12, + "projectMs": 0.5401249999999891, + "renderObjectMs": 0.39370800000006057, + "renderPrettyMs": 0.5560420000001614, + "isBundleMicros": 4.833000000189713 + }, + { + "iteration": 13, + "projectMs": 0.6339590000000044, + "renderObjectMs": 0.487041999999974, + "renderPrettyMs": 0.6120409999998628, + "isBundleMicros": 5.417000000079497 + }, + { + "iteration": 14, + "projectMs": 0.553957999999966, + "renderObjectMs": 0.43233299999997143, + "renderPrettyMs": 0.6383749999999964, + "isBundleMicros": 29.16700000014316 + }, + { + "iteration": 15, + "projectMs": 0.5789159999999356, + "renderObjectMs": 0.4511249999998199, + "renderPrettyMs": 0.6417089999999916, + "isBundleMicros": 6.0829999999896245 + }, + { + "iteration": 16, + "projectMs": 0.622916000000032, + "renderObjectMs": 0.4627499999999145, + "renderPrettyMs": 0.6935839999998734, + "isBundleMicros": 7.2920000000067375 + }, + { + "iteration": 17, + "projectMs": 0.6215829999998732, + "renderObjectMs": 0.43716700000004494, + "renderPrettyMs": 0.5997079999999642, + "isBundleMicros": 6.333999999924345 + }, + { + "iteration": 18, + "projectMs": 0.5726669999999103, + "renderObjectMs": 0.42325000000005275, + "renderPrettyMs": 0.5977089999998952, + "isBundleMicros": 17.20799999998235 + }, + { + "iteration": 19, + "projectMs": 0.5305829999999787, + "renderObjectMs": 0.3343750000001364, + "renderPrettyMs": 0.483208999999988, + "isBundleMicros": 4.583000000138782 + }, + { + "iteration": 20, + "projectMs": 0.46612500000014734, + "renderObjectMs": 0.3254589999999098, + "renderPrettyMs": 0.4749580000000151, + "isBundleMicros": 4.083999999920707 + }, + { + "iteration": 21, + "projectMs": 0.46091599999999744, + "renderObjectMs": 0.31083300000000236, + "renderPrettyMs": 0.47891600000002654, + "isBundleMicros": 4.000000000132786 + }, + { + "iteration": 22, + "projectMs": 0.4720420000001013, + "renderObjectMs": 0.3137090000000171, + "renderPrettyMs": 0.4705420000000231, + "isBundleMicros": 4.415999999991982 + }, + { + "iteration": 23, + "projectMs": 0.4644590000000335, + "renderObjectMs": 0.31237499999997453, + "renderPrettyMs": 0.5097919999998339, + "isBundleMicros": 4.8750000000836735 + }, + { + "iteration": 24, + "projectMs": 0.5452080000000024, + "renderObjectMs": 0.3479589999999462, + "renderPrettyMs": 0.5336250000000291, + "isBundleMicros": 5.791999999928521 + }, + { + "iteration": 25, + "projectMs": 0.5418329999999969, + "renderObjectMs": 0.37179199999991397, + "renderPrettyMs": 0.4988749999999982, + "isBundleMicros": 3.6250000000563887 + }, + { + "iteration": 26, + "projectMs": 0.48054099999990285, + "renderObjectMs": 0.315541999999823, + "renderPrettyMs": 0.47345900000004804, + "isBundleMicros": 2.958000000035099 + }, + { + "iteration": 27, + "projectMs": 0.4846669999999449, + "renderObjectMs": 0.371167000000014, + "renderPrettyMs": 0.46937500000012733, + "isBundleMicros": 5.000000000109139 + }, + { + "iteration": 28, + "projectMs": 0.4574579999998605, + "renderObjectMs": 0.37962500000003274, + "renderPrettyMs": 0.48433399999998983, + "isBundleMicros": 3.292000000101325 + }, + { + "iteration": 29, + "projectMs": 0.45537500000000364, + "renderObjectMs": 0.35874999999987267, + "renderPrettyMs": 0.49966699999981756, + "isBundleMicros": 8.666999999832115 + }, + { + "iteration": 30, + "projectMs": 0.4595839999999498, + "renderObjectMs": 0.3423750000001746, + "renderPrettyMs": 0.5145410000000084, + "isBundleMicros": 2.958000000035099 + }, + { + "iteration": 31, + "projectMs": 0.45566699999994853, + "renderObjectMs": 0.31904199999985394, + "renderPrettyMs": 0.5048749999998563, + "isBundleMicros": 2.7919999999994616 + }, + { + "iteration": 32, + "projectMs": 0.4547909999998865, + "renderObjectMs": 0.31500000000005457, + "renderPrettyMs": 0.5202919999999267, + "isBundleMicros": 2.9170000000249274 + }, + { + "iteration": 33, + "projectMs": 0.48745800000006057, + "renderObjectMs": 0.3213749999999891, + "renderPrettyMs": 0.55987499999992, + "isBundleMicros": 6.374999999934516 + }, + { + "iteration": 34, + "projectMs": 0.5102500000000418, + "renderObjectMs": 0.3493340000002263, + "renderPrettyMs": 0.5046250000000327, + "isBundleMicros": 3.9999999999054126 + }, + { + "iteration": 35, + "projectMs": 0.4591249999998581, + "renderObjectMs": 0.3097499999998945, + "renderPrettyMs": 0.4698750000000018, + "isBundleMicros": 2.7500000001055014 + }, + { + "iteration": 36, + "projectMs": 0.4632080000001224, + "renderObjectMs": 0.31654200000002675, + "renderPrettyMs": 0.47070899999994253, + "isBundleMicros": 2.8750000001309672 + }, + { + "iteration": 37, + "projectMs": 0.4883749999999054, + "renderObjectMs": 0.3182919999999285, + "renderPrettyMs": 0.4840409999999338, + "isBundleMicros": 2.958000000035099 + }, + { + "iteration": 38, + "projectMs": 0.46916699999997036, + "renderObjectMs": 0.32024999999998727, + "renderPrettyMs": 0.5315840000000662, + "isBundleMicros": 4.208000000062384 + }, + { + "iteration": 39, + "projectMs": 0.4754169999998794, + "renderObjectMs": 0.37687499999992724, + "renderPrettyMs": 0.4812500000000455, + "isBundleMicros": 3.165999999964697 + }, + { + "iteration": 40, + "projectMs": 0.5542920000000322, + "renderObjectMs": 0.320165999999972, + "renderPrettyMs": 0.4702500000000782, + "isBundleMicros": 2.8340000001207954 + } + ] } diff --git a/packages/architect-projection/tests/perf/compare-baseline.mjs b/packages/architect-projection/tests/perf/compare-baseline.mjs index 7c41321..75e9256 100644 --- a/packages/architect-projection/tests/perf/compare-baseline.mjs +++ b/packages/architect-projection/tests/perf/compare-baseline.mjs @@ -27,8 +27,25 @@ const HOT_PATH_BUDGETS = { graphBuild: { field: 'avgMs', budget: 2000, unit: 'ms' }, }; +const RENDER_MARKDOWN_BUNDLE_BUDGETS = { + patterns: { field: 'avgMs', budget: 1, unit: 'ms' }, + decisions: { field: 'avgMs', budget: 1, unit: 'ms' }, + 'requirements-executable': { field: 'avgMs', budget: 1, unit: 'ms' }, +}; + const BASELINE_MULTIPLIER = 1.5; +/** + * Absolute noise headroom added on top of the relative (1.5x) budget, per unit + * (~50 microseconds). Relative gating is meaningless for microsecond-scale + * operations: a few µs of timer/scheduler jitter is a large *relative* swing on + * a 10 µs op but is not a real regression. The effective budget is therefore + * `max(baseline * 1.5, baseline + slack)` — tiny metrics get absolute headroom + * while large metrics (e.g. graphBuild ~376 ms) stay governed by the tight 1.5x + * relative gate. The hard budget still caps everything above. + */ +const ABSOLUTE_SLACK_BY_UNIT = { ms: 0.05, us: 50 }; + const [report, baseline] = await Promise.all([ readJson(reportPath, 'perf report'), readJson(baselinePath, 'perf baseline'), @@ -40,6 +57,7 @@ const failures = [ checkAverageMetric('renderPretty'), checkScalarMetric('isBundleP50Micros'), ...Object.keys(HOT_PATH_BUDGETS).map((metricName) => checkHotPathAverageMetric(metricName)), + ...checkRenderMarkdownBundleMetrics(report), ].filter((failure) => failure !== undefined); if (failures.length > 0) { @@ -60,68 +78,106 @@ async function readJson(filePath, label) { function checkAverageMetric(metricName) { const budget = HARD_BUDGETS[metricName]; - const actual = getMetricValue(report, metricName, budget.field); - const baselineValue = getMetricValue(baseline, metricName, budget.field); - const baselineBudget = baselineValue * BASELINE_MULTIPLIER; - const allowed = Math.min(budget.budget, baselineBudget); const label = `${metricName}.${budget.field}`; - if (actual > allowed) { - console.error( - `FAIL ${label}: ${format(actual, budget.unit)} exceeds ${format(allowed, budget.unit)} ` + - `(hard ${format(budget.budget, budget.unit)}, baseline ${format(baselineBudget, budget.unit)})` - ); - return `${label} ${format(actual, budget.unit)} > ${format(allowed, budget.unit)}`; - } - - console.log( - `PASS ${label}: ${format(actual, budget.unit)} <= ${format(allowed, budget.unit)} ` + - `(hard ${format(budget.budget, budget.unit)}, baseline ${format(baselineBudget, budget.unit)})` - ); - return undefined; + return checkBudget({ + label, + actual: getMetricValue(report, metricName, budget.field), + baselineValue: getMetricValue(baseline, metricName, budget.field), + hardBudget: budget.budget, + unit: budget.unit, + }); } function checkScalarMetric(metricName) { const budget = HARD_BUDGETS[metricName]; - const actual = getNumber(report, metricName); - const baselineValue = getNumber(baseline, metricName); - const baselineBudget = baselineValue * BASELINE_MULTIPLIER; - const allowed = Math.min(budget.budget, baselineBudget); - - if (actual > allowed) { - console.error( - `FAIL ${metricName}: ${format(actual, budget.unit)} exceeds ${format(allowed, budget.unit)} ` + - `(hard ${format(budget.budget, budget.unit)}, baseline ${format(baselineBudget, budget.unit)})` - ); - return `${metricName} ${format(actual, budget.unit)} > ${format(allowed, budget.unit)}`; - } - console.log( - `PASS ${metricName}: ${format(actual, budget.unit)} <= ${format(allowed, budget.unit)} ` + - `(hard ${format(budget.budget, budget.unit)}, baseline ${format(baselineBudget, budget.unit)})` - ); - return undefined; + return checkBudget({ + label: metricName, + actual: getNumber(report, metricName), + baselineValue: getNumber(baseline, metricName), + hardBudget: budget.budget, + unit: budget.unit, + }); } function checkHotPathAverageMetric(metricName) { const budget = HOT_PATH_BUDGETS[metricName]; - const actual = getMetricValue(report.projectionHotPaths, metricName, budget.field); - const baselineValue = getMetricValue(baseline.projectionHotPaths, metricName, budget.field); - const baselineBudget = baselineValue * BASELINE_MULTIPLIER; - const allowed = Math.min(budget.budget, baselineBudget); const label = `projectionHotPaths.${metricName}.${budget.field}`; - if (actual > allowed) { + return checkBudget({ + label, + actual: getMetricValue(report.projectionHotPaths, metricName, budget.field), + baselineValue: getMetricValue(baseline.projectionHotPaths, metricName, budget.field), + hardBudget: budget.budget, + unit: budget.unit, + }); +} + +function checkRenderMarkdownBundleMetrics(source) { + const expectedDocumentTypes = Object.keys(RENDER_MARKDOWN_BUNDLE_BUDGETS); + const bundles = source.renderMarkdownBundles; + + if (bundles === undefined || typeof bundles !== 'object' || bundles === null) { + throw new Error('Missing renderMarkdownBundles section in perf report'); + } + + const actualDocumentTypes = Object.keys(bundles).sort(); + const expectedSortedDocumentTypes = [...expectedDocumentTypes].sort(); + + if (JSON.stringify(actualDocumentTypes) !== JSON.stringify(expectedSortedDocumentTypes)) { + throw new Error( + `Expected renderMarkdownBundles for ${expectedSortedDocumentTypes.join(', ')}, got ${actualDocumentTypes.join(', ')}` + ); + } + + return expectedDocumentTypes.map((documentType) => { + const budget = RENDER_MARKDOWN_BUNDLE_BUDGETS[documentType]; + const label = `renderMarkdownBundles.${documentType}.${budget.field}`; + + assertMetricFieldsPresent(bundles, documentType, ['p50Ms', 'iterations']); + + return checkBudget({ + label, + actual: getMetricValue(bundles, documentType, budget.field), + baselineValue: getMetricValue(baseline.renderMarkdownBundles, documentType, budget.field), + hardBudget: budget.budget, + unit: budget.unit, + }); + }); +} + +function assertMetricFieldsPresent(metricsHost, key, fields) { + for (const field of fields) { + getMetricValue(metricsHost, key, field); + } +} + +/** + * @param {object} args + * @param {string} args.label + * @param {number} args.actual + * @param {number} args.baselineValue + * @param {number} args.hardBudget + * @param {string} args.unit + * @returns {string | undefined} + */ +function checkBudget({ label, actual, baselineValue, hardBudget, unit }) { + const slack = ABSOLUTE_SLACK_BY_UNIT[unit] ?? 0; + const baselineBudget = Math.max(baselineValue * BASELINE_MULTIPLIER, baselineValue + slack); + const effectiveBudget = Math.min(hardBudget, baselineBudget); + + if (actual > effectiveBudget) { console.error( - `FAIL ${label}: ${format(actual, budget.unit)} exceeds ${format(allowed, budget.unit)} ` + - `(hard ${format(budget.budget, budget.unit)}, baseline ${format(baselineBudget, budget.unit)})` + `FAIL ${label}: ${format(actual, unit)} exceeds ${format(effectiveBudget, unit)} ` + + `(hard ${format(hardBudget, unit)}, baseline ${format(baselineBudget, unit)})` ); - return `${label} ${format(actual, budget.unit)} > ${format(allowed, budget.unit)}`; + return `${label} ${format(actual, unit)} > ${format(effectiveBudget, unit)}`; } console.log( - `PASS ${label}: ${format(actual, budget.unit)} <= ${format(allowed, budget.unit)} ` + - `(hard ${format(budget.budget, budget.unit)}, baseline ${format(baselineBudget, budget.unit)})` + `PASS ${label}: ${format(actual, unit)} <= ${format(effectiveBudget, unit)} ` + + `(hard ${format(hardBudget, unit)}, baseline ${format(baselineBudget, unit)})` ); return undefined; } diff --git a/packages/architect-projection/tests/support/test-graph-builder.ts b/packages/architect-projection/tests/support/test-graph-builder.ts index 6654395..53e5748 100644 --- a/packages/architect-projection/tests/support/test-graph-builder.ts +++ b/packages/architect-projection/tests/support/test-graph-builder.ts @@ -33,23 +33,17 @@ export interface PatternStubOptions { readonly status?: ExtractedPattern['status']; readonly maturity?: PatternMaturity; readonly role?: ExtractedPattern['role']; - readonly phase?: ExtractedPattern['phase']; - readonly quarter?: ExtractedPattern['quarter']; - readonly release?: ExtractedPattern['release']; - readonly completed?: ExtractedPattern['completed']; readonly file?: string; readonly description?: string; readonly boundedContext?: ExtractedPattern['boundedContext']; readonly adrLayer?: ExtractedPattern['adrLayer']; + readonly adrTheme?: ExtractedPattern['adrTheme']; readonly archContext?: string; readonly archLayer?: string; + readonly archTheme?: string; readonly productArea?: ExtractedPattern['productArea']; - readonly userRole?: ExtractedPattern['userRole']; - readonly businessValue?: ExtractedPattern['businessValue']; readonly team?: ExtractedPattern['team']; - readonly effort?: ExtractedPattern['effort']; - readonly effortActual?: ExtractedPattern['effortActual']; - readonly priority?: ExtractedPattern['priority']; + readonly workflow?: ExtractedPattern['workflow']; readonly deliverables?: ExtractedPattern['deliverables']; readonly executableSpecs?: ExtractedPattern['executableSpecs']; readonly behaviorFile?: ExtractedPattern['behaviorFile']; @@ -59,7 +53,7 @@ export interface PatternStubOptions { readonly usedBy?: readonly string[]; readonly enables?: readonly string[]; readonly implementsPatterns?: ExtractedPattern['implementsPatterns']; - readonly useCases?: readonly string[]; + readonly enforcesDecisions?: ExtractedPattern['enforcesDecisions']; readonly rules?: readonly BusinessRuleStubOptions[]; readonly adr?: ExtractedPattern['adr']; readonly adrStatus?: ExtractedPattern['adrStatus']; @@ -72,12 +66,12 @@ export interface PatternStubOptions { readonly level?: ExtractedPattern['level']; readonly parent?: ExtractedPattern['parent']; readonly children?: ExtractedPattern['children']; + readonly extractedShapes?: ExtractedPattern['extractedShapes']; } export interface GraphBuilderOptions { readonly patterns: readonly ExtractedPattern[]; readonly tagRegistry: TagRegistry; - readonly phaseNames?: Record | undefined; readonly relationshipIndex?: Record | undefined; readonly includeArchIndex?: boolean; } @@ -114,23 +108,18 @@ export function buildPatternStub(name: string, options: PatternStubOptions = {}) exports: [], extractedAt: '2026-04-19T00:00:00.000Z', status: options.status ?? 'active', - ...(options.phase !== undefined ? { phase: options.phase } : {}), - ...(options.quarter !== undefined ? { quarter: options.quarter } : {}), - ...(options.release !== undefined ? { release: options.release } : {}), - ...(options.completed !== undefined ? { completed: options.completed } : {}), ...(options.boundedContext !== undefined || options.archContext !== undefined ? { boundedContext: options.boundedContext ?? options.archContext } : {}), ...(options.adrLayer !== undefined || options.archLayer !== undefined ? { adrLayer: options.adrLayer ?? options.archLayer } : {}), + ...(options.adrTheme !== undefined || options.archTheme !== undefined + ? { adrTheme: options.adrTheme ?? options.archTheme } + : {}), ...(options.productArea !== undefined ? { productArea: options.productArea } : {}), - ...(options.userRole !== undefined ? { userRole: options.userRole } : {}), - ...(options.businessValue !== undefined ? { businessValue: options.businessValue } : {}), ...(options.team !== undefined ? { team: options.team } : {}), - ...(options.effort !== undefined ? { effort: options.effort } : {}), - ...(options.effortActual !== undefined ? { effortActual: options.effortActual } : {}), - ...(options.priority !== undefined ? { priority: options.priority } : {}), + ...(options.workflow !== undefined ? { workflow: options.workflow } : {}), ...(options.deliverables !== undefined ? { deliverables: options.deliverables } : {}), ...(options.executableSpecs !== undefined ? { executableSpecs: options.executableSpecs } : {}), ...(options.behaviorFile !== undefined ? { behaviorFile: options.behaviorFile } : {}), @@ -139,7 +128,9 @@ export function buildPatternStub(name: string, options: PatternStubOptions = {}) ...(options.implementsPatterns !== undefined ? { implementsPatterns: options.implementsPatterns } : {}), - ...(options.useCases !== undefined ? { useCases: [...options.useCases] } : {}), + ...(options.enforcesDecisions !== undefined + ? { enforcesDecisions: options.enforcesDecisions } + : {}), ...(options.rules !== undefined ? { rules: options.rules.map((rule) => ({ @@ -161,6 +152,7 @@ export function buildPatternStub(name: string, options: PatternStubOptions = {}) ...(options.level !== undefined ? { level: options.level } : {}), ...(options.parent !== undefined ? { parent: options.parent } : {}), ...(options.children !== undefined ? { children: options.children } : {}), + ...(options.extractedShapes !== undefined ? { extractedShapes: options.extractedShapes } : {}), ...(options.maturity !== undefined ? { maturity: options.maturity } : {}), ...(options.dependsOn !== undefined ? { dependsOn: options.dependsOn } : {}), ...(options.usedBy !== undefined ? { usedBy: options.usedBy } : {}), @@ -169,21 +161,12 @@ export function buildPatternStub(name: string, options: PatternStubOptions = {}) } export function buildGraphFromPatterns(options: GraphBuilderOptions): PatternGraph { - const { - patterns, - tagRegistry, - phaseNames = {}, - relationshipIndex, - includeArchIndex = false, - } = options; + const { patterns, tagRegistry, relationshipIndex, includeArchIndex = false } = options; const completed = patterns.filter((pattern) => pattern.status === 'completed'); const active = patterns.filter((pattern) => pattern.status === 'active'); const roadmap = patterns.filter((pattern) => pattern.status === 'roadmap'); const deferred = patterns.filter((pattern) => pattern.status === 'deferred'); const candidate = patterns.filter((pattern) => pattern.status === 'candidate'); - const phases = patterns - .map((pattern) => pattern.phase) - .filter((phase): phase is number => phase !== undefined); const roles = patterns .map((pattern) => pattern.role) .filter((role): role is string => role !== undefined && role.length > 0); @@ -208,19 +191,13 @@ export function buildGraphFromPatterns(options: GraphBuilderOptions): PatternGra candidate: [...candidate], }, byMaturity, - byPhase: buildPhaseGroups(patterns, phaseNames), - byQuarter: buildQuarterGroups(patterns), byRole: buildRoleGroups(patterns), bySourceType: { typescript: patterns.filter((pattern) => !pattern.source.file.endsWith('.feature')), gherkin: patterns.filter((pattern) => pattern.source.file.endsWith('.feature')), roadmap: [], prd: patterns.filter( - (pattern) => - pattern.adr === undefined && - (pattern.productArea !== undefined || - pattern.userRole !== undefined || - pattern.businessValue !== undefined) + (pattern) => pattern.adr === undefined && pattern.productArea !== undefined, ), }, byProductArea: buildProductAreaIndex(patterns), @@ -231,7 +208,6 @@ export function buildGraphFromPatterns(options: GraphBuilderOptions): PatternGra candidate: patterns.filter((pattern) => pattern.status === 'candidate').length, total: patterns.length, }, - phaseCount: new Set(phases).size, roleCount: new Set(roles).size, relationshipIndex: derivedRelationshipIndex, ...(includeArchIndex ? { archIndex: createArchIndex(patterns) } : {}), @@ -254,55 +230,6 @@ function buildMaturityGroups(patterns: readonly ExtractedPattern[]): PatternGrap return groups; } -function buildPhaseGroups( - patterns: readonly ExtractedPattern[], - phaseNames: Record -): PatternGraph['byPhase'] { - const grouped = new Map(); - - for (const pattern of patterns) { - if (pattern.phase === undefined) { - continue; - } - - const bucket = grouped.get(pattern.phase) ?? []; - bucket.push(pattern); - grouped.set(pattern.phase, bucket); - } - - return [...grouped.entries()] - .sort(([left], [right]) => left - right) - .map(([phaseNumber, phasePatterns]) => ({ - phaseNumber, - phaseName: phaseNames[phaseNumber], - patterns: [...phasePatterns], - counts: { - completed: phasePatterns.filter((pattern) => isPatternComplete(pattern.status)).length, - active: phasePatterns.filter((pattern) => isPatternActive(pattern.status)).length, - planned: phasePatterns.filter((pattern) => isPatternPlanned(pattern.status)).length, - candidate: phasePatterns.filter((pattern) => pattern.status === 'candidate').length, - total: phasePatterns.length, - }, - })) as PatternGraph['byPhase']; -} - -function buildQuarterGroups(patterns: readonly ExtractedPattern[]): PatternGraph['byQuarter'] { - const grouped: Record = {}; - - for (const pattern of patterns) { - const quarter = pattern.quarter?.trim(); - if (!quarter) { - continue; - } - - const bucket = grouped[quarter] ?? []; - bucket.push(pattern); - grouped[quarter] = bucket; - } - - return grouped; -} - function buildRoleGroups(patterns: readonly ExtractedPattern[]): PatternGraph['byRole'] { const grouped: Record = {}; @@ -322,7 +249,7 @@ function buildRoleGroups(patterns: readonly ExtractedPattern[]): PatternGraph['b function buildRelationshipIndex( patterns: readonly ExtractedPattern[], - overrides: Record | undefined + overrides: Record | undefined, ): Record { const index: Record = {}; @@ -343,6 +270,8 @@ function buildRelationshipIndex( extendedBy: override?.extendedBy ?? [], seeAlso: override?.seeAlso ?? [...(pattern.seeAlso ?? [])], apiRef: override?.apiRef ?? [...(pattern.apiRef ?? [])], + enforcesDecisions: override?.enforcesDecisions ?? [...(pattern.enforcesDecisions ?? [])], + enforcedBy: override?.enforcedBy ?? [], }; } @@ -379,6 +308,7 @@ function createArchIndex(patterns: readonly ExtractedPattern[]): PatternGraph['a byContext, byLayer, byView: {}, + byPackage: {}, all: [...patterns], }; } @@ -392,7 +322,7 @@ function getPatternName(pattern: ExtractedPattern): string { } function buildProductAreaIndex( - patterns: readonly ExtractedPattern[] + patterns: readonly ExtractedPattern[], ): PatternGraph['byProductArea'] { const grouped: Record = {}; diff --git a/packages/architect/PRD.md b/packages/architect/PRD.md new file mode 100644 index 0000000..acf8209 --- /dev/null +++ b/packages/architect/PRD.md @@ -0,0 +1,116 @@ +# architect (shell / composition root) — Package PRD + +> Scope: the "shell" — the bin-only meta package `@libar-dev/architect` (`packages/architect/`) plus the workspace composition root (root `package.json`, `architect.config.ts`, `pnpm-workspace.yaml`, `tsconfig.architect-base.json`, `eslint.config.mjs`) and the repo's dogfood/self-hosting wiring. Recorded from code/config as-is, not from annotations. + +## Purpose + +The shell is the **assembly layer** that turns five independently published runtime packages into one installable, runnable toolchain and one self-hosting dev environment. It does two distinct jobs. As a **distribution artifact**, the meta package `@libar-dev/architect` (`packages/architect/package.json`) installs the whole family in one dependency and re-exposes all 7 CLI/MCP bins — bin-only, no JS API. As a **composition root**, the repo root wires the workspace (`pnpm-workspace.yaml`), the shared strict-TS base (`tsconfig.base.json` → `tsconfig.architect-base.json`), the lint doctrine (`eslint.config.mjs`), and a script surface (root `package.json`) that dispatches to the package-owned bins, and it hosts the dogfood delivery-process instance (`architect.config.ts` + `architect/` + `tests/` + `docs-live/`) that runs the toolchain against this repo itself. + +## Public interface + +### Bins (7) — meta package re-exposes, owner packages implement + +The meta package's bin shims (`packages/architect/bin/*.js`) are one-line re-exports; the implementation lives in the owner package's own `./bin/` export. + +| Bin | Owner package | Shim re-exports | +| ------------------------- | -------------------------- | ------------------------------------------- | +| `architect` | `@libar-dev/architect-cli` | `architect-cli/bin/architect` | +| `architect-generate` | `@libar-dev/architect-cli` | `architect-cli/bin/architect-generate` | +| `architect-guard` | `@libar-dev/architect-cli` | `architect-cli/bin/architect-guard` | +| `architect-lint-patterns` | `@libar-dev/architect-cli` | `architect-cli/bin/architect-lint-patterns` | +| `architect-lint-steps` | `@libar-dev/architect-cli` | `architect-cli/bin/architect-lint-steps` | +| `architect-validate` | `@libar-dev/architect-cli` | `architect-cli/bin/architect-validate` | +| `architect-mcp` | `@libar-dev/architect-mcp` | `architect-mcp/bin/architect-mcp` | + +So 6 of 7 bins are owned by `architect-cli`; only `architect-mcp` is owned by `architect-mcp`. The CLI and MCP composition-root internals are out of scope here (other agents cover them). + +### Root script surface (`package.json`, 31 scripts) + +Scripts dispatch to package owners via `pnpm exec architect-` or run the dogfood CLI through `tsx` against `packages/architect-cli/src`. Grouped by intent: + +- **build / typecheck / lint / test** — `build`, `typecheck`, `lint`, `test` fan out across `./packages/**` via `pnpm -r --filter`; `typecheck:dogfood` (`tsc -b tsconfig.json`) and `test:dogfood` (`vitest run`) compile/test the repo-root dogfood instance; `smoke` (`tsx scripts/workspace-smoke.ts`), `clean`, `format`, `format:check`. +- **query** — `architect:q` (the graph handle eval front door) and `architect:graph` (named commands + the `dangling` CI gate), both `tsx --conditions=source ... graph-cli.ts --base-dir .` (ADR-014). +- **guard** — `architect:guard` (`--staged`), `architect:guard:all` (`--all`), `architect:lint-steps`; validation pair `validate:patterns`, `validate:all` (`--dod --anti-patterns`). +- **docs** — `docs:patterns`, `docs:architecture`, `docs:roadmap`, `docs:taxonomy`, `docs:api-reference`, and `docs:all` (`architect-generate --base-dir . --all -f`) → regenerates git-tracked `docs-live/`. +- **release / ci-adjacent** — `changeset`, `changeset:version`, `changeset:publish`, `release`; doctrine guards `audit:subtractive`, `guard:no-suppressions`, `check:skills`. + +> Note: the `pkg:*` and `ci:architect:*` script families referenced in some planning context **do not exist** in the current root `package.json`. The live surface is leaner than briefed; CI presumably invokes the existing scripts directly. + +### Config contract — `architect.config.ts` (49 lines) + +`export default defineConfig({ ... })` where `defineConfig` is owned by `@libar-dev/architect-core` (`src/config/define-config.ts`, re-exported from the package root and `./config`). The dogfood config consumes core-owned constants rather than hand-authoring values: + +- `roles: ARCHITECT_PACKAGE_ROLES` — the 8-role enum (sourced from `architect-core/src/config/self-hosting.ts`, shared with the static `WORKSPACE_TAG_REGISTRY`). +- `productAreas: ARCHITECT_PACKAGE_PRODUCT_AREAS`. +- `sources: { typescript, stubs, features }` — spread from `PACKAGE_SELF_HOSTING_SOURCES`. +- `output: { directory: 'docs-live', overwrite: true }`. +- `generators: DEFAULT_GENERATORS`. +- `packages: [...]` — 7 display-grouping entries with `match` globs/regexes (5 runtime packages + `architect-dev` = `tests/features/` + `architect-pkg-content` = `architect/`). + +Consumers in other repos supply their own `architect.config.ts` of the same shape; this file is the dogfood instance. + +### Shared TS base + +`tsconfig.architect-base.json` extends `tsconfig.base.json` and adds `noPropertyAccessFromIndexSignature: true`. The base enforces the strict doctrine: `strict`, `verbatimModuleSyntax`, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `isolatedModules`, `declaration`+`declarationMap`, `module: ESNext` / `moduleResolution: bundler`. `eslint.config.mjs` layers `strictTypeChecked` + `stylisticTypeChecked`, a local `no-suppression-comments` rule (No-BC enforcement, production `src` only), and `architect-projection` boundary import rules. + +## Enumerated functionality + +- **Bin composition** — 7 thin re-export shims in `packages/architect/bin/`; the meta `package.json` `bin` map points at them; owner packages (`architect-cli` ×6, `architect-mcp` ×1) carry the real entrypoints via their own `./bin/*` exports. +- **Script dispatch** — root `package.json` is the human/CI entrypoint; `pnpm exec architect-` resolves to the meta/owner bin, or `tsx` runs the CLI source directly (dogfood uses source, not built dist). +- **Config loading** — `defineConfig` (core-owned) validates and types `architect.config.ts`; the dogfood config pulls roles/areas/sources/generators from `architect-core` constants so taxonomy stays single-sourced. +- **Workspace / build wiring** — `pnpm-workspace.yaml` globs `packages/*` + `formal-spec`; `pnpm@10.4.1` pinned; recursive filtered build/test; shared tsconfig base + flat ESLint config + Prettier. +- **Dogfood / self-hosting** — `architect.config.ts` + `architect/` working state (specs, decisions, stubs, step-stubs, slices, ideations, design-reviews) + `tests/` (executable Gherkin under `tests/features/`, steps, support, fixtures) + `docs-live/` (git-tracked generated output, determinism-gate diff target) + `scripts/` (smoke, validate-workspace, generate-docs, subtractive audit, no-suppressions guard, skill-symlink check). +- **Formal-spec** — `formal-spec/` is the `@libar-dev/architect-spec` v0.2.0 methodology RFC (private, `*.md` only, 13 numbered chapters + appendix). A workspace member for tooling, but ships no code; it is the spec the package family is the reference implementation of. + +## Dependencies + +Family dependency graph (strictly acyclic; confirmed via the graph handle and each package's `dependencies`): + +``` +architect-core (leaf — no @libar-dev deps; deps: @cucumber/gherkin, typescript-estree, glob, zod) + ▲ ▲ ▲ + │ │ └──────────── architect-guard → core + │ └────── architect-projection → core + │ ▲ + ├── architect-cli → core, guard, projection + └── architect-mcp → core, projection (+ @modelcontextprotocol/sdk, chokidar) + +architect (meta) → core, projection, guard, cli, mcp (workspace:* — install-everything) +``` + +`architect-core` is the single sink; nothing depends on `cli`, `mcp`, or the meta package internally. The meta package depends on all five (so installing it installs the family). The repo-root `package.json` depends on `architect-core` + `architect-guard` (runtime) and dev-depends on cli/mcp/projection. + +Notable external tooling: **pnpm** (workspaces, pinned `10.4.1`), **tsx** (run CLI source directly), **vitest** + `@vitest/coverage-v8` + `@amiceli/vitest-cucumber` (executable Gherkin tests), **typescript** + **typescript-eslint** + **eslint** + **eslint-plugin-import** + **eslint-config-prettier** + **prettier**, **@changesets/cli** (release), **zod** (boundary contracts). Each owner package builds with its own bundler (per-package `build` scripts, not centralized here). + +## Consumers + +- **Developers** — run the dogfood scripts (`pnpm architect:q`, `architect:graph`, `architect:guard`, `validate:all`, `docs:all`) against this repo. +- **CI** — invokes build/typecheck/lint/test, the guards (`guard:no-suppressions`, `audit:subtractive`, `check:skills`), the docs determinism gate (`docs:all` + `git diff --exit-code docs-live`), and changesets release. +- **Agents / harnesses** — Codex, Claude Code, OpenCode reach the toolchain through `pnpm architect:q` (the graph handle) and the `architect-mcp` server. +- **Studio / desktop (proprietary)** — consume the same projections the shell exposes. +- **Consuming repos** — install `@libar-dev/architect` (or the granular splits for a narrower footprint), wire their own `architect.config.ts` of the same shape, and expose their own `architect:q` / `architect:graph` scripts. + +## Load-bearing vs incidental (cut-list) + +### Load-bearing — must stay + +- **The meta `package.json` bin map + the 7 shim files** — the entire reason the meta package exists (single-install distribution of the family's bins). Bin-only is a deliberate v1→v2 contract (no JS barrel). +- **`defineConfig` + `architect.config.ts` shape** — the stable public config contract every architect-managed repo wires; single-sources taxonomy from `architect-core` constants. +- **`pnpm-workspace.yaml` + `tsconfig.base.json`/`tsconfig.architect-base.json` + the No-BC ESLint rule** — the acyclic-build + strict-type + no-suppression doctrine the whole family depends on. +- **`docs-live/` generation wiring (`docs:all`) + the dogfood `architect/`+`tests/` instance** — the self-hosting proof and the determinism gate; this is the product validating itself. + +### Incidental / deletion-candidate — specific + +- **Highest-confidence cut — the per-doc `docs:*` scripts (`docs:patterns`, `docs:architecture`, `docs:roadmap`, `docs:taxonomy`, `docs:api-reference`).** Five single-generator wrappers around `architect-generate -g -f` that `docs:all` already subsumes. As the projection pipeline collapses the documentType-first star into source-first Views over one engine, per-documentType invocation scripts are exactly the accreted surface that should disappear; keep `docs:all` only. +- **`tsconfig.architect-base.json` adds a single flag** (`noPropertyAccessFromIndexSignature`) over `tsconfig.base.json`. Two base files for one extra option is borderline; the flag could fold into `tsconfig.base.json` and the extra file be deleted — verify no package extends only the plain base first. +- (The former `architect:overview` / `architect:status` convenience aliases were removed with the verb CLI — ADR-014.) +- **Naming drift to fix, not necessarily cut:** a bin named `architect-lint-patterns` exists, but the wired root script is `validate:patterns` (→ `architect-validate`), and `architect:lint-steps` wraps `architect-lint-steps`. The `lint-patterns` bin has no root-script entrypoint — confirm it is still reached (e.g. by the guard pipeline) or it is a dangling bin. +- **Planning-context script families `pkg:*` and `ci:architect:*` do not exist** in the current root `package.json` — no cut needed, but any doc/skill claiming they exist is stale and should be corrected. + +## Size signal + +- **Packages:** 6 in `packages/` (`architect` meta + 5 runtime: core, projection, guard, cli, mcp) + 1 workspace member `formal-spec` (`@libar-dev/architect-spec`, docs-only). pnpm workspace globs `packages/*` + `formal-spec`. +- **Root scripts:** 31. +- **Bins:** 7 (6 cli-owned, 1 mcp-owned). +- **Config size:** `architect.config.ts` ≈ 49 lines (mostly the 7-entry `packages` display map); `tsconfig.base.json` ≈ 28 lines, `tsconfig.architect-base.json` ≈ 8 lines, `eslint.config.mjs` ≈ 435 lines (the large surface is `architect-projection` import-boundary rules, not generic shell config), `pnpm-workspace.yaml` 3 lines. +- **Pattern-graph scale (dogfood, from the graph handle; drifts with every annotation):** ~350 patterns — re-derive live via `pnpm architect:graph census`. diff --git a/packages/architect/package.json b/packages/architect/package.json index e196004..df6335e 100644 --- a/packages/architect/package.json +++ b/packages/architect/package.json @@ -30,6 +30,10 @@ "architect-validate": "./bin/architect-validate.js", "architect-mcp": "./bin/architect-mcp.js" }, + "scripts": { + "test": "node ../../scripts/architect-bin-smoke.mjs", + "prepack": "node ../../scripts/architect-bin-smoke.mjs" + }, "dependencies": { "@libar-dev/architect-cli": "workspace:*", "@libar-dev/architect-core": "workspace:*", diff --git a/plans/annotation-coverage-campaign.md b/plans/annotation-coverage-campaign.md new file mode 100644 index 0000000..de765cd --- /dev/null +++ b/plans/annotation-coverage-campaign.md @@ -0,0 +1,134 @@ +# Plan — Annotation coverage: make the curated graph useful (not big) + +## North star + +**"Useful coverage" = the graph answers agent questions truthfully and cheaply** — a +file's owner resolves (`g.byFile`), a seam groups (`boundedContext`), impact reaches what +matters (`blastRadius` + curated edges), guarantees surface (`invariantsOf`), and noise +doesn't drown the signal. Coverage **percentage is a diagnostic, never a target**: the +curated layer is a deliberate ~6–11% editorial selection of the import firehose +(playground/CONTEXT.md §3 — divergence from the mechanical graph is curation, not drift). +The work is **bidirectional** (CONTEXT §9.1): subtractive where over-annotated, additive +where load-bearing modules are dark. + +All numbers below are as-of-this-writing; **always re-derive live** (`pnpm architect:graph +census` / `fan-in` / `drift`, and the TRIAGE/DRIFT recipes in +`.agents/skills/architect-graph-handle/references/recipes.md`). The graph wins. + +Baseline (post-ADR-014 replacement): ~347 patterns · coverage cli 52% / core 69% / guard +52% / mcp 57% / projection 80% · edge-dark ~30% · dangling 0 (CI-gated) · `boundedContext` +absent on ~⅓ of patterns. + +## Phase 0 — instruments & policy prerequisites + +1. **Resolve the realization-edge policy** _(human, ADR-level — deferred decision from + ANNOTATION-FLEET-FINDINGS)_: should `@architect-implements` against a non-`active`/ + non-projecting target project a reverse edge (candidate node) or stay dropped? Today it + silently drops, which produced 2 dead annotations the fleet had to revert + (`RoadmapMarkdownExecutableTests`, `RequirementExecutableDigestExecutableTests`). This + gates whether those specs can return. Also reconcile the status discrepancy the fleet + flagged (the two targets are annotated `completed` in source, `roadmap` in the brief). +2. **Add the marker-tag guard lint**: a JSDoc block carrying `@architect-pattern` but + missing the leading bare `@architect` marker (or with tags after prose) silently drops + the whole node — the exact round-2 failure class. Sibling of the existing + `gherkin-tag-space-form` detector in `packages/architect-guard/src/validation/`; + error-level, unit-tested. Closes the last silent-drop class the fleet hit. +3. **Build the `deletionReady` / value-transfer view** (REVIEW-NOTES 🔭 #4; + `architect/specs/value-transfer-state.feature`, now re-pointed to the handle surface): + a pattern is `deletionReady` when its authored design-spec invariants each have an + `executable`-provenance counterpart. Sits directly on the maturity×provenance grid + `g.invariantsOf` already computes — a view + recipe first; a named `architect:graph` + command only if a second machine consumer needs the frozen contract (ADR-014 bar). + This is the **instrument for the subtractive side** (finds zombie specs and + value-transferred projections). + +## Phase 1 — subtractive (noise out) + +- **The ~57 zero-consumer projection-role patterns** (of 68; re-derive: + `pnpm architect:q 'g.patterns.filter(p => p.role === "projection" && !p.usedBy.length).length'`) + mirror the documentType-first star the `DocumentationProjection` epic is folding down. + **Do not strip their annotations first** — the annotation dies _with the code_ in the + epic's subtraction; stripping early is grave-tending and hides the fold-down list. The + campaign's job: keep the TRIAGE-REMOVE shortlist current and feed it to the epic. +- **Post-ADR-014 recount**: projections whose second consumer was the retired verb CLI now + have only MCP. When the Studio Design-Review view decides what it actually reads, recount + consumers — anything left with a single markdown consumer joins the fold-down list + (ADR-010's bar, same as the epic applies). +- **Borderline-leaf watch** (fleet round-2 flags): `DeterministicFormatUtils`, then + `SlugCanonicalization` — first candidates if the curated layer tightens. + +## Phase 2 — additive (signal in), ranked by agent value + +1. **The live fan-in tail** (`pnpm architect:graph fan-in` — never a frozen list): current + top is small and flat (~4 importers: `architect-core/src/utils/runtime-helpers.ts`, + `architect-guard/src/cli/shared.ts`, `architect-guard/src/lint/steps/types.ts`) — the + assist loop has already drained the big hubs; treat remaining entries as a per-batch + pickup, not a campaign. +2. **`cli-runtime.ts` (`buildCliContext`)** — live-graph bootstrap for the handle + and the dangling gate, still node-dark. (`architect-generate` builds through its + own `buildGraph` + `createCliProjectionContext`.) High signal-per-node; annotate + as a `service` in `bounded-context:cli`. +3. **Guard (52%) before the rest**: validation rules are what agents confront when gates + fire; a dark guard subsystem means gate failures explain themselves with file spelunking + instead of `g.byFile`. MCP is small (4/7) — finish it opportunistically. +4. **G7 — `boundedContext` backfill (~⅓ absent)**: seam grouping (`A2` recipe) currently + leaves a third of patterns unplaced. Batch-fill from the package/directory mapping; + reuse existing context values (`pnpm architect:q` over the A2 grouping shows the live + vocabulary). This is the cheapest large win for "what seam am I extending?". +5. **Edge-dark re-audit (~30% of patterns carry no uses/usedBy/implementedBy)**: run + TRIAGE's ADD side — `g.graphDiff().aspirational` for authored-intent candidates and the + fan-in cross-check for load-bearing-but-edge-dark. Genuine root primitives stay + rootless **by design** (fan-in is their weight); re-audit which zero-edge contracts are + genuinely roots vs just unauthored (comma-form `@architect-uses`!). + +## Phase 3 — status & spec hygiene (the truthfulness axis) + +- **DRIFT push** (REVIEW-NOTES 🔭 #3): the DRIFT recipe lists patterns with a live test but + `status < completed`. For each: advance the status (the test already proves it) or record + why the design genuinely lags. Target: list → 0 or every entry explained. +- **F1 cohort-promotion pilot** (REVIEW-NOTES 🔭 #1): on the `reporting.feature` 7-pattern + cohort, promote per-Rule invariants to their own feature-owned patterns + (`@architect-implements:` the parent) so the spec bridge stops labeling them + cohort-ambiguous. Pilot first; roll out only if the cohort labels measurably mislead. +- **Axis split** (REVIEW-NOTES 🔭 #2, ADR-worthy, born-accepted after the pilot): represent + "realized by a live test" as a derived badge from the `@architect-implements` edge in the + real projection model, decoupled from the maturity ladder — lifting the playground's + maturity⟺provenance coherence clamp into gen-1 proper. + +## Batch protocol (every batch, non-negotiable — the fleet-verified loop) + +1. **Author** with the verified syntax rules: leading bare `@architect` marker FIRST (tags + before prose), **comma-form** `@architect-uses A, B, C` (space form silently drops the + node), colon-form tags on `.feature` files, roles from the 8-value enum, reuse existing + bounded-context values. +2. **Verify landed, live**: the handle builds fresh per call — `pnpm architect:graph census` + - `pnpm architect:q 'g.pattern("")'` immediately; a node either materialized or it + didn't (no rebuild step, no silent failure window). +3. **Significance triage** every batch (the fleet held 92% and 21/21 pass bars): each node + must be a genuine seam — significance = curated edge ∨ rules/scenarios ∨ realization ∨ + enforced decision ∨ children ∨ structural role. Revert what fails. +4. **Gates stay green**: dangling 0 (`pnpm architect:graph dangling --baseline +packages/architect-guard/src/lint/dangling-baseline.json --strict` — now in `ci:verify`), + `pnpm validate:all`, typecheck. +5. **Batch size** ~20–30 nodes max; sparse and deliberate beats broad (both fleet rounds + proved small high-signal batches land at 90%+; the graph, not a quota, names the next + targets). + +## Success criteria (agent-usability probes, not percentages) + +- `g.byFile` on the current fan-in-tail files returns a **curated** answer (not the + mechanical fallback). +- The A2 seam grouping places >90% of patterns (G7 closed). +- `blast` recovered-set stays meaningful while curated downstream coverage rises. +- DRIFT list empty or every entry deliberately explained. +- `deletionReady` enumerates the epic's fold-down list mechanically. +- Dangling stays 0 across every batch (CI-enforced). + +## Explicitly out of scope + +- Chasing 100% node coverage (violates the editorial-sparsity doctrine). +- Deriving `@architect-uses` edges from imports wholesale (rebuilds the language server, + destroys curation — CONTEXT §3's core correction). +- Stripping projection annotations ahead of the epic's code deletion. +- Changing read-model edge-projection semantics as a side effect (Phase 0 #1 is a human + ADR decision first). diff --git a/plans/delivery-grouping-navigation-and-releases-report.md b/plans/delivery-grouping-navigation-and-releases-report.md new file mode 100644 index 0000000..9fb2b5c --- /dev/null +++ b/plans/delivery-grouping-navigation-and-releases-report.md @@ -0,0 +1,127 @@ +# Delivery Grouping, Navigation & Releases — Decision Report + +**Date:** 2026-06-05 +**Type:** Ideation + decision session (self-contained handoff for a fresh execution session) +**Status:** Decisions locked. No code/spec changes made this session. Mechanical work scoped for a fresh session. Release-model wiring deliberately deferred. + +> This report is self-contained: a fresh session should be able to execute the "Mechanical work" section from this file alone, without the originating conversation. + +--- + +## 1. The two questions + +**Q1 — Epics as durable navigation aids.** Because value transfer deletes design specs once they become executable (`ephemeral-spec-deletion.md`), browsing `architect/specs/` shows progressively fewer files and a human loses the thread of _which specs formed one logical unit of completed work, and where their executable specs now live_. Want: thin epics in the architect state folder as a durable navigation/reference index — also usable as a grouping key when generating docs (business-rules, requirements). + +**Q2 — Releases / phases.** The package was just extracted from a monorepo and is **not practicing releases yet**. The monorepo's release/phase machinery arrived as residue. Want: figure out how to tackle releases/phases, including whether phases are a useful _additional_ grouping for specs that together complete an epic. Guiding instinct: **less is more if we don't need it**; phases may still be valuable for _planning_ at any semver level. + +--- + +## 2. What we found — live graph (this repo) + +- **Epic→member is edge-derived.** `gherkin-extractor.ts:539` inverts each pattern's `@architect-parent` into a `parentToChildren` map. The epic's `**Members:**` prose is **not parsed anywhere** — it is pure human documentation and a drift risk (the authoritative member set is the reverse parent edges). +- **The epic already survives value transfer.** `DesignReviewProjection` is `active`, TS-owned, its design spec already deleted — yet it still carries `@architect-parent:DocumentationProjection` in JSDoc (`design-review.ts:9`). The parent edge rides to the durable surface. Idea-tier epics are not in the deletion-gate scope to begin with (the gate targets design-tier specs only). +- **`@architect-implements` is a fully traversable, bidirectional navigation edge — verified.** `DesignReviewProjection → implementedBy:[DesignReviewProjectionExecutableTests]`; `EmissionDescriptor → implementedBy:[EmissionDescriptorTesting], implementsPatterns:[TaxonomyDocumentationCluster]`. Reverse edges built at `relationship-resolver.ts:104,141–152`. (An earlier "implementedBy is missing" finding was a **JSON-path error** on our side — relationships live under `.root.relationships` for bundle-style verbs, the documented "three envelope shapes" gotcha — not a graph defect. No fix needed.) +- **The hierarchy `phase` rung is registered but unused.** Taxonomy registry has `@architect-level` (epic/phase/task/slice) + `@architect-parent`. Zero patterns use `@architect-level:phase`. +- **The temporal axis is empty.** `getAllPhases`, `getActivePhases`, `getQuarters` all return `[]`. +- **Release nodes are graph orphans.** `ReleaseV100` (completed), `ReleaseVNEXT` (active) have no edges (`arch neighborhood` empty). The changelog projection (`ReleaseNotesDigest`, `release-notes-digest.ts`) is a **Zod contract only** — it ships empty and does **not** read the release nodes. So retiring the nodes breaks nothing technically — **but see §4: vNEXT is a wanted construct, not dead weight.** +- **"Phase" is overloaded across four senses** (the key disambiguation): + 1. Hierarchy `@architect-level:phase` (epic›phase›task) — registered, **unused**. + 2. Numeric delivery-sequence `@architect-phase:N` — **cut at the registry by the Wave 1–4 taxonomy migration** (ADR-001 snapshot note), but ~8 vestigial annotations remain on test features. + 3. USDP 6-phase lifecycle (Inception→Retrospective) — **ADR-001 Rule 8**. + 4. Release-version phase ("phase N of vX") — `phase-numbering-conventions.feature`, **never built**. +- **`@architect-quarter` is still canonical** — ADR-001 Rule 6 lists it in `CANONICAL_FEATURE_ONLY_TAG_SUFFIXES`, Rule 7 defines its `YYYY-QN` format. So retiring it touches a published architect-core constant. +- **Generated docs currently group by `package`** (business-rules), not by epic. + +## 3. What we found — old repos (empirical grounding) + +Two older, far-more-populated implementations were surveyed: `libar-platform/architect/` (mature production delivery process, 49 features) and `architect-studio/.../architect/` (the extraction source, 38 features). + +**Tag frequency (the headline):** + +| Tag | libar-platform | architect-studio | +| ----------------------- | -------------- | ---------------- | +| `@architect-release` | 53 | 2 | +| `@architect-phase` | 33 | 2 | +| `@architect-quarter` | 28 | 0 | +| `@architect-implements` | 32 | 17 | +| `@architect-level` | 1 (epic) | 0 | +| `@architect-parent` | 0 | 0 | + +**Key findings:** + +- **Releases were the primary, mature, _generated_ organizing unit.** Codified in libar-platform's `adr-002-release-management-architecture`: thin (~20-line) release files + `@architect-release:` tags on deliverables + **generated changelog/roadmap**. Verbatim insight: _"releases (external versions) are what actually matter"_ and _"phases become optional internal detail, not primary organizer."_ The `CHANGELOG-GENERATED.md` was real and populated — **sourced from release files + tags, not git** (despite "git is the event store" framing). +- **Numeric phases were used heavily, then deliberately demoted** (TS phase files archived; values were messy — `100` sentinels, junk literals). +- **phase-numbering / living-roadmap-cli were aspirational** — never built. +- **Epics were barely used, never edge-derived, never durable/terminal, and bloated where used.** The current repo's edge-derived epic→members model is a **net-new invention with no prior art to restore.** (The bloat failure mode recurs: old + current epics both ballooned into design-substrate docs.) +- **`implementedBy` was first-class in the mature tooling** (reverse-resolver + rendered "## Implementations" doc section) — corroborating it is intended to work, which it does today. +- **Navigation/grouping in practice:** by release → phase → product-area, cross-linked via `implementedBy`. Epics/parents played **no** navigational role historically. + +## 4. Corrections made during the session (kept honest) + +1. **"implementedBy gap" was false** — a `.root` JSON-path parsing error. The edge works in both directions. No FEEDBACK.md entry, no fix. +2. **A heavy PDR-006 draft was written, then withdrawn** — it conflated two lineages (taxonomy + process) and was over-engineered. Removed (uncommitted, net-zero). +3. **The tag retirements belong to the taxonomy lineage** (ADR-001 / ADR-007), not a new process decision record — ADR-001 already cut `@architect-phase`; ADR-001 owns `@architect-quarter` and the USDP phases; ADR-007 (`@architect-status:active`) is the live narrowing vehicle. +4. **vNEXT and the release nodes are NOT dead weight.** vNEXT is the established "accumulate unreleased scope under a floating label, name the version once scope is clear" practice — i.e. the **forward-planning staging container**. It is currently unwired, not unwanted. Kept. +5. **`.pr-coordination/` is for non-spec-driven bootstrap work**, not the home for this. + +--- + +## 5. Decisions (locked) + +1. **Epics/slices are durable, thin, edge-derived navigation nodes.** Deletion-exempt (the value-transfer gate targets only design-tier specs). Members derived from reverse `@architect-parent` edges, never hand-listed. Design rationale that accretes during member design goes to ADRs/JSDoc — the epic stays a thin index. (Net-new doctrine; the code already proves the survival mechanism.) +2. **Two orthogonal axes.** A durable **structural hierarchy** (`epic › phase › task` via `@architect-level`/`@architect-parent`) is the read model's navigation + doc-grouping unit. A **temporal release axis** is separate. A pattern's hierarchy position never encodes when it shipped; the release axis never groups patterns structurally. (Conflating the two was the documented mistake of the pre-extraction process.) +3. **Release has a two-sided lifecycle:** + - **Planned / unreleased** (vNEXT, or a named future target) = **live read-model state** — a forward-planning grouping you assign roadmap work to _before_ cutting. This is live intent, not history, so it legitimately lives in the read model. **vNEXT is kept** as the standing staging node. This is the proven thin-node + `@architect-release` tag model. + - **Shipped** = the cut is a git event; _"history lives in git."_ The changelog is a generated projection. + - **Direction:** thin release nodes + `@architect-release` tag + generated changelog (matches both libar-platform ADR-002 and the user's vNEXT practice, and — unlike pure-git — **supports planning ahead**, since git tags are retrospective and cannot hold a not-yet-cut release). +4. **`implements` + epics is sufficient for navigation** (verified). No wiring work needed. +5. **Hierarchy `@architect-level:phase` is held in reserve** — introduced only when an epic is large enough to need an intermediate planning bucket. It is the plan-ahead / sub-epic-grouping container candidate. Costs nothing to adopt later (already registered). +6. **Numeric/quarter/USDP-phase retirement rides the taxonomy lineage**, not a process record. `@architect-phase` is already cut at the registry; the quarter + USDP-phase removal and vestigial-tag cleanup are a new ADR amending ADR-001 / extending ADR-007's active narrowing. +7. **Retire genuinely-aspirational residue:** `phase-numbering-conventions.feature`, `living-roadmap-cli.feature` (confirmed orphans, never built, old monorepo paths). + +## 6. Keep / Retire / Defer + +| | Item | Disposition | +| -------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| **Keep** | `ReleaseVNEXT` (forward-planning staging) | Keep — wire later when releases are practiced | +| **Keep** | Changelog projection contract (`ReleaseNotesDigest`) | Keep — wire to source later | +| **Keep** | Hierarchy axis; `implements` edge | Keep — working as intended | +| **Keep** | Hierarchy `@architect-level:phase` rung | Hold in reserve (unused, zero-cost) | +| **Retire** | `phase-numbering-conventions.feature`, `living-roadmap-cli.feature` | Delete (orphans, aspirational) | +| **Retire** | Vestigial `@architect-phase:N` annotations (~8 test features) | Clean up (registry tag already cut) | +| **Retire/re-scope** | `@architect-quarter` + USDP-6-phase canonical defs | Via taxonomy ADR (touches `CANONICAL_FEATURE_ONLY_TAG_SUFFIXES`, schema, `byQuarter`/`byPhase` views, `getQuarters`/`getPatternsByPhase`) | +| **Defer** | Release-model wiring (changelog source: nodes vs git; tag registration) | Decide at first practiced release | +| **Defer** | `ReleaseV100` (shipped) representation: thin node vs git tag | Decide with the release-model wiring | +| **Defer (optional)** | `grouping: by-epic` for business-rules/requirements projections | Feature work, only if wanted | + +--- + +## 7. Mechanical work for a fresh session (the handoff) + +Run the standard gates after each unit: `pnpm typecheck && pnpm test && pnpm validate:all && pnpm architect:guard --staged`, plus `pnpm docs:all && pnpm docs:check` for determinism. No-BC throughout (delete, don't shim). Commit only when the user asks. + +1. **Skills doctrine (the net-new decision).** Edit the canonical `.agents/skills/` set (`.claude`/`.codex`/`.opencode` are symlinks): + - `architect-sessions/references/ephemeral-spec-deletion.md` — add: `@architect-level:epic|slice` specs are **durable** (deletion-exempt); their members are **edge-derived** from reverse `@architect-parent`, which persists on each member's durable surface, so the epic stays an accurate index after every member's design spec is deleted. + - `architect-base/SKILL.md` §3 (folder-role table) — epic/slice lifetime = durable navigation node. + - `architect-base/references/four-tier-ladder.md` + `spec-pattern-relationships.md` — "members are edge-derived; don't hand-list names; keep the epic a thin index." + - Run `pnpm check:skills`. +2. **Slim the `DocumentationProjection` epic** (`architect/specs/documentation-projection/00-documentation-projection.feature`, ~79 lines). Move the accreted "Resolved direction (…)" design substrate to ADRs (the emission-mode born-accepted ADR the epic already anticipates; note **ADR-011 is reserved** by the epic for the composition-basis amendment) + JSDoc. Drop the hand-listed member **names** from `**Members:**` (keep only the member-type classification — capability-invariant vs deliverable-family — if it carries signal the edges don't). This is editing an idea-tier epic → `architect-sessions` (plan), not refactor-session. +3. **Taxonomy ADR** (amends ADR-001 / extends ADR-007's active narrowing): retire `@architect-quarter` + the USDP-6-phase canonical definitions; clean up the ~8 vestigial `@architect-phase:N` annotations on test features; confirm removal from `CANONICAL_FEATURE_ONLY_TAG_SUFFIXES`, the `ExtractedPattern` schema fields, the `byPhase`/`byQuarter` views, and `getPatternsByPhase`/`getQuarters` — or explicitly re-scope. Author it as a born-accepted record (`@architect-adr-category:process`/`architecture`, theme `taxonomy`) — **do not** duplicate this into a process record. +4. **Retire orphans:** delete `architect/specs/phase-numbering-conventions.feature` and `architect/specs/living-roadmap-cli.feature` (confirmed no consumers: `usedBy`/`implementedBy` empty). +5. **Release model — record the direction, don't build it:** the thin-node + `@architect-release` tag + generated-changelog model (supports vNEXT staging + planning ahead). The open sub-decisions — changelog source (release nodes vs git), `@architect-release` tag registration, and `ReleaseV100`'s representation (thin node vs git tag) — are deferred until the first release is cut. **Keep `ReleaseVNEXT`.** + +## 8. Open / deferred decisions + +- **Release changelog source:** thin release nodes + `@architect-release` tag (ADR-002 proven; supports planning ahead) **vs** pure-git derivation (cleaner event-sourcing, but git tags are retrospective and **lose forward planning**). Leaning toward the node+tag model given the vNEXT practice. Decide at first practiced release. +- **Editorial release narrative** (highlights / breaking changes / migration notes) can't be derived from git or status — it needs a carrier (the thin release node, or an annotated git-tag message). Folded into the above. +- **`ReleaseV100` (shipped) representation:** thin node vs git tag — decide with the model. +- **`grouping: by-epic`** for business-rules/requirements projections — optional payoff; the epic becomes a generated-doc grouping key (today they group by package). +- **(minor)** Whether `documentation patterns` should render an "## Implementations" section (the mature tooling did; the edge is present, only the rendering may be absent) — verify and decide. + +## 9. References & evidence + +- **This repo — decisions:** ADR-001 (canonical values; `@architect-quarter` Rule 6/7, USDP phases Rule 8, `@architect-phase`-cut note), ADR-007 (active coordinated taxonomy redesign), ADR-003 (source-first — parent edge travels with identity), ADR-006 (single read model), ADR-010 (composition helpers; ADR-011 reserved), PDR-005 (process-guard FSM). +- **This repo — code:** `gherkin-extractor.ts:539` (parentToChildren / edge-derived members); `relationship-resolver.ts:104,141–152` (reverse edges incl. `implementedBy`); `fragments/delivery-reporting/release-notes-digest.ts` (changelog contract, unpopulated). +- **This repo — doctrine:** `ephemeral-spec-deletion.md`, `four-tier-ladder.md`, `spec-pattern-relationships.md`, `taxonomy.md`, `decision-records.md`. +- **Old repos:** `libar-platform/architect/.../adr-002-release-management-architecture` (thin release files + tags + generated changelog; "releases are what matter, phases are internal detail"); `CHANGELOG-GENERATED.md` (populated, node+tag-sourced); tag-frequency table (§3). +- **Verified empty/orphan:** `getAllPhases`/`getActivePhases`/`getQuarters` = `[]`; `arch neighborhood ReleaseVNEXT`/`ReleaseV100`/`PhaseNumberingConventions`/`LivingRoadmapCLI` = no edges. diff --git a/plans/documentation-projection-design-handoff.md b/plans/documentation-projection-design-handoff.md new file mode 100644 index 0000000..a42b29a --- /dev/null +++ b/plans/documentation-projection-design-handoff.md @@ -0,0 +1,203 @@ +# DocumentationProjection — Design-Session Context Handoff + +**Date:** 2026-06-04 · **Branch:** `campaign/docs-and-skills-consolidation` +**Audience:** the upcoming design-tier sessions on the `DocumentationProjection` epic. +**This is forward context, not a recap.** The specs under `architect/specs/documentation-projection/` +were refined this session (5 precision corrections landed, gate-green) and are accurate as of this +commit — **do not re-iterate them.** Everything below is the verified ground-truth and the +working-tool insights so design authoring starts from facts, not re-discovery. + +> Anti-anecdote discipline: every `file:line` below was verified against live source this session +> (by the correction-verification agents) or in the prior 9-agent review. It is canonical as of this +> commit; if a future session sees the live CLI/source disagree, the live source wins — re-confirm. + +--- + +## 0. Start-here (API-first path for the next session) + +```bash +pnpm architect:query overview +pnpm -s architect:query bundle DocumentationProjection --mode design --format json +pnpm -s architect:query open-questions --parent DocumentationProjection --include-self +``` + +- The **3 gating decisions** are the `[gating]`-prefixed open questions. **Count by the `[gating]` + prefix (3), not a substring match for "gating" (returns 4)** — one `TaxonomyDocumentationCluster` + member question cross-references the epic's gating question (a pointer, not a 4th decision). +- For the epic's **shape** (its 8 members) use `pattern` / `bundle` / `list --parent` — + **NOT `arch neighborhood`**, which drops the parent/child axis (FEEDBACK 2026-06-04). + +--- + +## 1. Readiness map (from the 9-agent review; still holds) + +- **Design-tier UNBLOCKED now:** `TaxonomyDocumentationCluster` (most-ready), `DesignReviewProjection` + (engine already shipped), the 3 capability invariants (`MultiSourceComposition` / + `OneSourceMultipleAudiences` / `SourceCanonical` — refine in place), `ApiReferenceShapeCoverage` + (pure `@architect-shape` annotation backfill). +- **Design-FINALIZATION blocked:** `GoalOrientedNavigation` (depends on emission-mode + the registry + re-home), `ReadModelReflexivity` (gated on read-model-reach). +- The corpus is internally self-consistent; every load-bearing `file:line` claim verified against source. + +--- + +## 2. The 3 gating decisions — state + what each unlocks + +| Gate | Tractable now? | Unlocks | Key fact | +| -------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Emission-mode / embedding boundary** | **Yes** — a decision, not waiting on code | Taxonomy skill + formal-spec shapes; `GoalOrientedNavigation` | Scope = the **embedded-region drift contract**. A skill managed-region (markdown) and a Studio panel rendering generated content inside an authored layout are the _same_ problem one sink over — decide at the embedding-boundary altitude or it is re-decided per sink. First concrete consequence = the **BundleRouting split** (§3). | +| **Read-model reach** | **Yes** — facts established; consequence is a contract change | `ReadModelReflexivity` + the `Manifest` family (INDEX · `--help` · MCP tool list · Studio command palette) | Fold CLI verb schema + MCP registry into the graph (`@architect-shape` precedent, preserves ADR-006 single read model). **Verified:** `PatternGraphSchema` carries `patterns`/`tagRegistry`/views only; CLI/MCP schema live outside the graph today. The amendment is a perf-gated `strictObject` change + every `parseAndProject*` boundary. | +| **Composition-basis ADR-011** | **No — correctly deferred** | only future facet-shaped families | **No heterogeneous second caller for `buildFacetBundle` exists** (verified adversarially, §3). Record born-accepted only after the Studio Design-Review composed view (or another heterogeneous caller) ships. **Do not promote any member assuming `buildFacetBundle` exists.** | + +None of the three blocks the _whole_ epic. + +--- + +## 3. Verified ground-truth the design work needs (`file:line`, as of this commit) + +### Shipped ADR-010 composition basis (the settled two shapes) + +- `projectSingle` — `packages/architect-projection/src/fragments/base.ts:53` (flat catalog). +- `buildGroupedRoutedBundle` — `…/projections/_shared/grouped-routed-bundle.internal.ts:56` + (grouped routed bundle); its docstring **deliberately carves `architecture` out as never-grouped**. +- `buildFacetBundle` — **does not exist in source** (spec prose only: epic feature + + `taxonomy-documentation-cluster.feature:20`). + +### BundleRouting — the emission-mode split target + +- **A TS `interface` + hand-written `isRoutingLike` guard, NOT a Zod schema:** + `fragments/base.ts:6` (interface), `:64` (guard). The 3 file-sink fields are optional: + `markdownRootTarget` / `markdownChildDirectory` / `entityPathLayout` (`base.ts:13/18/24`). +- **Shipped, in-use contract** (1 reader, 1 producer): exported `…/fragments/index.ts:75`; produced in + `business-rules.internal.ts:163`; read in `markdown-paths.ts:14,41` (+ `types.ts:16`). +- The split — logical routing + `disclosureSpec` stay on the **View**; the 3 file-sink fields move to + the **emission descriptor** — is a **No-BC shipped-contract refactor** (refactoring carve-out), not + additive growth. **Open decision the descriptor must make:** guard-vs-Zod under the repo's Zod-first + boundary doctrine. _(This is now stated in the epic spec via correction A1+A2.)_ + +### DocumentationTypeRegistry — a 4-axis star; only one axis retires + +- Role `contract`, status `active`. Its docstring names **4 orthogonal axes**: + **(1) identity-list** (document-type enumeration) · **(2) output-routing** (file-sink path literals) + · **(3) disclosure matrix** · **(4) cli-surface** (generator enumeration). +- `GoalOrientedNavigation` legitimately retires **only the identity-list axis** (the enumeration becomes + a projection over the families that actually emitted). The other 3 **re-home onto the BundleRouting + split / emission descriptor** — they are _not_ deleted. +- **Would break if the whole contract were deleted:** `generate-docs.ts:23,101-102` (maps the registry + by `generatorName`) → drives `generate-docs` / `docs:all` / the `ci:pre-push` determinism gate + (`package.json:39-40,49`). +- `GeneratorDegeneracyGuard` (`degenerate-guard.ts:3-5,44-51`) is a **separate, completed, + fragment-kind-keyed build guard that survives** — _not_ the same as the empty-doc / static-index-link + special-case that navigation subsumes. _(Disambiguated in the spec via correction A3.)_ + +### Block-vocab reconciliation (R8) — an IMPLEMENTATION prerequisite, NOT a plan/design blocker + +Two genuinely distinct 9-variant unions, **zero cross-import** (additive de-duplication, not wide blast radius): + +- **core `SectionBlock`** — `section-block.ts:144` (`z.union`), `:121` (`code.language` is a bare + `z.string().optional()`, no regex). Untracked plain type (no `@architect`). Consumers: core-internal + only (`presentation-contracts.ts`; `markdown-parser.ts` `parseMarkdownToBlocks` emits `SectionBlock[]`). +- **projection `BlockSchema`** — `blocks/schema.ts:211` (`z.discriminatedUnion`), `:121-127` + (`code.language` has regex `/^[A-Za-z0-9_+\-.]*$/u` **+ `.max(64)`**). Annotated + `@architect-pattern BlockSchema` (role `contract`, bounded-context `rendering`); **enables/usedBy 7 + patterns**, with constructors / `isBlock` / `BLOCK_TYPES`. +- **The collapse onto `BlockSchema` is a validation-TIGHTENING on `markdown-parser.ts` output (a runtime + change), not a cosmetic rename.** _(Now stated in FINDINGS R8 via correction A4.)_ +- Owned by the **composition-layer refactor** (`architect-refactor-session`). It blocks the shared-block- + renderer **implementation**; it does **not** block plan/design authoring. **Sequence it ahead of any + renderer-bound implementation.** + +### Phase/quarter axis (R1) — a source-availability question, not a composition one + +- Live: schema fields `extracted-pattern.ts:113,124`; views `pattern-graph.ts:182-183` + (`byQuarter`/`byPhase`); tag registration `source-ownership.ts:30` (+ `quarter-format.ts`, + `TIMELINE_GROUP_BY`). +- Unpopulated: `@architect-quarter` absent; the `@architect-phase:N` tags sit on `tests/features/*.feature` + files — **3 of 5 carry no `@architect-implements`** (the 2 that do: `pattern-graph-cli-query` → + `PatternGraphAPICLI`, `output-pipeline` → `DataAPIOutputShaping`) — and **none reach the pattern + record's `phase` field**, so `byPhase` is empty (verified `getPatternsByPhase` → `[]`). + _(The "all are realization edges" overstatement was loosened this session via correction A5.)_ +- **R1 decision before any timeline/roadmap family:** populate the axis / re-scope onto a live dimension + (status, level) / retire. Cross-ref: the degenerate-generator guard (C15) catches exactly `roadmap` + + `current-work` + `requirements-specs` as empty today. + +### ADR-011 evidence — every bundle's `children` are a single fragment kind + +- `architecture-diagram.ts:82` → `Record` (homogeneous; lenses vary only `scope`) +- `design-review.ts:160` → `Record` (homogeneous) +- `operational-insights/index.ts:1187-1203` → `Record` (the two-level + `requirements-*` shape; still one fragment kind; **lone caller**) +- `delivery-reporting:442` → `Record` (single type param) +- `grouped-routed:82` `buildGroupChild` → one kind per caller +- → **No bundle mixes kinds → no qualifying heterogeneous `buildFacetBundle` caller.** The likeliest + first is the **unbuilt** Studio Design-Review composed view (pattern + dependency subgraph + + rule-coverage + conflicts). **Nestable children** stays deferred _separately_ (lone caller + `requirements-*`), **not** folded into ADR-011. + +--- + +## 4. Per-member impact (design-relevant, four-way split) + +| Member | Change kind | Touches | +| ----------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| 3 capability invariants | additive (invariants, not deliverables) | no shipped contract | +| `TaxonomyDocumentationCluster` | additive — rides shipped `projectSingle`/`projectTaxonomyDigest` | `taxonomy-digest.ts:73` (reused) + 2 unbuilt emission shapes | +| `DesignReviewProjection` | additive — already shipped | `design-review.ts:160` | +| `ApiReferenceShapeCoverage` | annotation backfill | `@architect-shape` on exported decls; zero renderer/contract touch | +| Emission-mode / BundleRouting split | **No-BC shipped-contract refactor** | `fragments/base.ts:6-25` + 1 reader + 1 producer + registry (contained in `architect-projection`) | +| Block-vocab reconciliation (R8) | **No-BC shipped-contract refactor** | `section-block.ts` (delete) → `blocks/schema.ts` (survivor); 2 isolated trees | +| `ReadModelReflexivity` | **mixed** — net-new emission **+ `PatternGraphSchema` amendment** | perf-gated `strictObject` + every `parseAndProject*` boundary | +| `GoalOrientedNavigation` | No-BC deletion (identity-list axis only) | registry identity axis; other 3 axes re-home | +| ADR-011 / `buildFacetBundle` | blocked-on-gating (prose-only) | **no code** — correctly deferred | + +--- + +## 5. Recommended sequencing (corrections A1–A5 already applied) + +1. **`TaxonomyDocumentationCluster` design spec** — most-ready (rides the shipped + `projectSingle`/`projectTaxonomyDigest` basis; single-slice; explicitly needs no facet helper). + Defer finalizing the **skill + formal-spec embedded shapes** until emission-mode lands. +2. **Make the emission-mode gating decision** — scope to the embedded-region drift contract; specify + the BundleRouting split (incl. the guard-vs-Zod call). +3. **Tighten `GoalOrientedNavigation`** before promoting (the registry re-home is now named in the spec). +4. **Spawn R8 block-vocab reconciliation** as a tracked refactoring carve-out, ahead of any + renderer-bound implementation (does not block plan/design authoring). +5. **Make the read-model-reach decision** before designing `ReadModelReflexivity`; plan the + `PatternGraphSchema` slice amendment + enumerate the `parseAndProject*` re-parse sites. +6. **Leave ADR-011 deferred** — record born-accepted only after a heterogeneous caller ships. +7. **Resolve R1** (populate / re-scope / retire the quarter/phase axis) before any timeline/roadmap family. + +--- + +## 6. Working-tool insights from this session (save time next session) + +- **API boundary is pattern-record-grained.** Gather _state_ via API (`pattern` / `bundle` / `rules` / + `open-questions`); verify _contract shapes_ (BundleRouting, BlockSchema, the registry's 4 axes, + `children:Record` homogeneity) via **source reads**. `search BundleRouting` / `SectionBlock` / + `buildFacetBundle` all return `[]` because they're not `@architect-pattern`-annotated — `BlockSchema` + _is_, so it shows. This is the right boundary, not a defect. +- **`arch neighborhood ` drops the parent/child axis** (FEEDBACK 2026-06-04) — use + `pattern` / `bundle` / `list --parent` for an epic's member shape. +- **`open-questions --parent --include-self`** for the gating set; count gating by the `[gating]` + prefix, not a substring (the substring count is inflated by a cross-reference). +- **`pnpm docs:check`** is the mid-changeset determinism probe (re-renders, diffs the working tree, + writes nothing, non-zero on drift). Confirmed this session: **candidate-tier spec prose/rules do NOT + reach `docs-live/`** — these 5 spec edits produced **zero `docs-live/` drift**. +- **Prose mentions of pattern names in specs are not graph edges** — only `@architect-*` tags are. Safe + to reference `BundleRouting` / `GeneratorDegeneracyGuard` / `DocumentationTypeRegistry` in spec prose + without creating dangling references. +- **Don't re-discover this session's corrections** — they're now in the specs: + BundleRouting-is-interface-not-Zod (A1), split-is-No-BC-refactor (A2), registry-is-4-axis-only-identity- + retires (A3), R8-tightens-validation (A4), phase-tags-not-all-realization-edges (A5). + +--- + +## Pointers + +- **Specs:** `architect/specs/documentation-projection/{00-04}.feature`, + `architect/specs/taxonomy-documentation-cluster.feature` +- **Working reference:** `.pr-coordination/DOCS-IA-FINDINGS.md` (corpus inventory, overlap matrix, R-items) +- **Prior review (full):** `plans/please-review-these-wip-delegated-pony.md` (+ the two agent sub-reports + in the same folder) +- **Re-confirm gating state any time:** `pnpm -s architect:query pattern ADR011` (→ not found); + `pnpm -s architect:query open-questions --parent DocumentationProjection --include-self` diff --git a/plans/please-plan-the-remaining-synthetic-floyd.md b/plans/please-plan-the-remaining-synthetic-floyd.md new file mode 100644 index 0000000..3c205e7 --- /dev/null +++ b/plans/please-plan-the-remaining-synthetic-floyd.md @@ -0,0 +1,211 @@ +# Plan — Playground graph-handle: basic MVP (review · test · address gaps · make discoverable) + +## Context + +`playground/` is the **gen-2-alternative agent read surface** for the PatternGraph: expose the +raw data shapes + a few trusted view functions and let the agent script the rest, instead of a +30-verb API that hides the shapes. The handle (`graph.ts → loadGraph()`) builds both cores live +(~1.5s, no dump), and last session's self-review (REVIEW-NOTES.md, F1–F4) confirmed the headline +numbers reproduce and fixed the two load-bearing honesty bugs. + +But two things make it **not yet an MVP**: + +1. **It has never had a real test.** The only validation so far is the author's own review. No + cold working session has stress-tested the surface against real repo questions, so the actual + gaps (ergonomics, missing cuts, wrong/missing data) are unknown. +2. **It is undiscoverable.** Grep confirms the playground is referenced _nowhere_ outside its own + docs except two `package.json` scripts — no skill, no `AGENTS.md`/`CLAUDE.md` pointer. A fresh + agent session would never know it exists. + +Meanwhile the repo is deliberately moving **off** the gen-1 verb API: the SessionStart hook +(`.claude/hooks/architect-api-first.sh`) has been stripped of the API-first contract and the live +`overview` execution, and `architect-data-api` was de-mandated to on-demand (AGENTS.md). The hook's +own comment reserves the slot: _"When the new graph-handle surface is proven, this hook is where its +pointer/skill would be wired in."_ This plan fills that slot. + +**Goal:** take the handle from _experimental / self-reviewed / undiscoverable_ → _genuinely tested, +regression-guarded, and discoverable via a new basic skill_. That is the first real MVP step toward +the handle becoming an agent's default read surface (a complement to the verbs, not a replacement). + +## Decisions locked (from this session's Q&A) + +- **Scope** = review the code, run a genuine first real test, and address the gaps found. NOT + "ship what's already there" — the user explicitly rejected the near-done framing. +- **Regression guard** = a minimal opt-in smoke script (`pnpm playground:smoke`), **not** wired + into CI gating. +- **Discoverability** = a **new basic skill** for the handle (the old data-api mandate is removed, + the hook is drafted), validated, then wired into the hook + AGENTS.md/CLAUDE.md. + +## Non-goals (stay future-session unless the test proves them needed) + +- The `value-transfer` / `deletionReady` view (CONTEXT §5 #1) — only build it if the first real + test surfaces it as a blocking gap. +- F1-upstream "promotion" pilot on `reporting.feature` (REVIEW-NOTES §5). +- Graduating to a `packages/architect-*` package (ITERATION.md: "later, not now"). +- Wiring the smoke into `ci:verify` (keep playground CI-excluded per doctrine). + +--- + +## Phase 0 — Baseline + code review (read + run; no fixes yet) + +**Establish the baseline runs** (first execution step — confirms the surface works before we judge it): + +```bash +pnpm playground:cli census # reproduces ~348 / core ~65% / projection ~80% +pnpm playground:q 'g.patterns.length' +pnpm playground:cli drift # expect 0 dangling / 0 orphaned +``` + +**Systematic correctness review** of the 8 source files (already read this session). Catalog gaps — +known candidates to confirm or dismiss: + +- `views.ts` vs `graph.ts` `blastRadius` return a **different `atRiskSpecs` shape** (feature-path + strings vs `AtRiskSpec[]`); `cli.ts blast` uses one, `cli.ts specs` the other. Confirm this is + intentional layering, not a latent confusion. +- The REVIEW-NOTES §2 "Minor" items: `maturity` ladder undercounts realized invariants + (`ruleCount>0` only sees directly-carried Rules); `blastRadius`/`specsReverifying` seed only from + `.ts` files, so editing a `.feature` yields no impact. Decide per item: fix vs document-as-known. +- `q.ts` inspect output is capped (`depth: 4, maxArrayLength: 200`) and truncates **silently** — + a real agent-ergonomics gap (USAGE asks to report output-size friction). Candidate fix: print a + "truncated — N more" hint instead of silent cut. + +Output of Phase 0: a written gap list (append to REVIEW-NOTES.md findings, or a scratch note). + +## Phase 1 — First real test (the core of the ask) + +Run a **genuine working session** against the handle, using **real questions an agent hits during +repo work**, scored by the rubric the playground already defines (USAGE.md §"What to report back": +friction · missing cuts · wrong/missing data · latency · handle-vs-verb). + +Exercise the full demand map and recipe set: + +- **Entry adapters (grep→graph bridge):** `g.byFile("packages/architect-projection/src/fragments/base.ts")`, + `g.bySymbol("ProjectionBundle")`, `g.findByConcept("taxonomy")`. +- **Spec bridge:** `g.invariantsOf("ProjectionContext")`, `g.specsReverifying([...changed files])`. +- **Impact:** `g.blastRadius(changed)` over a real `git diff`. +- **Recipes (run each as written, verify output):** I1 (downstream walk), A1 (precedent), + A2 (seam group), DRIFT alarm, COMPOSE (blast→invariants→provenance), ESCAPE HATCH. +- **`q.ts` forms + error paths:** argv expr, stdin pipe, a stray `import` (expect the caught hint), + a thrown error, usage with no args. +- **`cli.ts` commands:** `diff`, `blast HEAD~8`, `fan-in`, `drift`, `census`, `find`, `file`, + `symbol`, `invariants`, `specs`, `maturity`. + +**Cold-agent pass (the truest discoverability test):** dispatch a fresh subagent (`general-purpose`, +no playground context) handed **only** the new skill + USAGE.md, and ask it to answer 2–3 real repo +questions. What it stumbles on is onboarding friction the full-context author cannot feel — this +directly validates the skill in Phase 4. (Run this once a draft skill exists; it bridges Phase 1↔4.) + +Output of Phase 1: a concrete, evidence-backed gap list (every entry = command run + actual output + +- what was wrong/awkward/missing). + +## Phase 2 — Address the gaps + +Fix what Phases 0–1 surface, smallest-diff-first, keeping views pure (IO stays in `cli.ts`/`q.ts`): + +- **Pure-view / handle bugs** → `views.ts` / `graph.ts`. +- **Ergonomics** (output truncation hints, error messages, quoting) → `q.ts` / `cli.ts`. +- **Missing cuts** → add a verified recipe to `recipes.md` first; promote to a handle method **only** + if it clears the freeze-vs-script bar (ITERATION.md: many-consumers AND irreducible-join). Default + to a recipe — do not grow the surface casually. +- **Wrong/missing data** → annotation gap or real pipeline bug; capture in `FEEDBACK.md` if it's a + verb/pipeline surprise. + +Re-run each affected path to verify the fix (the playground is CI-excluded; `tsx` is the gate). + +## Phase 3 — Minimal smoke script + +Add `playground/smoke.ts` and a `playground:smoke` script (bakes `--conditions=source`, matching the +`playground:q`/`playground:cli` convention). + +**Assert invariants that survive annotation growth — NOT frozen counts** (the no-dump/live-state +doctrine says exact numbers drift; asserting `=== 348` would smuggle in the determinism gate the +playground refuses): + +- `g.patterns.length > 0` and a generous sanity floor (e.g. `> 300`). +- `driftFlags` → `dangling.length === 0` (the real invariant; should stay 0 as cleanup completes). +- **F2 coherence** (the bug we fixed): no spec where `provenance==='executable' && maturity!=='executable'`, + and none where `provenance==='authored' && maturity==='executable'` → both `=== 0`. +- Each entry adapter returns non-empty for a known-stable input (`bySymbol("ProjectionBundle")`, + `findByConcept("taxonomy")`, `byFile(
)`). +- `q.ts` argv **and** stdin round-trips produce the expected shape; the three error paths exit + non-zero with the right hint. +- **Print** (informational, not asserted) the live census numbers so a human sees drift at a glance. + +Wire into `package.json` scripts only (opt-in); document in USAGE.md + README.md. Do **not** add to +`ci:verify`. + +## Phase 4 — New basic skill (discoverability) + +Create `.agents/skills/architect-graph-handle/SKILL.md` (canonical path; name adjustable — parallels +`architect-data-api`). Keep it **basic**: one `SKILL.md`, no `references/` yet — it points to the +playground's own USAGE.md / recipes.md / CONTEXT.md for depth. + +- **Frontmatter:** `name`, `description` (when to reach for the handle vs the verbs — the demand + map in one paragraph), `allowed-tools: [Bash, Read, Glob, Grep]` (model on data-api's). +- **Body (small, always-loaded-safe):** the one command (`pnpm playground:q`), the `g.*` surface + list, the handle-vs-verb demand map, the freeze-vs-script principle, `--conditions=source` is baked + into the pnpm scripts, and pointers to USAGE/recipes. **Framing is doctrine-critical:** "complements + the verbs (canonical, product-facing); the handle is the agent-sink for ad-hoc cross-cuts" — never + "replaces the API." +- **Symlink wiring:** add symlinks in `.claude/skills/` and `.codex/skills/` (and `.opencode/skills/` + if it belongs to the Architect domain set), then `pnpm check:skills` must pass. Check + `scripts/check-skill-symlinks.mjs` for the exact mirror requirement before adding. +- **Validate** (the user's "once validated" gate): the Phase 1 cold-agent pass run against this skill + must reach a successful query from a standing start. Iterate the skill text until it does. + +## Phase 5 — Wire discoverability (only after Phase 4 validates) + +- **Hook** (`.claude/hooks/architect-api-first.sh`): fill the reserved slot — add an on-demand + pointer to the handle skill in the `SKILL_BLOCK` (on-demand, like data-api; not auto-loaded). +- **AGENTS.md** (`CLAUDE.md` symlinks to it): add the new skill to the §Skills section, framed as the + on-demand agent-sink complement to the verbs. +- Keep both edits doctrine-correct (complement, not replacement). + +## Phase 6 — Docs consolidation + commit + +- Finish the F3 punch list (REVIEW-NOTES §4): lead all run snippets with `pnpm playground:*`; caveat + stale inline numbers as illustrative-as-of-SHA (don't chase every number). +- Prune REVIEW-NOTES items that graduated to code/skill/smoke. +- **Commit** on `experiment/annotation-fleet` (not main — safe per global git rules). Suggested split, + extending REVIEW-NOTES §7: + - `feat(playground): cohort-honest spec bridge` / `coherent executable maturity` / `playground:q|cli scripts` (the existing F1/F2/F4 work, currently uncommitted) + - `fix(playground): ` + - `test(playground): minimal smoke (playground:smoke)` + - `feat(skills): architect-graph-handle skill + symlink wiring` + - `chore(hooks,docs): wire handle skill into SessionStart hook + AGENTS.md; refresh playground docs` + - Confirm with the user before committing (default per repo convention: batch for review). + +--- + +## Critical files + +| File | Change | +| -------------------------------------------------------- | ------------------------------------------------------------------------ | +| `playground/{views,graph,q,cli}.ts` | Phase 2 gap fixes (pure views in views.ts; IO/ergonomics in q.ts/cli.ts) | +| `playground/smoke.ts` _(new)_ | Phase 3 invariant smoke | +| `package.json` | add `playground:smoke` script | +| `.agents/skills/architect-graph-handle/SKILL.md` _(new)_ | Phase 4 skill (+ symlinks in `.claude/`, `.codex/`, maybe `.opencode/`) | +| `.claude/hooks/architect-api-first.sh` | Phase 5 — fill the reserved skill-pointer slot | +| `AGENTS.md` | Phase 5 — list the new on-demand skill | +| `playground/{README,USAGE,CONTEXT,REVIEW-NOTES}.md` | Phase 6 — F3 punch list + prune graduated items | + +## Reuse (don't reinvent) + +- **Test protocol** already exists: USAGE.md §"What to report back" (the 5-point rubric). +- **Commit split** already drafted: REVIEW-NOTES.md §7. +- **Skill template**: `.agents/skills/architect-data-api/SKILL.md` (frontmatter + structure). +- **Smoke parity targets**: ANNOTATION-FLEET-FINDINGS.md (348 / core 65% / projection 80% / 0 dangling) — as the _printed_ reference, not asserted equalities. +- **`--conditions=source` convention**: the `playground:q`/`playground:cli` scripts already bake it; `playground:smoke` follows suit. + +## Verification (end-to-end) + +1. `pnpm playground:cli census` → numbers reproduce (~348 / 65% / 80%); `pnpm playground:cli drift` → 0/0. +2. Every Phase-1 demand-map command + recipe runs clean via `pnpm playground:q` / `pnpm playground:cli`. +3. `pnpm playground:smoke` → all invariant assertions pass; census numbers printed. +4. **Cold-agent validation**: a fresh subagent given only the new skill + USAGE.md reaches a correct + answer to a real repo question from a standing start. +5. `pnpm check:skills` passes (symlink wiring intact). +6. `pnpm typecheck` still green (playground is excluded, but confirm no stray import leaked into a + compiled package; the hook/AGENTS edits don't touch TS). +7. Gaps found in the first real test are either fixed (re-run proves it) or recorded as known/🔭. diff --git a/plans/please-review-the-uncommited-fluffy-hinton.md b/plans/please-review-the-uncommited-fluffy-hinton.md new file mode 100644 index 0000000..441e81f --- /dev/null +++ b/plans/please-review-the-uncommited-fluffy-hinton.md @@ -0,0 +1,213 @@ +# Plan — Finalize TaxonomyDocumentationCluster + reconcile value-transfer / code-stub-identity doctrine + +## Context + +The `campaign/docs-and-skills-consolidation` branch carries uncommitted work from a prior +session that (a) heavily expanded the `TaxonomyDocumentationCluster` design spec + authored its +`emission-descriptor.ts` stub, and (b) **reversed a doctrine call**: a code/contract stub now +carries its **own** code-originated `@architect-pattern` identity (e.g. `EmissionDescriptor`) +plus `@architect-implements`/`@architect-target`, instead of being node-less. This session +reviews and finalizes that work across the four fronts the user named: + +1. Review + finalize the `TaxonomyDocumentationCluster` spec → implementation-ready. +2. Reconcile any related specs. +3. Make **value transfer** unmistakable at the **top-level** skills — deletion of an ephemeral + spec does **not** destroy information; the value _moves_ to durable carriers. +4. Validate the `@architect-pattern`-on-code-stubs approach and ensure skills + other docs cover it. + +### What this session validated (via the architect API + canonical sources) + +- **The code-stub-identity reversal is canonically CORRECT** (not a regression): + `formal-spec/04-tag-registry.md:31` (`@architect-pattern` MUST on stubs) + `:148` + (`@architect-implements` MUST on stubs); `formal-spec/07-stub-format.md:86-94` (definitive + code-stub Required-Tags table); `adr-003:52-68` ("TS source owns pattern identity" + lifecycle + table + "identity travels with code from stub through production"); `adr-008:127-130` + (step-definition stubs are the **lone** carve-out); `merge-patterns.ts:6-29` (rejects only the + **same name** in both TS+Gherkin — distinct names pass). +- **`scope-validate … implement` → READY**, `dep-tree` clean (`TaxonomyDigestProjection` is + `completed`), `arch dangling` clean, the stub resolves both edge directions. +- **Authoring conventions are surface-dependent** (measured): `.ts` JSDoc → + `@architect-pattern`/`-implements`/`-target`/`-status` **space**; + `-role:`/`-bounded-context:`/`-product-area:` **colon**. `.feature` → `-pattern:`/`-implements:` + **colon**. ⇒ the stub is authored correctly; only the doctrine _text_ is off. +- **An adversarial spec-review found one genuine BLOCKER** the green gate misses (B1 below). + +--- + +## A. Finalize `TaxonomyDocumentationCluster` (Task 1) — one BLOCKER + tightenings + +### A0 — BLOCKER: the contract can't express the formal-spec shape it must emit + +`EmbeddedRegionEmissionSchema` carries a **single** `region` (`emission-descriptor.ts:124-127`, +`EmbeddedRegionTargetSchema` = one `{hostFile, regionId}`), but the spec mandates **multiple +regions in one host**: + +- formal-spec shape = "**one region per digest-emitted group** (Core Identity, Classification, + Relationships, ADR, Hierarchy, …)" (spec line 30); +- skill shape = **two** regions, `taxonomy-role-enum` + `taxonomy-tag-count` (spec line 29). + +The digest demonstrably produces multiple groups in **one childless `projectSingle` bundle** +(`fragments/governance/supporting.ts` `TagGroupEntrySchema`; `projections/governance/taxonomy-digest.ts` +returns `projectSingle`, `children:{}`), and doc-gen associates **one View → one descriptor** +(`documentation-definition.internal.ts:58`). The epic repeats the singular "a region target" +(`00-documentation-projection.feature:35`). So today the formal-spec shape — one of the two +embedded shapes that are the _entire point_ of this proof-point cluster — is unrepresentable. + +**Recommended resolution (ADR-010-clean):** make the embedded-region emission express **N +regions per host**, modeled as a _routing map_ (the embedded analog of whole-artifact's +`childDirectory`/`entityPathLayout` child→path routing), **never** per-region content config: + +- Stub: hoist `hostFile` to the emission level; replace `region` with + `regions: z.array(z.strictObject({ regionId, /* selection key */ })).min(1)`. Each entry routes + one digest selection to one marker region — it names **where** content lands, not **what** it is + (keeps DD-3 / ADR-010 intact: still a write target, not a content tree). +- Spec: state that each region maps to a distinct digest selection — formal-spec: one region per + digest tag-group (routed from the existing `TagGroupEntrySchema` group structure); skill: one + region per embedded fact (role-enum, count). Reconcile spec **line 30**, the Background + deliverable row, and the stub so they agree. +- The one genuinely implement-time choice (slice the digest into a routed multi-child bundle vs. + small dedicated per-selection Views) is named as known work, not pre-decided — but the + _contract_ must express the cardinality now so the spec stops contradicting itself. + +Alternatives considered (record, don't adopt without a reason): (b) keep `region` singular + N +separate descriptors per host — collides with the one-View-one-factory + `projectSingle` model; +(c) collapse formal-spec to one whole-enumeration region — fails the skill shape outright (its two +facts live in different authored sections of `taxonomy.md`). + +### A1 — SHOULD-FIX (spec tightenings surfaced by the review) + +- **Multi-region scenario (masks B1 today):** add a scenario under Rule 2 — two marker-bounded + regions in one host, regenerate, assert each is rewritten from its selection and the + inter-region authored prose is preserved. Without it the suite passes against the broken + single-`region` contract. +- **Region identity scope (S2):** state that region identity is `(hostFile, regionId)` and the + marker scan is host-scoped; add cross-host-collision to Rule 2's malformed/duplicate-marker + error scenario (today it only covers duplicates within one host). +- **Normalization contract (S3):** line 28 promises byte-deterministic blank-line normalization, + but EOL/trailing-newline/whitespace policy is one prose clause with no `@boundary` scenario — + and the embedded hosts are hand-authored (likely mixed EOLs). Specify the EOL + blank-line + + final-newline contract as an invariant with a boundary scenario; add nested/interleaved markers + to the malformed-marker error case. +- **First-run / absent-host (S4):** add a scenario for "host file exists but region markers not yet + present" (and missing host) → the same loud failure as malformed markers, since the multi-target + write path is net-new infra. + +### A2 — NICE-TO-HAVE (low-risk corrections) + +- **Wording (S5):** "the three file-sink fields" — only **two** (`markdownRootTarget`, + `markdownChildDirectory`) are renamed/unified; `entityPathLayout` is already consistent across + `BundleRouting`/registry/stub and is carried forward unchanged. Reword spec line 32 accordingly. +- **Package attribution (N1):** the registry files cited in DD-5 / spec body live in + **architect-projection** (`src/projections/documentation-composition/documentation-type-registry*.ts`), + not architect-core; the `generate-docs.ts` functions live in **architect-cli** + (`src/cli/generate-docs.ts`), not architect-projection. Add package prefixes so the implementer + greps the right package. +- **Same-commit step migration (N2):** the sequencing's "migrate the executable step files" is the + _last_ sub-step, but ≥3 step files spread the file-sink fields onto `BundleRouting`; any typed + `BundleRouting` literal breaks in the **same** commit the interface fields are removed — state + they migrate in that commit, not as a follow-up. +- **arch-layer diff scenario (N3):** the canonical-vs-digest-emitted boundary OQ is resolved; add a + scenario asserting a spec-canonical-but-undigested tag (`arch-layer`) surfaces as a reviewable + diff (the behavior the cluster markets), so the resolved rule is tested. + +### A3 — settled polish regardless of B1 + +- Add `@architect-bounded-context:documentation-composition` to `emission-descriptor.ts` (all + sibling fragments in the target dir carry one; additive, makes the node fully classified). + +> All of A is **design-tier** work (specs + stubs only) — no production code, no FSM transition. + +--- + +## B. Related specs (Task 2) + +- **Epic `00-documentation-projection.feature:35`** — reconcile the singular "a region target" to + the multi-region cardinality from A0 (one-line consistency fix; the rest of the epic's + 2026-06-04 emission-mode direction stays as-is and is consistent). +- Everything else is **consistent, no change**: `03-goal-oriented-navigation` (output-routing + re-homes onto the descriptor — prerequisite-of, confirmed), MultiSourceComposition, + OneSourceMultipleAudiences, ADR-010, `.pr-coordination/DOCS-IA-FINDINGS.md` (R8 prerequisite). +- The `` markers in `taxonomy.md` / `formal-spec/04-tag-registry.md` are + **implement-time** work the spec already documents — **not** added now (a design session never + writes the generation targets). + +--- + +## C. Value-transfer clarity — top-level skills (Task 3) + +Deep references (`ephemeral-spec-deletion.md`, `annotation-ownership.md`) are already correct. The +gap is at the **top level**: sharpen "value moves, nothing is lost," and surface the +code/contract-stub-promotion nuance that currently lives only in deep references. + +- **`architect-base/SKILL.md` §13** — lead with: deletion removes a redundant copy _after_ its + value has moved; it never destroys information. Make the three destinations explicit: + design `.feature` → executable Gherkin (+ JSDoc) then deleted; **step-definition stubs** → the + executable feature's step wiring then deleted; **code/contract stubs** → **promoted to `src/`** + (identity persists per ADR-003, status advances roadmap→completed), staging copy removed, pattern + not discarded. +- **`architect-sessions/SKILL.md` §"The spec is a scaffold"** — re-word "design-level specs **and + stubs** are ephemeral scaffolds" so "scaffold comes down" clearly means _the duplicate is removed + after transfer, not the value_, and call out the code/contract-stub promotion exception. +- **`architect-base/SKILL.md` §8** — already consistent; add a one-line pointer that a + code/contract **stub** carries its own identity (cross-ref `annotation-ownership.md`). + +Surgical edits; no restructuring. + +--- + +## D. `@architect-pattern` code-stub doctrine — precision (Task 4) + +- **Colon/space authoring examples:** `design.md` (stub-authoring bullet) and + `annotation-ownership.md` ("do not duplicate identity" example) prescribe colon-form `.ts` tags + (`@architect-pattern:`/`-implements:`/`-target:`); the measured `.ts` convention is **space**. + Fix the examples to space-form (or add a one-line cross-ref to `taxonomy.md`'s csv-vs-colon rule) + so authors don't copy the wrong form. The stub itself is already correct. +- **Pin `@architect-status:roadmap`** in the `design.md` stub example (per `07-stub-format.md` + "always roadmap for stubs"); the status-advances-on-promotion rule already lives in `implement.md`. +- No new lint/check — `findStubPatterns`' graph-node requirement is the correct contract + (FEEDBACK.md 2026-06-05). + +--- + +## E. formal-spec precision pass (user-approved) + +Distinguish **code/contract-stub promotion** (identity travels to `src/`) from +**behavioral-spec / step-stub deletion** in three sections that currently say "all stubs are +deleted": + +- `07-stub-format.md:186-210` ("Stub Lifecycle" + "Critical rule") — "deleted" for a code stub = + staging copy removed _because the `src/` implementation IS the realized stub_ (text already says + this); make identity-persistence (ADR-003) explicit. +- `08-spec-evolution.md:376` + diagram (`:20`,`:58`) + "What Survives" table (`:531`,`:460`). +- `02-artifact-types.md:81,196-201`. + +Do **NOT** touch `formal-spec/04-tag-registry.md` (cluster's implement-time generation target). + +--- + +## F. FEEDBACK.md + +Append: validated the code-stub-identity reversal against canonical sources (no doctrine change — +already correct); found B1 (single-`region` descriptor can't express the formal-spec multi-region +shape) behind a green `scope-validate` — a substantive-gap class the gate can't see; propagated the +value-transfer "nothing lost" + stub-promotion framing to top-level skills; fixed colon/space +authoring examples; reconciled the formal-spec stub-lifecycle sections. + +--- + +## Verification (architect API first — never hand-edit a projection) + +1. `pnpm -s architect:query scope-validate TaxonomyDocumentationCluster implement` → still READY. +2. `pnpm -s architect:query pattern EmissionDescriptor --format json` → role `contract`, + bounded-context `documentation-composition`, `implementsPatterns [TaxonomyDocumentationCluster]`. +3. `pnpm -s architect:query rules --pattern TaxonomyDocumentationCluster --only-invariants` → + the new multi-region + normalization scenarios present; counts increased. +4. `pnpm -s architect:query arch dangling --baseline packages/architect-guard/src/lint/dangling-baseline.json --strict` → 0. +5. `pnpm check:skills` → wiring intact, 0 dangling symlinks. +6. `pnpm docs:check` (or `docs:all && git diff --exit-code docs-live`) → no projection drift + (edits are working-state + skills + formal-spec, not source). +7. Re-read the edited top-level skill sections: a reader cannot read "deletion = lost work," and + the code-stub-promotion exception is visible without opening a deep reference. + +> Scope guardrail: **design/doctrine session** — writes specs, stubs, skills, formal-spec prose +> only. No production code, no FSM transition, no spec deletion, no `` markers. diff --git a/plans/please-review-the-uncommitted-shimmering-mountain.md b/plans/please-review-the-uncommitted-shimmering-mountain.md new file mode 100644 index 0000000..3e8ec41 --- /dev/null +++ b/plans/please-review-the-uncommitted-shimmering-mountain.md @@ -0,0 +1,345 @@ +# Strategic review — `TaxonomyDocumentationCluster`: does it prove the bet? + +> **Not a ready-to-code plan** (per your steer). The epic's real question is binary: +> _prove by minimum implementation that a universal/flexible doc generator is buildable, or +> drop the design+implementation from the epic._ This review judges the slice **against that +> bet**, not against a style guide. Verified against the live tree + 3 Explore passes + the +> parallel review. + +## The one-paragraph verdict + +The slice cleanly proves the **cheap** seams (the embedded-region marker engine, the +`BundleRouting`→emission-descriptor split, one cross-bucket "function group" read, the +region-aware determinism gate) and **defers every expensive one** the "universal" claim +actually rests on. By the epic's own text, the load-bearing risks are still untested: +heterogeneous composition (`buildFacetBundle` — "no qualifying caller yet"), multi-slice Views, +the descriptor being _consumed_ for whole-artifact (the `emission` field is wired but **nothing +reads it** yet — that's `GoalOrientedNavigation`), and whether "function group" generalizes past +**one** group. So this slice de-risks maybe **~20%** of the bet. It is necessary and well-built, +but it **cannot by itself** justify keep-or-drop. The decision needs exactly one more, _harder_, +experiment — and the cheapest decisive one costs about a day. Nothing here is groundbreaking +because the slice deliberately avoided the parts where the surprises live. + +## Five insights that matter more than the lint + +### I1 — The proof avoided the load-bearing risk by construction + +The taxonomy cluster is **single-slice** (`projectTaxonomyDigest` → `projectSingle`, no routing). +"Universal & flexible" is a claim about _heterogeneous, multi-source_ composition. None of that +is exercised here. The epic concedes it: ADR-011 (facet helper) "waits for a genuine +heterogeneous second caller"; nesting "stays deferred"; the descriptor re-home is +`GoalOrientedNavigation`, not this cluster. **Consequence:** treat this slice as _seam-existence +proof_, not _generality proof_. Reading it as evidence the universal generator works is the +trap — it proves the plumbing compiles, not that it bends. + +### I2 — The "function group" abstraction has a visible ceiling (this is the real answer to concern #2) + +`TAXONOMY_FUNCTION_GROUPS` models a group as a **flat cross-bucket selection of digest tag-rows**, +rendered in the reference's fixed 8-column schema (`buildTaxonomyFunctionGroupTable`). That fits +`Classification` (3 tag rows). It will **not** cleanly cover the RFC groups whose content is _not +tag-row-shaped_: + +- **Relationships** carries a direction / "Blocks?" / authored-vs-derived **semantics** table + (`04-tag-registry.md:184-193`) that is **nowhere in the digest** — it's edge semantics, not tag + metadata. +- **Status→Maturity** carries the `DEFAULT_MATURITY_BY_STATUS` mapping (`:404-412`) — a _different_ + projection, not the tag digest. +- **Core Identity**'s "Required" is tier-conditional doctrine, not a flat flag. + +So the abstraction tops out at ~2–3 more groups, then hits content that needs **new projections** +or stays authored. The flexibility question ("is this hardcoding a problem?") is answered not by +refactoring `planRegions` (cosmetic) but by this ceiling: the path generalizes _within tag-row +content_ and _stops_ at derived/doctrinal content. **That bound is the keep/drop-relevant fact.** + +### I3 — For doctrine docs, the mixed authored/generated host is the END STATE, not a scaffold (concern #1) + +Follows from I2. The RFC (`~35–40%` generatable, `~2%` generated today) will **never** flip to +whole-artifact, because ~half its generatable content isn't digest-shaped and the rest is +irreducible doctrine. Same for the skill (teaches the model + 2 facts by design). So +"majority auto-generated" is the right goal for **enumeration docs** (`docs-live/TAXONOMY.md`), +but for **normative/teaching docs** the honest target is a _first-class mixed host_, not +elimination of the authored part. **Design implication:** stop treating the marker region as a +transitional crutch; commit to making the mixed host a supported, legible shape — which surfaces +I4. + +### I4 — The boundary that's blurry is _semantic_, not _spatial_ (the parallel review's best point, generalized) + +Markers solve _where_ generated content sits. They do nothing for _meaning collisions_ between +authored and generated vocabulary. Live example the parallel review caught: the generated region +calls itself "the **canonical** enumeration" (3 digest-emitted tags) while the authored summary +12 lines down says Classification has **4 canonical** tags incl. `arch-layer` +(`:62` vs `:358`). One word, two sets, one section. As you generate _more_ into authored hosts, +these collisions multiply and **the determinism gate can't see them** — only a human reading the +rendered page can. This is the genuine scaling hazard of "majority generated," and it's a +_naming discipline_ problem (spec-canonical vs digest-emitted as distinctly named sets), not a +generation-coverage problem. No amount of additional wiring fixes it; the model needs the +distinction as a concept. + +### I5 — The descriptor split may currently be vestigial + +`emission-descriptor.ts` is a clean contract, but the **whole-artifact** path (`TAXONOMY.md`) is +still written by the legacy `generator.outputPath`, and the injector only attaches `emission` +when `routing !== undefined` — which `projectTaxonomyDigest` never sets. So today the descriptor +is _consumed_ only in `embedded-region` mode; the `whole-artifact` half is a contract with no +reader until `GoalOrientedNavigation`. That's defensible (the split is a real No-BC refactor), +but it means **the split's payoff is unproven** until the re-home lands. If you're deciding +whether the architecture holds, the descriptor being load-bearing for _both_ modes is part of +what you haven't yet seen work. + +## What this means for your open questions + +- **"Will the next chunk unlock anything? Should we implement some of it and iterate on the + uncommitted changes?"** — Yes, and that's the right instinct. The slice is _evidence-poor_ + precisely because it's done; the _next_ experiment is where the keep/drop signal lives. Do it + **on the uncommitted changes, before committing**, so the proof is cumulative. +- **The capability invariants** (`MultiSourceComposition` · `OneSourceMultipleAudiences` · + `SourceCanonical`) are **not implementation targets** (your prompt; epic §"capability + invariants") — correctly left alone. Don't "make them ready." +- **Concern #1 (count drift)** you marked non-essential — agreed, **downgraded**. It's cosmetic + relative to I4 (the _semantic_ boundary), which is the version of concern #1 worth your time. +- **Concern #3 (prose)** — **defer.** The DD-1..DD-7 / `S2` labels do orphan on spec deletion, + but slimming them is a code-review-batch chore, not a bet-relevant decision. The parallel + review agrees ("would not block this iteration"). One real sub-point: don't widen the barrel + export of `TAXONOMY_CLASSIFICATION_TAGS`/`TAXONOMY_FUNCTION_GROUPS` (`projections/index.ts`) — + keep the proof seam internal until a second group needs it. + +## Candidate next experiments (cheapest-decisive → most-decisive) + +| # | Experiment | What it proves | Cost | Keep/drop signal | +| --- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ---------------- | +| A | **2nd function group** in the RFC (e.g. Relationships or Hierarchy) | Whether "function group" generalizes (I2). Clean data-only drop-in ⇒ abstraction holds; renderer surgery / needs-new-projection ⇒ it's bespoke | ~½–1 day | High, cheap | +| B | **Descriptor re-home** (route `TAXONOMY.md` through `emission.markdownFileRoute`) — a slice of `GoalOrientedNavigation` | Whether the `BundleRouting` split is load-bearing for _both_ modes, not vestigial (I5) | ~1–2 days | Medium | +| C | **2nd cluster: API/verbs** (CLI schema + MCP registry) | Whether the View/emission model survives a _structurally different source_ — the real "universal across sources" claim (epic's stated proof-point #2) | several days | **Decisive** | + +A is the highest signal-per-hour and directly tests concern #2's ceiling; C is the true +keep/drop oracle but expensive. B makes the descriptor real. My recommendation: **run A first** +on the uncommitted changes — if Relationships _fights_ the abstraction (it will partly, per I2), +you've learned the bound for ~free and can decide whether C is worth funding before touching it. + +## The one concrete fix worth doing regardless (correctness, from the parallel review — I concur) + +`renderEmbeddedExecution` (`generate-docs.ts:779`) **silently skips a missing host in every +mode**, but the executable spec only justifies skip under `--all` (portability). An explicit +`-g taxonomy-formal-spec` against a bad/missing path **exits 0 with nothing written** — too easy +to greenlight in CI. **Fail loud for explicit `-g`; skip only under `--all`.** Minor doc nits in +the same file (the `docs:check` remediation message still says "commit docs-live/" though regions +now live in `formal-spec/`+`.agents/`; `--all` help says "all document types + index" but now +also mutates authored hosts) are real but trivial. + +## Verification (how to confirm any of this) + +- I2 ceiling: try adding `Relationships` to `TAXONOMY_FUNCTION_GROUPS` and see what the digest + _can't_ supply (the `:184-193` semantics table). +- I4 collision: read `04-tag-registry.md:62` and `:358` together — two meanings of "canonical". +- I5 vestigial split: `grep -n "emission" packages/architect-projection/src/projections/documentation-composition/documentation-bundle.internal.ts` and confirm `TAXONOMY.md` writes via `generator.outputPath`, not the descriptor. +- `-g` skip bug: `pnpm exec architect-generate -g taxonomy-formal-spec -b ` → exit 0. + +--- + +# Experiment A — wire a 2nd function group (Relationships) [CHOSEN] + +**This is an experiment to extract a keep/drop signal, not a feature.** Success ≠ "it +generates"; success = "we learn whether the function-group abstraction generalizes, and where it +stops." Built on the uncommitted changes, before committing, so the proof is cumulative. + +## Hypothesis (pre-registered, so the result is honest) + +The RFC's Group 4 has **two** tables: + +1. the **tag table** (`uses` · `implements` · `extends` · `see-also`, `04-tag-registry.md:177-182`) + — these ARE digest rows (the "Relationship Tags" bucket). Prediction: drops in as **data + only**, _zero_ renderer/projection code, because `buildTaxonomyRegionBlocks` already routes any + `TAXONOMY_FUNCTION_GROUPS` source through `buildTaxonomyFunctionGroupTable`. +2. the **semantics table** (direction / "Blocks?" / authored-vs-derived, `:184-193`) — NOT in the + digest. Prediction: **cannot** be sourced; stays authored outside the region. + +If both predictions hold, I2's ceiling is confirmed _empirically_: the path generalizes within +tag-row content and stops at derived/semantic content — and the 2nd group is **cheaper** than the +1st (the 1st needed the renderer branch; the 2nd needs none). If prediction 1 _fails_ (needs +renderer surgery), the abstraction is bespoke and that's a strong drop-signal. + +## Minimal change set (measure the edit count — that IS the result) + +- `taxonomy-embedded.ts`: add `TAXONOMY_RELATIONSHIPS_SOURCE = 'relationships'` + + `TAXONOMY_RELATIONSHIPS_TAGS = ['uses','implements','extends','see-also']`; add one entry to + `TAXONOMY_FUNCTION_GROUPS`; extend the `TAXONOMY_FORMAL_SPEC_GENERATOR` branch of `planRegions` + to return a **second** region `{ source:'relationships', regionId:'taxonomy-relationships' }` + alongside `taxonomy-classification`. (Do **not** widen the barrel export — parallel-review + point; keep the seam internal.) +- `formal-spec/04-tag-registry.md`: wrap **only** the tag table (`:177-182`) in + ``; leave the semantics table and the + Informative note authored, outside the markers. Note the selection deliberately **subsets** the + digest (omits `enforces-decision`, which the digest's bucket carries but the RFC's canonical set + doesn't) — a small flexibility point in the abstraction's favor. +- **Expected renderer/projection diff: none.** If that holds, record it; if not, record what was + needed and why (the signal). +- Extend the executable feature `taxonomy-documentation-cluster.feature` with a scenario that a + host with **two** function-group regions rewrites each independently (the "multiple regions per + host" rule already exists; this gives it a real second instance). + +## What we read off it (the actual deliverable) + +A one-paragraph finding appended here: edit-count for the 2nd group, whether renderer code moved, +and the confirmed/observed ceiling — feeding the **keep / fund-Experiment-C / drop** decision. Two +secondary confirmations expected as side effects: the 8-col-vs-5-col schema clash (F2) recurs, and +the mixed-host-is-end-state read (I3) firms up. + +## Out of scope (kept separate on purpose) + +The `-g` fail-loud fix and doc-message nits are **not** bundled — bundling would pollute the +edit-count measurement. Apply them in a separate commit if desired. + +## Verification + +`pnpm test` (architect-projection + the CLI dogfood feature) · `pnpm docs:check` (the new region +must be byte-stable) · re-read the rendered Group 4 to eyeball the authored/generated seam · the +finding paragraph above is written before committing. + +## RESULT (2026-06-06) — both predictions held; the abstraction generalizes with a sharp ceiling + +**Edit count: 3 edits in ONE file** (`taxonomy-embedded.ts`: a `*_SOURCE` const, a `*_TAGS` +const + one `TAXONOMY_FUNCTION_GROUPS` entry, one `planRegions` branch returning a 2nd region) + +marker insertion in the host. **Zero renderer changes, zero projection changes, zero new barrel +exports.** The generic `buildTaxonomyRegionBlocks` dispatch absorbed the new group untouched — so +**the 2nd group was cheaper than the 1st** (the 1st needed the `buildTaxonomyFunctionGroupTable` +renderer branch; the 2nd needed none). Prediction 1 confirmed. + +**The ceiling is real and clean (prediction 2 confirmed).** The relationship _tag table_ generated +byte-consistent with `docs-live/TAXONOMY.md`; the relationship _semantics table_ (direction / +"Blocks?") stayed authored outside the region because the digest cannot supply it. **Bonus +finding:** the function group also _subsets_ a single bucket (dropped the derived +`enforces-decision`), not only gathers across buckets — the audience-read lever is more expressive +than "cross-bucket gather" implied. + +**Signal for keep/drop:** _positive within the tag-row domain_ — function-group generalization is +data-only and the marker engine, descriptor, and gate all held across a 2nd region with no +surprises. The ceiling is not a defect; it's the honest boundary (I2/I3). **But this does NOT +upgrade the heterogeneous/multi-source risk** — that's still untested; **Experiment C (API/verbs) +remains the decisive oracle** before the epic's universal claim is proven. + +**Downstream effect caught (and fixed):** making the formal-spec generator write 2 regions made an +existing CLI test fixture (which prepared only the `taxonomy-classification` region) fail loud on +the unprepared `taxonomy-relationships` markers — the engine's "host not region-prepared" guard +working exactly as designed. Fixture updated to prepare the full region set. + +**Also landed this session (high-confidence, per your steer):** + +- `-g` fail-loud fix: an explicit `-g ` against an absent host now exits non-zero + instead of silently skipping (skip stays `--all`-only for portability) — code + a new executable + scenario + the Rule invariant updated. +- Two doc-message nits (the `docs:check` remediation text and `--all` help) now name the embedded + hosts outside `docs-live/`. +- New executable scenario: the relationships function group subsets one bucket to the canonical + authored set. + +Gates: `pnpm typecheck`, `pnpm test` (1856 projection), `pnpm test:dogfood` (128 CLI), +`pnpm validate:all`, `pnpm lint`, and `pnpm docs:check` (47 files, region-aware) all green. + +--- + +# Two experiments for separate sessions (review → execute independently) + +Experiment A (above) proved the seam exists and generalizes for tag-row content. The two below +are the remaining de-risking probes for the epic's keep/drop call. Each is self-contained — liftable +into its own session prompt. **B is independent and low-risk; C is the decisive oracle but has a +cheap blocker-check to run first.** Order: either, but run C's blocker-check before scoping C. + +## Experiment B — Descriptor re-home: make the `BundleRouting` split load-bearing + +**Keep/drop value (tests I5 — MEDIUM signal).** Today the emission descriptor's `whole-artifact` +half has **no reader**: `docs-live/TAXONOMY.md` is written by the legacy `generator.outputPath`, +and the doc-gen injector attaches `emission` only when `routing !== undefined` — which +`projectTaxonomyDigest` (a `projectSingle`, no-routing View) never sets. So the split that the +whole cluster's contract rests on is **unproven for whole-artifact**. B routes a whole-artifact doc +through `emission.markdownFileRoute.rootTarget`, proving the descriptor is load-bearing for _both_ +modes and that the `.md` + repo-relative containment contract is defined **once** on the descriptor +(DD-5), collapsing the registry's parallel `markdownRootTarget`. + +**Scope.** A _slice_ of the roadmap pattern `GoalOrientedNavigation` (the registry output-routing +re-home), **not** the whole pattern — that pattern's broader open question (reader intents for +multi-page graph-entity families) is explicitly out of scope. B is just the single-doc whole-artifact +descriptor wiring the cluster spec deferred to step 5. + +**Pre-registered hypothesis.** Routing `TAXONOMY.md` through the descriptor is a clean redirect (the +CLI reads `emission.markdownFileRoute.rootTarget` instead of `generator.outputPath`); the +descriptor's `.md`+containment contract subsumes the registry's looser `.md$` rule. **The real risk +is the injector's `routing !== undefined` gate**: a `projectSingle` View must now also carry a +whole-artifact descriptor, and that may ripple to _every_ flat-catalog doc — quantify the blast +radius before committing to No-BC (no parallel write paths). + +**Entry points.** `projections/documentation-composition/documentation-bundle.internal.ts` (the +injector); `architect-cli/src/cli/generate-docs.ts` (`renderProjectionDocument` / +`resolveOutputDirectory` → consume `emission.markdownFileRoute.rootTarget`); +`documentation-type-registry.output-routing.ts` (`markdownRootTarget` → reconcile to `rootTarget`); +`fragments/emission-descriptor.ts` (`WholeArtifactEmissionSchema`, already shipped). + +**Minimal change set.** Attach a whole-artifact `emission` descriptor for `projectSingle` docs +(start with `TAXONOMY.md`); make the CLI write path prefer `emission.markdownFileRoute.rootTarget` +when present; under No-BC, migrate _all_ whole-artifact docs rather than keeping a parallel +`generator.outputPath` path. + +**Measure / signal.** Determinism gate stays green with whole-artifact docs written via the +descriptor; the registry's `markdownRootTarget` collapses into the descriptor's `rootTarget` (one +definition, not two). Clean re-home ⇒ the split is real and strengthens the architecture; a messy +ripple across every `projectSingle` doc ⇒ the split was premature — a useful drop-adjacent signal. + +**Session kickoff.** `pnpm -s architect:query bundle GoalOrientedNavigation --mode design` then read +`documentation-bundle.internal.ts` to see the `routing !== undefined` gate and count `projectSingle` +vs routed docs (the blast radius). + +## Experiment C — 2nd cluster: API/verbs (the decisive "universal across sources" oracle) + +**Keep/drop value (DECISIVE).** Taxonomy is a single tag-registry slice. The API/verbs cluster's +source is **structurally different** — CLI verb schema + MCP tool registry + `@architect-shape`. If +the same View → audience-shapes → emission model absorbs it the way taxonomy did, the "universal +generator" claim is _earned_; if it forces a bespoke pipeline, that's the _drop_ signal. This is the +epic's own stated proof-point #2. + +**The cluster (one source → many shapes), directly analogous to taxonomy — hosts all exist:** + +- _Reference shape (full catalog):_ `docs-live/API-REFERENCE.md` — **already ships** via + `ApiReferenceProjection` / `ApiReferenceDigest` (whole-artifact). The parallel of `TAXONOMY.md`. +- _Live-API context (no descriptor):_ the verb/tool catalog the CLI/MCP already carry. +- _Skill shape (embedded-region — NEW):_ `.agents/skills/architect-data-api/SKILL.md` — embed the + drift-prone catalog facts (verb list, MCP tool names) as regions. +- _Formal-spec shape (embedded-region — NEW):_ `formal-spec/12-live-documentation-api.md` — the + catalog in normative prose. + +**Hard seams new vs taxonomy (this is where the surprises live):** + +1. **A structurally different, possibly multi-source digest.** Does `ApiReferenceDigest` already + expose _one selectable catalog_ the embedded shapes can read (like `projectTaxonomyDigest`), or + does the catalog span multiple slices? Multi-slice ⇒ this is the first real caller of + heterogeneous composition (`buildFacetBundle` / ADR-011), which the epic has been _waiting_ for. +2. **The read-model-reach `[gating]` decision.** If the CLI verb schema + MCP registry are **not** + graph-resident (the epic says they live outside the graph today; only `@architect-shape` is + folded in), the embedded shapes cannot read them as a digest without the read-model-reach + fold-in first — so **C may be BLOCKED on that gating decision.** This is the cheap blocker-check. +3. **A "function group" analog for verbs?** e.g. grouping verbs by purpose (orient / inspect / + navigate) as an audience read over the catalog — the API parallel of the RFC's function grouping. + +**Pre-registered hypothesis.** The embedded-region mechanism, descriptor, and gate carry over +unchanged (already proven sink-agnostic across 2 taxonomy regions). The open risk is the **source**: +single selectable catalog ⇒ C is "taxonomy with a different digest" (~1–2 days, cheap); multi-slice +or non-graph-resident ⇒ C is exactly where the deferred heterogeneous-composition and/or +read-model-reach decisions finally get a caller. **Either outcome is decisive**: clean carry-over = +universal claim earned; forced into facet/reach territory = the true cost of "universal" is now +visible and fundable (or droppable) with evidence. + +**Prerequisite / BLOCKER-CHECK (run before scoping).** Read `api-reference.ts` + +`bundle ApiReferenceDigest` to determine (a) whether the digest is a single selectable catalog and +(b) whether the verb/tool schema is graph-resident. This decides whether C is unblocked or gated on +read-model-reach — do **not** start the build before answering it. + +**Entry points.** `projections/documentation-composition/api-reference.ts`, `api-reference-routes.ts`, +the `ApiReferenceDigest` projection; hosts `.agents/skills/architect-data-api/SKILL.md` and +`formal-spec/12-live-documentation-api.md`; **reuse** the shipped `renderers/managed-region.ts`, +`fragments/emission-descriptor.ts`, and the embedded-generator track in `cli/generate-docs.ts` +(now proven across two taxonomy regions). New code mirrors `taxonomy-embedded.ts` as an +`api-embedded.ts` (routing only) + an api-catalog managed-region renderer branch. + +**Measure / signal.** How much of the embedded mechanism carried over unchanged (target: all of it); +whether C tripped the read-model-reach gate or the facet seam; edit-count vs taxonomy's 2nd group. + +**Session kickoff.** `pnpm -s architect:query bundle ApiReferenceDigest --mode design` + read +`api-reference.ts` — answer the blocker-check first, then decide single-digest build vs gated. diff --git a/plans/please-review-these-wip-delegated-pony-agent-a88333e3008f64922.md b/plans/please-review-these-wip-delegated-pony-agent-a88333e3008f64922.md new file mode 100644 index 0000000..e019b35 --- /dev/null +++ b/plans/please-review-these-wip-delegated-pony-agent-a88333e3008f64922.md @@ -0,0 +1,55 @@ +# Cluster review: block-vocab-reconciliation (IA-findings R8 / ADR-010 consequence) + +Read-only verification. No edits beyond this plan file. + +## Verdict: claim HOLDS + +Two genuinely distinct block vocabularies coexist today; reconciliation to one (No-BC) is a +real prerequisite the spec correctly scopes to the composition-layer refactor (carve-out), +not to a capability member. + +### Vocab A — architect-core config `SectionBlock` + +- `packages/architect-core/src/config/section-block.ts:62` (`SectionBlock`), + `:144` (`SectionBlockSchema` = top-level `z.union`). +- Plain UNTRACKED type — no `@architect` annotations. Not a PatternGraph pattern. +- `code.language: z.string().optional()` — NO regex (`section-block.ts:121`). +- Consumers (core-internal only): `config/presentation-contracts.ts:43,57,64` + (`preamble`/`epilogue` on ReferenceDocConfig + IndexCodecOptionsContract), + `config/index.ts:43`, `index.ts:64` (re-export), `utils/markdown-parser.ts` + (`parseMarkdownToBlocks` emits `SectionBlock[]`). + +### Vocab B — architect-projection `BlockSchema` + +- `packages/architect-projection/src/blocks/schema.ts:211` (`BlockSchema` = `z.discriminatedUnion('type', ...)`), + `:181` (`Block`), `:229` (`BlockType`), `:237` (`BLOCK_TYPES`), `:257` (`isBlock`), + 9 constructor helpers (`:274`-`:388`). +- Annotated `@architect-pattern BlockSchema` (role:contract, bounded-context:rendering, status:active, maturity:design). +- `code.language` regex `/^[A-Za-z0-9_+\-.]*$/u` + `.max(64)` (`schema.ts:123-127`). +- API: enables/usedBy 7 patterns — ArchitectureDiagram, DecisionRecord, + DocumentationCompositionSupporting, MarkdownRenderer, OperationalInsightsSupporting, + PrChangeReview, UiRenderer. + +### Distinct, not re-exported — confirmed + +- Discriminants identical (9 variants, same `type` literals). +- Projection NEVER imports core's `SectionBlock`; core NEVER imports projection's `BlockSchema`. + Two fully separate definitions. Divergences: name, `z.union` vs `z.discriminatedUnion`, + `code.language` regex/max present only on projection, richer projection surface + (constructors/guard/BLOCK_TYPES vs none on core). + +### Reconciliation surface + +- Core side: 1 schema + 1 parser + 2 presentation-contract option types + 2 barrel re-exports. +- Projection side: 1 contract (BlockSchema) feeding 7 patterns via fragments/renderers. +- Cross-package: zero coupling today, so this is an additive de-duplication (collapse core onto + the richer projection contract, or hoist one shared contract), NOT a wide blast radius. +- Genuinely a precondition for the shared block renderer the composition members build on — + but the members (taxonomy/CLI proof points) are not blocked from PLAN/DESIGN authoring; + the block reconciliation blocks the IMPLEMENTATION that lands on the shared renderer. + +### API coverage + +Mixed. API nailed vocab B (pattern + relationships + role/context). Vocab A is invisible to the +API (untracked plain type) — required grep + source reads to find, compare shapes, and prove +the two trees are isolated. diff --git a/plans/please-review-these-wip-delegated-pony-agent-af0c6422bee773e6d.md b/plans/please-review-these-wip-delegated-pony-agent-af0c6422bee773e6d.md new file mode 100644 index 0000000..c383081 --- /dev/null +++ b/plans/please-review-these-wip-delegated-pony-agent-af0c6422bee773e6d.md @@ -0,0 +1,67 @@ +# Cluster review: composition-basis-adr011 (READ-ONLY) + +Verdict: the epic's central composition-basis argument HOLDS in full. No +heterogeneous second caller for `buildFacetBundle` exists; ADR-011 is correctly +deferred / not ratify-ready. + +## Per-claim evidence + +1. architecture children HOMOGENEOUS — HOLDS. + `architecture-diagram.ts:82` = `const children: Record = {}`. + Lenses (package-seam/layered/by-theme) vary only `scope`; all values are the + single `ArchitectureDiagram` kind. internal.ts confirms one fragment shape. + +2. grouped-routed carves architecture out — HOLDS. + `grouped-routed-bundle.internal.ts:16-17`: "`architecture` is a fixed-lens + projection and never grouped." Docstring also documents the requirements-\* + bespoke two-level carve-out and ADR-010's "no generality before a 2nd caller". + +3. design-review per-member diagrams HOMOGENEOUS — HOLDS. + `design-review.ts:160` = `Record`; by-layer/ + by-theme/by-package lenses, all one fragment kind. + +4. validation/ + taxonomy/ sub-docs UNBUILT — HOLDS. + `taxonomy-digest.ts:73` and `validation-rule-digest.ts:42` both return + `projectSingle(...)` — flat, no children, no facet split. + +5. shipped helpers are exactly projectSingle + buildGroupedRoutedBundle; + buildFacetBundle absent from source — HOLDS. + `projectSingle` def in `fragments/base.ts:53`; `buildGroupedRoutedBundle` def + in `_shared/grouped-routed-bundle.internal.ts:56`. grep over packages/ + + architect/ shows `buildFacetBundle` only in spec PROSE (epic feature + + taxonomy-documentation-cluster.feature:20), never in any .ts. No ADR-011 + record file; API: "Pattern not found: ADR011". + +6. nestable-children lone caller = requirements-_ — HOLDS. + `operational-insights/index.ts:1187-1203`: one `Record` carrying BOTH package-index children + (`createRequirementPackageIndexRouteId`) and per-entity detail children + (`createRequirementPackageDetailRouteId`) — the two-level shape. Still + homogeneous in fragment kind; only requirements-_ uses it. + +## Adversarial hunt — NO heterogeneous second caller found + +Enumerated every `children: Record<...>` and every `children[...]=` / +`buildGroupChild` in the projection package: + +- architecture-diagram.ts:82 -> Record +- design-review.ts:160 -> Record +- operational-insights:1187 -> Record +- delivery-reporting:442 -> Record (single type param) +- grouped-routed:82 via buildGroupChild:(group)=>Fragment — one builder, one + kind per caller (api-reference -> ApiReferenceDigest; business-rules -> + scoped business-rule set). + +Every bundle's children are a SINGLE fragment kind. No bundle mixes kinds, so +no qualifying heterogeneous `buildFacetBundle` caller exists. The spec is +correct to defer ADR-011. + +Note: `businessRuleGroupFacets` in business-rules.internal.ts is a grouping +label/sortKey helper, NOT a bundle helper — unrelated to `buildFacetBundle`. + +## apiCoverage: required-file-read + +The API confirmed ADR-010's shipped basis and ADR-011's non-existence, but the +load-bearing claims (child-record homogeneity, line 82, grouped-routed +docstring, requirements two-level shape) are file:line facts the Data API does +not surface. Source reads were required. diff --git a/plans/please-review-these-wip-delegated-pony.md b/plans/please-review-these-wip-delegated-pony.md new file mode 100644 index 0000000..eb05c66 --- /dev/null +++ b/plans/please-review-these-wip-delegated-pony.md @@ -0,0 +1,165 @@ +# Review: `documentation-projection` WIP specs — API helpfulness, impact clarity, design-tier readiness + +## Context + +The `DocumentationProjection` epic (`candidate`) is the next big step in the from-scratch +rearchitecture of the projection pipeline: collapse the documentType-first projection star into +**source-first Views over one engine**. The five WIP specs under +`architect/specs/documentation-projection/` (epic + 3 capability invariants + `GoalOrientedNavigation`) +plus the out-of-folder members (`TaxonomyDocumentationCluster`, `DesignReviewProjection`, +`ReadModelReflexivity`, `ApiReferenceShapeCoverage`) were reviewed to answer three questions the user +posed: + +1. How helpful is the Data API for this review? +2. Is the impact of implementation on existing code already clear? +3. Do we have everything to continue refining specs and authoring design-level specs? + +Method: Architect Data API as first read surface (capability tour passed, zero graph drift), then a +9-agent review workflow — 6 adversarial verifiers checking the specs' `file:line` code claims against +live source, then 3 assessors (impact / readiness / API-helpfulness) over the verified claim-set. +**Every load-bearing claim in the specs holds against source.** The corpus is internally self-consistent; +no dead context survives that No-BC should have deleted. + +--- + +## Answer 1 — API helpfulness: **helpful, with a correct boundary** (`helpful-with-gaps`) + +Strong on everything **graph-grained**, silent only where it is correctly out of scope. + +**Answered deterministically and well** (no file scan needed): + +- Member state/maturity/role/file for all 8 members + epic (`pattern`, `bundle --mode design`). +- Epic↔member hierarchy (`bundle` Member list + `pattern` Hierarchy block — both directions). +- Every invariant verbatim with scenario coverage (`rules --pattern`, bundle Blocks) — incl. the + load-bearing epic rules naming the BundleRouting split, `buildFacetBundle`, the block-vocab prerequisite. +- The full open-question set **including the 3 `[gating]` decisions** (`open-questions --parent … --include-self`). +- `scope-validate design` READY verdicts with exact missing-stub warnings. +- `documentation design-review` — renders all 9 candidate epic nodes with live `(role · status)` annotations + (the spec's "a design review includes not-yet-implemented specs" rule self-demonstrating). +- `documentation architecture` by-theme ADR lens — ADR-010 in the `projections` theme, **no ADR-011 node**. +- ADR existence/status (`pattern ADR010…` → completed/enables-epic; `pattern ADR011` → not found). + +**Did not answer — correctly out of scope** (the four clusters marked `required-file-read`/`mixed`): + +- Every `file:line` code claim (`architecture-diagram.ts:82` homogeneous children, `design-review.ts:160`, + the requirements-\* two-level shape) — the graph indexes pattern records, not AST shapes inside a projection. +- Every **contract-shape** claim — `search BundleRouting` / `SectionBlock` / `buildFacetBundle` all return `[]`. + The query surface is **pattern-record-grained**; Zod-schema / TS-interface / helper-function altitude is invisible + unless `@architect-pattern`-annotated (so `BlockSchema` shows, its untracked twin `SectionBlock` does not). +- Pure design judgment (split BundleRouting? ratify ADR-011? does GoalOrientedNavigation overclaim?) — the API + _fed accurate state into_ these calls, which is its job. + +**Surprises worth a `FEEDBACK.md` entry:** + +- **`arch neighborhood` silently omits the parent/child member axis.** `arch neighborhood DocumentationProjection` + returns only `uses ADR010` — the 8 member edges are dropped, so the epic reads as a near-isolated node. The + edges exist (pattern/bundle expose them); neighborhood is dependency-axis-only without saying so. **(file a note)** +- Contract-shape blindness is **systematic** — route schema-shape claims to source reads from the start. +- `open-questions` has no `--gating-only` filter; the 3 gating decisions are only distinguishable by parsing the + `[gating]` prose prefix, and only appear with `--include-self`. + +## Answer 2 — Impact on existing code: **mostly clear, gaps cleanly named** (`impact-mostly-clear-gaps-named`) + +Impact splits four ways, each pinned to verified `file:line` evidence: + +| Member | Change kind | Clarity | Touches | +| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 3 capability invariants (`MultiSourceComposition` / `OneSource…` / `SourceCanonical`) | additive (invariants, not deliverables) | clear | no shipped contract | +| `TaxonomyDocumentationCluster` | additive — rides shipped `projectSingle`/`projectTaxonomyDigest` | clear | `taxonomy-digest.ts:73` (reused unchanged) + 2 unbuilt emission shapes | +| `DesignReviewProjection` | additive — **already shipped** | clear | `design-review.ts:160` (homogeneous children) | +| `ApiReferenceShapeCoverage` | annotation backfill | clear | `@architect-shape` on exported decls; zero renderer/contract touch | +| **Emission-mode / BundleRouting split** | **No-BC shipped-contract refactor** | clear | `fragments/base.ts:6-25` + 1 reader (`markdown-paths.ts`) + 1 producer (`documentation-bundle.internal.ts`) + guard + registry source — **small, contained in `architect-projection`** | +| **Block-vocab reconciliation (R8)** | **No-BC shipped-contract refactor** | partially clear | `architect-core/config/section-block.ts` (delete) → `architect-projection/blocks/schema.ts` (survivor); 2 isolated trees, zero cross-import | +| `ReadModelReflexivity` | **mixed** — net-new emission **+ contract amendment** | partially clear | requires extending `PatternGraphSchema` (perf-gated strictObject) + `build-pipeline.ts:113` + every `parseAndProject*` boundary | +| `GoalOrientedNavigation` | No-BC deletion | **partially clear (overclaims)** | `DocumentationTypeRegistry` is a 4-axis star; navigation owns **only the identity axis** | +| ADR-011 / `buildFacetBundle` | blocked-on-gating (prose-only) | clear | **no code** — correctly deferred | + +**Biggest unknowns (named, bounded):** + +- `GoalOrientedNavigation` never names where the registry's 3 surviving non-identity axes (output-routing + file-sink literals, disclosure matrix, cli-surface generator enumeration) **re-home**. Deleting the active + `@architect-role:contract` pattern as written would break `generate-docs.ts`, `docs:all`, and the + `ci:pre-push` determinism gate with no destination. +- ADR-011's qualifying heterogeneous second caller is the **unbuilt** Studio Design-Review composed view — + genuinely absent, not merely unwired. +- Block-vocab survivor `BlockSchema` **tightens** `code.language` validation (regex + `max(64)`) on + `markdown-parser.ts` output — a runtime change, not a cosmetic rename; no pattern owns the reconciliation yet. +- Read-model-reach: the `PatternGraphSchema` delta shape (new top-level key vs per-pattern field) is undecided. + +## Answer 3 — Readiness: **yes, with named prerequisites for two members** (`ready-with-named-prereqs`) + +**Spec refinement can continue across all 9 members now.** Design-tier authoring is **unblocked** for: +`TaxonomyDocumentationCluster` (2 of 4 shapes already ship, single-slice, explicitly needs no facet helper), +`DesignReviewProjection` (shipped), the 3 invariants (refine in place), and `ApiReferenceShapeCoverage` (backfill). + +**Two members cannot be design-_finalized_ yet:** + +- `GoalOrientedNavigation` — registry-retirement claim over-stated; depends on the emission-mode BundleRouting split. +- `ReadModelReflexivity` — gated by read-model-reach (needs a `PatternGraphSchema` amendment first). + +**Gating decisions — none blocks the whole epic:** +| Gate | Tractable now? | Blocks | +|---|---|---| +| Emission-mode / embedding boundary | **yes** (decision, not waiting on code) | Taxonomy skill+formal-spec shapes, GoalOrientedNavigation | +| Read-model reach | **yes** (facts established; consequence is a contract change) | ReadModelReflexivity + API/verbs family | +| Composition-basis ADR-011 | **no — correctly deferred** (no heterogeneous caller exists) | only future facet-shaped families | + +--- + +## Forward path — refining specs → design-level specs + +### A. Spec-wording corrections the review surfaced (precision, not direction) + +The specs are directionally sound; these are accuracy fixes before promotion: + +1. **BundleRouting is a TS `interface` + hand-written `isRoutingLike` guard, NOT a Zod schema** — the rule + "A generated document is one emission of a sink-agnostic view" (`00-…feature:60`) calls it a schema. The new + emission descriptor must decide guard-vs-Zod under repo Zod-first doctrine. (Spec names 3 file-sink fields, correct.) +2. **The BundleRouting split is a No-BC shipped-contract refactor, not additive** — frame it via the refactoring + carve-out (`architect-refactor-session`), not additive growth. +3. **`GoalOrientedNavigation` "DELETES the `DocumentationTypeRegistry`" overclaims** — change to "retires the + **identity-list role** of the registry" and name the carrier for the output-routing / disclosure / cli-surface + axes (the epic's BundleRouting split). Disambiguate "empty-doc special-cases" (static-index-link concern, which + navigation subsumes) from `GeneratorDegeneracyGuard` (a completed, fragment-kind-keyed build guard that survives). +4. **Sharpen R8 (block-vocab)** — it undercounts: projection's `code.language` has a regex + `max(64)` core lacks, + so the collapse onto `BlockSchema` tightens validation; not a cosmetic rename. +5. **Loosen the phase-edge wording** in the "never shipped empty" rule — not all phase-tagged `tests/features` + files are `@architect-implements` edges (3 of 5 carry none); the data-emptiness conclusion is unaffected. + +### B. Sequencing (recommended order) + +1. **Author `TaxonomyDocumentationCluster` design spec** — most ready. Design reference + live-API shapes fully now + (they ride the shipped `projectSingle` basis); defer finalizing the skill + formal-spec embedded shapes until + emission-mode lands. +2. **Make the emission-mode gating decision** (tractable now) — scope to the embedded-region drift contract; + specify the BundleRouting split. Apply corrections A1–A2 first. +3. **Tighten `GoalOrientedNavigation`** (A3) before promoting it. +4. **Spawn R8 block-vocab reconciliation** as a tracked refactoring carve-out, sequenced **ahead of any + renderer-bound implementation** (does not block plan/design authoring). +5. **Make the read-model-reach decision** before designing `ReadModelReflexivity`; plan the `PatternGraphSchema` + slice amendment + enumerate the `parseAndProject*` re-parse sites. +6. **Leave ADR-011 deferred** — do not promote any member assuming `buildFacetBundle` exists; record it + born-accepted only after the Studio Design-Review composed view (or another heterogeneous caller) ships. +7. **Resolve R1** (populate / re-scope / retire the quarter/phase axis) before any timeline/roadmap family. + +### Critical files + +- Specs: `architect/specs/documentation-projection/{00-04}.feature`, `architect/specs/taxonomy-documentation-cluster.feature` +- Shipped basis: `packages/architect-projection/src/fragments/base.ts` (BundleRouting + `projectSingle`), + `…/projections/_shared/grouped-routed-bundle.internal.ts` (`buildGroupedRoutedBundle`) +- Refactor surfaces: `…/blocks/schema.ts` ↔ `architect-core/src/config/section-block.ts` (R8); + `…/documentation-composition/documentation-type-registry.*` (registry star); `architect-core/src/validation-schemas/pattern-graph.ts` (read-model reach) +- Working reference: `.pr-coordination/DOCS-IA-FINDINGS.md` (corpus inventory, overlap matrix, R-items) + +## Verification + +- Re-run the determinism gate to confirm no drift was introduced: `pnpm docs:all && git diff --exit-code docs-live`. +- Re-confirm gating state any time: `pnpm -s architect:query pattern ADR011` (→ not found) and + `pnpm -s architect:query open-questions --parent DocumentationProjection --include-self`. +- After any spec edit: `pnpm validate:all && pnpm architect:guard --staged`. +- Verified code claims (spot-check): `architecture-diagram.ts:82` + `design-review.ts:160` (homogeneous children), + `fragments/base.ts:6-25` (BundleRouting conflation), `architect-core/config/section-block.ts` vs + `architect-projection/blocks/schema.ts` (two block vocabularies). + +> **Note:** this plan's primary deliverable is the **review report above**. The forward-path section is the +> answer to "do we have everything to continue" — it is the work, not yet done. Confirm scope before executing. diff --git a/plans/taxonomy-cluster-impact-assessment.md b/plans/taxonomy-cluster-impact-assessment.md new file mode 100644 index 0000000..eb98888 --- /dev/null +++ b/plans/taxonomy-cluster-impact-assessment.md @@ -0,0 +1,145 @@ +# TaxonomyDocumentationCluster — implementation-readiness impact assessment + +**Pattern:** `TaxonomyDocumentationCluster` (status `roadmap`, parent `DocumentationProjection`) +**Spec:** `architect/specs/taxonomy-documentation-cluster.feature` +**Stub:** `architect/stubs/taxonomy-documentation-cluster/emission-descriptor.ts` +**Date:** 2026-06-05 · branch `campaign/docs-and-skills-consolidation` + +> Companion to the design spec. The spec carries the _invariants_; this file carries the +> _map of what to touch_ — a volatile `file:line` consumer list that must NOT live in the +> `.feature` (it would rot). Every claim below was verified against the live tree, not the +> spec's own line numbers. Where a load-bearing spec claim had drifted or was over-broad, it +> is corrected here. + +--- + +## 1. Verdict + +**Contract-ready; two of the four shapes are gated behind net-new infrastructure.** + +- The spec's load-bearing facts are all **confirmed** against the live tree (§2). +- The single new contract — the emission descriptor (`BundleRouting` split) — is **correctly shaped** in the stub and its blast radius is **narrower than the spec implied** (§3): one injector site, three renderer/​type files, three test files. Several surfaces the raw investigation first flagged (`generate-docs`, `render-json`, `business-rules`, `grouped-routed-bundle`) are confirmed **out of scope**. +- The two **shipped** shapes (whole-artifact `docs-live/TAXONOMY.md`, no-descriptor live-API context) work today and ship first. +- The two **new** shapes (embedded-region skill + formal-spec) cannot ship until **two pieces of infrastructure that do not exist anywhere in the codebase** are built (§4): a **multi-target write path** (today the generator resolves a single output dir) and a **region-aware determinism diff** (today the gate is whole-file byte comparison; `grep architect:gen packages/` returns **zero hits**). The spec correctly scopes these as "implementation, not contract shape," but they are large — this is where the real risk sits. + +--- + +## 2. Verified load-bearing facts + +| Spec claim | Verdict | Evidence | +| ----------------------------------------------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TaxonomyDigestProjection` is `completed` and returns `projectSingle` (no routing) | ✅ confirmed | `projections/governance/taxonomy-digest.ts:73` (`return projectSingle(...)`); `projectSingle` → `{root, children:{}, routing: undefined}` (`fragments/base.ts:53-58`) | +| Live registry = 8 roles · 22 metadata · 3 aggregation · 33 total | ✅ confirmed | `architect:query taxonomy --count` | +| `architect:query taxonomy` is the no-descriptor sink (only `{children, root}`, no routing) | ✅ confirmed | `architect:query taxonomy --format json` | +| `isRoutingLike` is a hand-written guard at `fragments/base.ts:64`, delegating to `DisclosureSpecSchema.safeParse` | ✅ confirmed | `base.ts:64`, `base.ts:87-89`; **exactly one caller** (`isBundle`, `base.ts:50`); not exported from the barrel | +| `BundleRouting` is a TS `interface` (not Zod) carrying logical + 3 file-sink fields | ✅ confirmed | `base.ts:6-25` — `markdownRootTarget?` (13), `markdownChildDirectory?` (18), `entityPathLayout?` (24), all optional | +| The `.md` rule `z.string().regex(/\.md$/u)` is on the registry schema | ✅ confirmed | `documentation-type-registry.ts:42` — field is **`markdownRootTarget`** (required, no `.optional()`) | +| The `` `${string}.md` `` template type exists | ✅ confirmed | `documentation-type-registry.output-routing.ts:7` | +| `formal-spec` lists `@architect-arch-layer` as canonical but it is absent from the live registry | ✅ confirmed (with nuance) | `formal-spec/04-tag-registry.md:66,80`; digest has `adr-layer`, **not** `arch-layer`. **Nuance:** an `arch-layer-values.ts` enum _does_ exist in `architect-core` — so the divergence is "enum exists in core, tag not projected into the registry," which the per-tag reconciliation must handle | +| `formal-spec` omits `shape` / `executable-specs`; both are in the digest | ✅ confirmed | digest contains `shape`, `executable-specs`; `formal-spec` tables list neither | +| `formal-spec` count (~26) drifts from the live count (33) | ✅ confirmed | `formal-spec/04-tag-registry.md:315` ("≈ 26 total") vs live `33 total` | +| skill `references/taxonomy.md` already "does the right thing" (links, no full enumeration) | ⚠️ mostly | It **links** for the count (line 54) but **hand-restates the role enum** (line 28) — that 8-value block is itself a drift risk and is the natural skill region | + +--- + +## 3. Blast radius of the `BundleRouting` split (No-BC) + +The split: keep the **logical** fields on `BundleRouting` (`rootRouteId`, `childRouteIds`, `childPathStrategy`, `anchorStrategy`, `disclosureSpec`); move the **three file-sink** fields onto the optional `emission?: EmissionDescriptor`; delete `isRoutingLike`. + +### IN scope — must change + +| File | Symbol | Change | +| -------------------------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `fragments/base.ts` | `BundleRouting` (6-25) | Remove `markdownRootTarget`/`markdownChildDirectory`/`entityPathLayout`; keep logical fields. | +| `fragments/base.ts` | `isRoutingLike` (64-77) + private helpers (`isOptionalString`, `isOptionalEntityPathLayout`, …) | **Delete.** Re-implement `isBundle`'s routing check (50) via a Zod schema for the slimmed logical `BundleRouting`, or validate at the descriptor trust boundary. Parse once. | +| `fragments/` (new) | `emission-descriptor.ts` | Land the stub at its `@architect-target` (`packages/architect-projection/src/fragments/emission-descriptor.ts`). | +| `renderers/markdown-paths.ts` | `resolveLogicalRoutePath` (12-39), `resolveRootMarkdownPath` (41-47) | Reads `routing?.markdownChildDirectory` (22), `routing?.entityPathLayout` (25), `routing?.markdownRootTarget` (42-43). Accept the `MarkdownFileRoute` as a separate parameter, or resolve the path at the call site. **Core renderer-side refactor.** | +| `renderers/types.ts` | `MarkdownRouteProfile.mapPath` (3, 16) | Extend the signature to also receive the `MarkdownFileRoute` descriptor. | +| `renderers/render-markdown.ts` | `mapPath` call sites (269, 446) | Thread the descriptor alongside `routing`. **Logical-routing reads (262-266, 282, 390-453) are unchanged.** | +| `projections/.../documentation-bundle.internal.ts` | `projectDocumentationBundleInternal` (59-94), injection at **86-88** | **THE sole site** that injects the three markdown fields onto `bundle.routing`. Stop injecting; instead build a `WholeArtifactEmission` descriptor from the registry definition and attach as the View's optional `emission?`. Keep `disclosureSpec` injection (85) on logical routing. | +| `cli/commands/_shared/output.ts` | `isBundle` import (7), error strings (100, 107) | No functional break if `isBundle` keeps its signature; update the two `…routing?: BundleRouting` error strings to reflect the slimmed shape + optional `emission`. | +| `tests/.../render-markdown.feature.steps.ts` | `createBundleWithRouting` (700-719; spreads 708-715) | Move the three fields out of the routing fixture into a `MarkdownFileRoute`/`emission` fixture. Evolve the executable feature in place (refactoring carve-out). | +| `tests/.../config-documentation.steps.ts` | assertion at **1085** (`routing?.markdownChildDirectory === 'architecture'`) | Re-point at `emission.markdownFileRoute.childDirectory`. | + +### OUT of scope — explicitly confirmed unaffected (refuting over-broad raw findings) + +| File | Why it does NOT break | +| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `cli/generate-docs.ts` (103, 106, 123) | Reads `metadata.markdownRootTarget` from the **registry** (`SUPPORTED_DOCUMENTATION_TYPE_REGISTRY`), **not** from `BundleRouting`. Impacted only by a later registry re-home (GoalOrientedNavigation). | +| `renderers/render-json.ts` (97-114) | Serializes only logical fields (`anchorStrategy`, `childPathStrategy`, `rootRouteId`, `childRouteIds`) — the JSON/API sink is unaffected. | +| `projections/governance/business-rules.internal.ts` (163-172) | `businessRuleRouting` returns logical-only routing; assigns no markdown fields. | +| `projections/_shared/grouped-routed-bundle.internal.ts` (48, 87) | Only references the `buildRouting` callback type; callers supply logical-only routing. Type tightens automatically. | +| `tests/.../registry-contract.steps.ts` | The registry schema is unchanged by this cluster; stable. (Touched only by GoalOrientedNavigation's later re-home.) | + +### The naming consolidation the descriptor earns + +Today the markdown-routing fields live in **three** surfaces with **inconsistent names**: + +- `BundleRouting` (`base.ts`): `markdownRootTarget?`, **`markdownChildDirectory?`**, `entityPathLayout?` (string, optional). +- registry schema (`documentation-type-registry.ts:42-49`): `markdownRootTarget` (required regex), **`childDirectory?`**, `entityPathLayout?`. +- `DOCUMENTATION_TYPE_OUTPUT_ROUTING` (`output-routing.ts:6-10`): `` markdownRootTarget: `${string}.md` ``, `childDirectory?`, `entityPathLayout?`. + +The descriptor's `MarkdownFileRouteSchema` (`emission-descriptor.ts:96-104`) unifies these to **`rootTarget`** (required, `.md` regex preserved — **not** relaxed to `.min(1)`), `childDirectory?`, `entityPathLayout?` — the "defined once, not forked" win (stub DD-5). Note the asymmetry the descriptor correctly preserves: `rootTarget` is **required** (matching the registry), even though `BundleRouting` declared it optional. + +--- + +## 4. Net-new infrastructure required (the real risk) + +Both pieces are scoped by the spec as implementation, but **neither exists anywhere today**: + +1. **Multi-target write path.** `resolveOutputDirectory` (`generate-docs.ts:484-496`) resolves exactly **one** output dir per generator (default `docs-live`); `writeGeneratedFiles` (507-522) writes every file under it via `path.resolve(outputDir, file.path)`. The embedded shapes write into `.agents/skills/architect-base/references/taxonomy.md` and `formal-spec/04-tag-registry.md` — **outside** any single configured dir, and into a _region_ of an existing file rather than a whole new file. Net-new per-emission host-file targeting (`emission.region.hostFile`). + +2. **Region-aware determinism diff.** `reportDriftAndExit` (524-609) does whole-file byte comparison (**line 541**, `current !== file.content`) + a manifest diff (554-592). There is **no marker scan** (`grep architect:gen packages/` = 0 hits). For embedded regions the gate must (a) locate the begin/end sentinels derived from `regionId`, (b) regenerate and diff **only** the inter-marker span, (c) preserve everything outside the markers byte-for-byte on write. + +**Coverage-hole warning:** the manifest diff (554-592) assumes one `outputDir` per generator. Embedded targets outside `docs-live` must either be folded into the manifest (host-file region hashes) **or** CI's `git diff --exit-code docs-live` will **not cover the embedded regions at all** — a silent coverage hole that would defeat the entire drift-killing purpose of the cluster. The revised spec names this as an explicit invariant (Rule: "Generation into a host file outside `docs-live` is covered by the determinism gate"). + +--- + +## 5. Impact on the 14 generated doc types + +Only **4** of the 14 are touched; only **1** changes its output. + +| Doc type | Impact | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `taxonomy` | **Output set expands** (the cluster's purpose): adds the two embedded-region shapes (skill + formal-spec) to today's whole-artifact `TAXONOMY.md` + no-descriptor live-API context. | +| `requirements-executable` | The **only** type using `entityPathLayout: 'nested-index'` (`output-routing.ts:47`). The split moves `entityPathLayout` onto the descriptor — highest-risk path-resolution case (`markdown-paths.ts:25-26`). **No output change.** | +| `architecture` | `config-documentation.steps.ts:1085` asserts `routing.markdownChildDirectory === 'architecture'`; the test/wiring moves to the descriptor. **No output change.** | +| `design-review` | Carries `childDirectory` routing that re-homes onto the descriptor like every multi-file type. **No output change** — but `DesignReviewProjection` is the next deliverable family and should land on the post-split shape. | +| (other 10) | Unaffected. | + +--- + +## 6. Improvement opportunities the capability unlocks + +| Effort | Opportunity | +| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| large | **Retire the drifting facts** in the two hand-authored surfaces: generate the role enum + count into the skill, the enumeration tables into the formal-spec — turning today's silent rot (`arch-layer` ghost, missing `shape`/`executable-specs`, count 26 vs 33) into a determinism-gate diff. _This is the cluster's entire payoff._ | +| medium | **Fix the formal-spec count + `arch-layer`/`shape`/`executable-specs` divergences as the first reviewable region diff**, settling the canonical-vs-recognized boundary one tag at a time against live data. | +| medium | **Collapse `BundleRouting` to logical-only** and define the `.md` sink contract **once** on the descriptor — deletes the hand-written `isRoutingLike` guard and the duplicated `.md` regex now living on both `BundleRouting` (via `isRoutingLike`) and the registry. Advances the source-first/No-BC collapse. | +| large | **Extend the determinism gate to arbitrary host files**, closing the `docs-live`-only coverage hole and making the gate the enforcement mechanism for _all_ generated facts (reusable for generated tables in README/CONTRIBUTING/RFCs, not just taxonomy). | +| medium | **Make the embedded-region write path generic** so `DesignReviewProjection` and any skill/RFC can carry generated facts without becoming fully-generated artifacts — the substrate `OneSourceMultipleAudiences` needs broadly. | +| large | **Foundation for GoalOrientedNavigation's registry re-home:** the slimmed `BundleRouting` + optional descriptor is the clean base onto which GoalOrientedNavigation moves the registry's output-routing axis, removing the registry/`BundleRouting` duplication. | + +--- + +## 7. Sequencing + +1. **R8 block-vocab reconciliation FIRST** (independent, blocking, _not_ part of this cluster). The epic marks reconciling `architect-core`'s `SectionBlock` (`section-block.ts:121`, bare `z.string().optional()`) and `architect-projection`'s `BlockSchema` (`schema.ts:121-127`, regex + `.max(64)`) to one as an ADR-010-consequence prerequisite "before the composition layer builds further on the shared block renderer" (`00-documentation-projection.feature:65`; `DOCS-IA-FINDINGS.md` §6 R8, High). A shipped-contract refactor under the refactoring carve-out. The embedded shapes render through the shared block renderer, so R8 should land before them. +2. **Descriptor + logical-routing split** (within this cluster, ordered to keep the tree compiling): introduce `emission-descriptor.ts` + a Zod schema for the slimmed logical `BundleRouting` → migrate the sole injector (`documentation-bundle.internal.ts:86-88`) and the renderer call sites (`markdown-paths.ts`, `render-markdown.ts:269/446`, `types.ts:16`) → delete `isRoutingLike` and re-point `isBundle` → remove the three fields from the interface **in the same commit** the descriptor takes over → migrate the three test step files + author `taxonomy-cluster.feature` steps. +3. **Ship whole-artifact + live-API parity** (both already green). +4. **Build the multi-target write path + region-aware gate**, then the **two embedded shapes** + the formal-spec reconciliation diffs. +5. **GoalOrientedNavigation comes AFTER** — it re-homes the registry's output-routing axis onto _this_ descriptor (`03-goal-oriented-navigation.feature:10`), so this cluster's descriptor is a **prerequisite for it, not a dependency of it**. (`TaxonomyDocumentationCluster` has no `depends-on`/`see-also` referencing GoalOrientedNavigation — confirming the direction.) + +--- + +## 8. Spec-readiness gaps → addressed in the revised spec + +| Gap (severity) | Resolution in the revised `.feature` | +| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Marker grammar + region semantics only in the stub comment (**blocking**) | Pinned in the embedded-region Rule (sentinel grammar, per-host `regionId`s, newline normalization) + new error/boundary scenarios for missing/duplicate markers. | +| Gate coverage outside `docs-live` undefined — coverage hole (**blocking**) | New Rule: "Generation into a host file outside `docs-live` is covered by the determinism gate," with the multi-target write path named as a deliverable. | +| Canonical-vs-recognized left open with no rule (**important**) | Open Question sharpened to the three-way distinction (spec-canonical / digest-emitted / scanner-recognized) + the `arch-layer-values.ts` nuance; the starting rule is firmed up as THE rule for the first diff, the per-tag fine-tuning kept as the deliberately-deferred part. | +| No No-BC deletion sequence (**important**) | Short "Sequencing" note in the spec. | +| Executable-spec scenario mapping absent (**important**) | Forward link already set; the 5 scenarios are authored to map 1:1 to the planned step file; the three existing step files are flagged for in-place migration. | +| Sequencing vs R8 / GoalOrientedNavigation not stated (**nice-to-have**) | "Sequencing" note. | +| `rootTarget` vs `markdownRootTarget` naming drift (**nice-to-have**) | Clarified in the Stubs block (the descriptor renames + consolidates; the registry re-home is later, under GoalOrientedNavigation). | diff --git a/plans/we-have-just-completed-joyful-turing.md b/plans/we-have-just-completed-joyful-turing.md new file mode 100644 index 0000000..eb27b1e --- /dev/null +++ b/plans/we-have-just-completed-joyful-turing.md @@ -0,0 +1,172 @@ +# Plan — Make the Architect API a no-brainer grep replacement (effectiveness pass) + +## Context + +The architect package family was recently extracted from a monorepo. Prior sessions got it **operationally green** (typecheck/test/validate/guard/docs-determinism/perf all passing) and proved the read kernel (`PatternGraphAPI`) is **correct but under-exercised and under-promoted**. The repo now needs the _effectiveness_ layer, not more correctness. + +The user set the tone for this whole effort (answering the core-package scoping question): + +> "Correctness of annotations is not measurable mechanically. Annotations are good **if Claude gets what is needed in the graph and API for effective codebase inspection** and architectural views and graph slices — token-efficient views as a **no-brainer replacement for grep** and custom scripts for repo exploration. … Core capabilities of architect are almost there but **not very effective at the moment**. [This] is intended to … trigger rethinking on what is needed and **what can be removed**." + +**Organizing principle for every workstream below:** the success test is not "validators pass" — it is _"can an agent answer the real questions it would otherwise grep for, token-efficiently, through the API?"_ That reframes goal 2 (annotations are good iff they yield useful graph slices), unifies it with goals 1/3 (overview/API are the surfaces that must _deliver_ those slices), licenses **removal** of dead/noisy surface (No-BC), and makes **dogfooding the measure of done**. + +A reframing surfaced during exploration that must be stated up front: **the core-package annotation audit found ZERO mechanical defects** (all roles/statuses/`@architect-uses` valid, no dangling refs, no duplicate identities — verified via `arch dangling`/`diagnostics` returning `[]`). So this is not a defect-cleanup. The real gaps are _effectiveness_ gaps: lossy/under-surfaced API output, an unguarded read kernel, a flat front door, and a stale manual. + +Confirmed decisions this session: + +- **Goal 2:** Option 1 directionally (specs + fixes), but **measured by dogfooding effectiveness, and including removal** — not a mechanical sweep. +- **Hook:** improve the bash stopgap **in place** (plugin migration is a separate cross-repo campaign; `architect-claude-plugin` lives in the proprietary `architect-studio` repo). +- **Extras (all three included):** self-documenting CLI value errors, restore dropped classification fields on `pattern`, and a `pnpm format` sweep. + +Branch fit: we are on `campaign/docs-and-skills-consolidation`, which already owns goals 1/3/4 and the bulk of the unformatted files — this work lands on that campaign. + +--- + +## Execution model — agent-heavy, dogfood-driven + +Run as a multi-agent **Workflow** (ultracode is on; the user asked for heavy agent use). The shape is **discover → fan-out fix → verify**, with strict file-ownership boundaries between concurrent implementers and a dogfooding harness book-ending the run (baseline before, re-measure after). + +Phase 0 produces the **Gap Ledger** that drives Phases 1–2 — we do not guess what to add/remove; we let real exploration tasks reveal it. + +--- + +## Phase 0 — Dogfooding effectiveness baseline (discovery) + +**Why first:** the user's bar is "no-brainer grep replacement." We must _measure_ the gap before closing it, and the measurements become regression evidence. + +Fan out N agents (≈8–12), each handed one realistic **fresh-agent exploration scenario** — the questions an agent actually asks when it lands in this repo — with a hard rule: \_answer ONLY through `pnpm -s architect:query ` (or `architect\__` MCP); record every grep/Read fallback as a failure.\* Representative scenarios: + +- "What is the read model and what reads it?" (kernel discovery) +- "What does `MarkdownRenderer` depend on, transitively, and what's blocking it?" +- "What business rules constrain the projection trust boundary?" +- "Show me the architecture of the projection pipeline and its bounded contexts." +- "What's the taxonomy — valid roles/statuses/tags — and where is it enforced?" +- "Which ADRs govern the read model, and what did they decide?" +- "What's `active` right now and what's the next workable roadmap item?" +- "Classify `pattern-graph-api.ts`: role, context, layer, product-area." ← directly exercises the dropped-classification-fields gap. + +Each agent emits a structured record per scenario: `{question, verbsTried, answeredViaApi: bool, grepFallbackNeeded: bool, payloadTooBig|tooSmall|missingField, frictionNote}`. A synthesis agent **deduplicates** into the **Gap Ledger**, bucketed: + +- **ADD** — missing views/slices/fields/verbs the API should expose. +- **REMOVE** — dead/orphaned/noisy surface that wastes tokens or misleads (No-BC deletes). +- **ANNOTATE** — patterns whose graph slice was unhelpful because annotations under-describe them. +- **GUIDE** — overview/hook/skill guidance gaps (agent didn't know the right verb existed). + +**Critical files to seed agents:** `scripts/api-capability-tour.sh` (existing 9-step demo — extend its spirit), the data-api skill verb table, `docs-live/INDEX.md`. + +**Output artifact:** `.pr-coordination/DOGFOOD-GAP-LEDGER.md` (campaign-scoped, ephemeral). This is the spec for Phases 1–2. + +--- + +## Phase 1 — Overview as the self-promoting front door (goals 1 + 3) + +The overview verb is the agent's first touch. Today it renders PROGRESS → ARCHITECTURE → ACTIVE PHASES → BLOCKING → GENERATED VIEWS → CLI HINTS, and the data behind richer slices is **already precomputed on the graph but unsurfaced**. + +**1a. Wire the dead `summary-with-references` richness tier (the headline of goal 3).** +`--richness summary-with-references` currently renders **byte-identical to `summary`** — confirmed: `render-compact-text.ts:183` (architecture) and `:205-210` (generated views) both take the `summary` path. This is a designed-but-unwired tier whose name _promises references it never adds_. Wire it to surface the progressive-disclosure orientation docs the user named — TAXONOMY, DECISIONS, VALIDATION-RULES, BUSINESS-RULES, API-REFERENCE — **derived, not hand-authored**: + +- These map 1:1 to `documentation ` verbs already in `SUPPORTED_DOCUMENTATION_TYPE_IDENTITIES` (`index.ts:137-142`). Tag an "orientation" subset on the registry identities (or derive by key) so the references list never drifts from the supported set — same discipline as the existing derived `OVERVIEW_GENERATED_VIEWS`. +- Surface the `--disclosure ` mechanic here so agents learn drill-down exists (`disclosure/levels.ts:9`). + +**1b. Surface precomputed distributions, richness-gated.** +Add overview sections fed by views the graph _already_ computes — near-zero cost: + +- **Role / bounded-context distribution** — from `context.graph.byRole` / `listRoles()` (`read-api/pattern-graph-api.ts`). +- **Annotation-coverage line** — `buildAnnotationCoverage` already exists at `projections/operational-insights/index.ts:264`. +- **Orphan-pattern count** — `findOrphanPatterns` in `read-api/graph-inventory.ts` (the rot-detector that is itself currently un-surfaced). +- Gate so `summary` stays lean, `summary-with-references` adds orientation links, `full` itemizes everything. + +**1c. Curate CLI hints from the Gap Ledger.** `OVERVIEW_CLI_HINTS` (hardcoded in `index.ts`) should name exactly the verbs the dogfooding agents _wished they'd known_ — and drop any that didn't earn their line. + +**Insertion points (from exploration):** new sections at `render-compact-text.ts` after line 120 / 129 / 152; fragment fields added to `fragments/operational-insights/{overview-digest,supporting}.ts` (Zod `strictObject`, `z.infer` types). Keep `name-only` untouched. + +**1d. Extra fix — self-documenting CLI value errors.** `parseSchemaValue` (`commands/_shared/schemas.ts:127-133`) `catch`es the Zod error and re-throws a bare `new Error(errorMessage)`, so `--disclosure brief` collapses to the cryptic `Error: --disclosure` instead of "expected one of essential|important|useful|advanced". Surface the accepted enum in the thrown message (the `z.enum` carries it), mirroring the self-documenting `query ` whitelist behavior the skill already praises. This fixes a whole class of flag errors at once (`parseDisclosureLevel` `read.ts:62`, and siblings). + +**1e. Extra fix — restore dropped classification fields on `pattern`.** `pattern --format json` returns `{role, status, maturity, …}` but **omits `boundedContext`, `productArea`, `level`** though the source carries them (verified: keys are `deliverableManifest, deliverables, description, file, kind, maturity, package, patternName, relationships, role, rules, source, status, stubs`). This makes the per-pattern read-kernel output lossy — directly undercutting "classify this file via the API." Extend the `PatternDetail` projection + its fragment schema to surface the three classification axes (role · bounded-context · layer + product-area). **Ripples the determinism gate** — regenerate and commit `docs-live/` in the same change. + +--- + +## Phase 2 — Core package effectiveness (goal 2, reframed) + +Driven by the Gap Ledger, not by a quota. The measure is "does the graph slice for this pattern answer what an agent needs." + +**2a. Fix the two test/prod pattern-name mismatches (real correctness bugs).** + +- `tests/features/types/error-factories.feature` declares `@architect-pattern:ErrorFactories` but production is `ErrorFactoryTypes` (`types/errors.ts:3`). +- `tests/features/types/result-monad.feature` declares `@architect-pattern:ResultMonad` but production is `ResultMonadTypes` (`types/result.ts:3`). + These break the bipartite spec↔pattern link. Align names and add `@architect-implements:` so reverse traceability resolves. + +**2b. Add the 4 missing `@architect-implements` edges.** The 4 feature files without the tag (`extractor/external-relationship-tags.feature`, `extractor/value-format-canonical-values.feature`, plus the two above) — wire each to its production pattern or confirm it's a deliberately test-scoped pattern. + +**2c. Backfill executable specs for the read kernel — ONLY for surfaces dogfooding proves useful.** +`tests/features/read-api/` has just `pattern-graph-api.feature` + `pattern-graph-api-consistency.feature`. `GraphInventory`, `ArchitectureInspection`, `PatternHelpers`, `PatternClassification` have no dedicated executable spec. For each, the Gap Ledger decides: **spec it** (if the dogfooding agents reached for it) or **remove it** (No-BC — if it's orphaned surface nobody needs). The prior session already committed the _canonicalize-onto-kernel_ direction (`b6221f3`), so default is "spec the surfaces that survived canonicalization; delete the ones that didn't." + +**2d. Wire the jsdoc-boilerplate audit to core + clean boilerplate.** `packages/architect-projection/scripts/jsdoc-boilerplate-audit.mjs` runs in CI for `architect-projection` only; **architect-core uses the same boilerplate phrases uncaught** (e.g. `read-api/pattern-graph-api.ts:11-12`, `architecture-inspection.ts:11-12`). Extend the audit to scan `architect-core` and replace the boilerplate with substantive `@architect-*` rationale where the Gap Ledger flagged an unhelpful slice. + +**2e. ANNOTATE / REMOVE per ledger.** Enrich annotations where the slice was unhelpful; delete dead exports/methods the dogfooding surfaced (the prior review found ~23/29 kernel methods had zero production callers — re-confirm post-canonicalization and prune the genuinely-dead ones). + +--- + +## Phase 3 — Hook improvement in place (goal 1) + +Improve `.codex/hooks/architect-api-first.sh` (and its `.claude/hooks/` twin — keep them identical; both registered at `.claude/settings.json:3-14` and `.codex/hooks.json:2-14`): + +- **Close the PostCompact gap.** The hook already detects `SOURCE` (startup/resume/clear/compact) but deliberately **skips** the live overview + contract on `compact` — so long sessions lose the API-first context after compaction (FEEDBACK.md:41). Re-inject at least the contract + skill-load nudge on `compact`. +- **Remove the silent 4000-char truncation** of the live overview snapshot (no marker today) — either render `--richness name-only`/`summary` (now compact by design after Phase 1) so truncation is unnecessary, or add an explicit "(truncated — run `overview` for full)" marker. +- **Align hook content with the improved overview** — the contract's default-verb list and the "Load mandatory skills now" block should match the Phase-1 overview and Phase-4 skills. + +Out of scope (explicitly): porting `architect-claude-plugin` (PreToolUse enforcement, full PostCompact) — that is a separate cross-repo campaign. + +--- + +## Phase 4 — Skill sync (goal 4) + +Bring the three SKILL.md bodies back in lockstep with the live CLI (they are the agent's manual; they drift silently because they're not projected). Update `.agents/skills/architect-{base,data-api,sessions}/SKILL.md` (canonical; the `.claude`/`.codex`/`.opencode` trees symlink in — `check:skills` stays green): + +- **data-api skill:** document `--richness` levels (`overview`) **and** the valid `--disclosure` levels `essential|important|useful|advanced` (`documentation`) — the skill currently shows neither enum, which is _exactly_ what caused the false "flag broken" report. Correct the documentation type count to **13** (the skill says 12 and omits `api-reference`). Document the new overview behavior (orientation references, distributions). Note the `pattern` output now carries classification fields (after Phase 1e). Note the `open-questions --parent X` quirk (excludes X's own questions). Refresh stale example counts (266→~286). Bump the **Provenance** line to today's verified state. +- **base skill:** verify §3/§14 against live (the `design-reviews` "auto-generated" labeling is already correct in the loaded body — confirm). Sync the verb-surface summary with the Phase-1 overview. +- **sessions skill:** confirm reference routing still matches; no verb drift expected. +- **FEEDBACK.md:** close the entries this chunk resolves (the `--disclosure` confusion → now self-documenting; classification-field gap → now surfaced; PostCompact → now re-injected) with resolving-commit notes; leave open ones (plugin migration) annotated. + +--- + +## Phase 5 — Verification + housekeeping + +**Re-run the dogfooding harness (the effectiveness measure).** Re-execute the Phase-0 scenarios against the changed API and diff the Gap Ledger — every ADD/REMOVE/ANNOTATE item should now resolve API-only with no grep fallback. This is the proof the user's bar is met, not just that gates are green. + +**Extra fix — format sweep (3c).** `pnpm format` over the 42 unformatted files (mostly skills/docs this branch owns) as a **dedicated `style:` commit**, separate from the substantive changes, to clear the red `format:check` CI gate. + +**Full gate run (all must be green before any commit):** + +```bash +pnpm typecheck && pnpm typecheck:dogfood +pnpm test && pnpm test:dogfood +pnpm lint && pnpm format:check +pnpm validate:all && pnpm architect:guard --staged +pnpm docs:all && git diff --exit-code docs-live # determinism gate — Phases 1e/2 ripple here +pnpm -s architect:query arch dangling --strict --baseline packages/architect-guard/src/lint/dangling-baseline.json +pnpm test:perf:baseline # overview now does more graph walks — watch the budget +bash scripts/api-capability-tour.sh # smoke the demoed verbs +``` + +**Commit hygiene:** explicit-file staging, `type(scope): summary`, logically grouped (front-door / core-effectiveness / hook / skills / style). Commit only when the user asks. Determinism-gate-coupled changes (source + regenerated `docs-live/`) committed together. + +--- + +## Critical files (reference, by phase) + +| Phase | Files | +| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 0 | `scripts/api-capability-tour.sh`; `docs-live/INDEX.md`; new `.pr-coordination/DOGFOOD-GAP-LEDGER.md` | +| 1 | `packages/architect-cli/src/cli/commands/reporting.ts:26-56`; `packages/architect-projection/src/projections/operational-insights/index.ts:130-262`; `…/renderers/render-compact-text.ts:103-211`; `…/fragments/operational-insights/{overview-digest,supporting}.ts`; `…/disclosure/{levels,spec}.ts`; `…/read-api/{pattern-graph-api,graph-inventory}.ts`; `packages/architect-cli/src/cli/commands/_shared/schemas.ts:127-133` + `read.ts:62`; the `PatternDetail` projection + fragment (1e) | +| 2 | `packages/architect-core/tests/features/types/{error-factories,result-monad}.feature`; `…/tests/features/extractor/{external-relationship-tags,value-format-canonical-values}.feature`; `…/tests/features/read-api/`; `…/src/read-api/{graph-inventory,architecture-inspection,pattern-helpers,pattern-classification}.ts`; `packages/architect-projection/scripts/jsdoc-boilerplate-audit.mjs` | +| 3 | `.codex/hooks/architect-api-first.sh`; `.claude/hooks/architect-api-first.sh`; `.claude/settings.json`; `.codex/hooks.json` | +| 4 | `.agents/skills/architect-{base,data-api,sessions}/SKILL.md`; `FEEDBACK.md` | + +## Doctrine guardrails (do not violate) + +- **No hand-authored projections.** Overview references/distributions derive from the graph + registry, never a hardcoded list that can drift. +- **No-BC.** Removals are deletes, not deprecations/aliases. No `@ts-ignore`, no `eslint-disable`, no `--no-verify`. +- **Zod-first.** New fragment fields are `z.strictObject` with `z.infer` types, parsed once at the boundary. +- **Determinism gate is load-bearing.** Any projection change requires regenerating and committing `docs-live/` in the same change. +- **Dogfood-or-delete the kernel.** Don't spec a surface you're about to delete; the Gap Ledger forces the canonicalize-vs-remove call rather than deferring it. diff --git a/plans/we-have-just-completed-velvety-abelson.md b/plans/we-have-just-completed-velvety-abelson.md new file mode 100644 index 0000000..75b9a91 --- /dev/null +++ b/plans/we-have-just-completed-velvety-abelson.md @@ -0,0 +1,33 @@ +# Plan: Improve API Discoverability + Core Annotation Fixing + Skill Sync + +> Status: DRAFT — exploration in progress. This file is being built incrementally. + +## Context + +Architect was recently extracted from a monorepo. The prior sessions got the repo to an **operational, green, committed** state: the PatternGraph read kernel (`PatternGraphAPI`) is correct, reachable through the `query` passthrough, guarded by a consistency suite, and the CLI returns compact payloads. The branch is `campaign/docs-and-skills-consolidation`. + +The next chunk of work targets **dogfooding effectiveness** — making the Architect tooling and PatternGraph state genuinely usable for continued work in this repo. Four threads: + +1. **API discoverability for agent sessions is still weak.** A cold session does not reliably discover what the API can answer. Levers: the `overview` verb, the SessionStart hook (`.codex/hooks/architect-api-first.sh`), and the mandatory-skill loading protocol. +2. **Core package annotations need a careful review + mass fix** across `packages/architect-core/src/` and `packages/architect-core/tests/`. +3. **The `overview` verb should actively "promote" the value of the API** — surfacing the high-value data dimensions (business rules, decisions, taxonomy, validation rules, API shapes) that the generated-doc indexes (`docs-live/*.md`) already summarize. +4. **The three Architect skills are essential context AND critical artifacts** — they must be improved and continuously kept in sync with the implementation. + +### Grounding already gathered + +- `overview` already emits: PROGRESS, ARCHITECTURE (mermaid), BLOCKING, GENERATED VIEWS (13 doc types), and a "DATA API — Use Instead of Explore Agents" verb list. Current graph: **266 delivery patterns (118 completed, 129 active, 19 planned) = 44%, + 20 candidates**. Packages: cli (4), core (31), guard (20), mcp (5), projection (103). +- The "demo hook" is already a substantial SessionStart injector: API-first contract + mental model + skill-load directive + live `overview` snapshot. Bounded-read stdin handling, graceful fallback. It lives under `.codex/`. +- `docs-live/` index files already compute exactly the promotable summary stats: BUSINESS-RULES (292 rules / 6 packages), DECISIONS (10 ADRs), TAXONOMY (8 roles / 20 metadata / 3 aggregation = 31 tags), VALIDATION-RULES (6 rules / 4 FSM states / 3 protection levels), API-REFERENCE (241 shapes / 3 packages). +- Note discrepancy to investigate: `docs-live/BUSINESS-RULES.md` lists 6 packages (incl. `architect-dev`, `architect-pkg-content`) but live overview shows 5 — possible doc grouping by feature-path vs package, or staleness. + +## Findings (from exploration agents) + +_pending_ + +## Recommended approach + +_pending_ + +## Verification + +_pending_ diff --git a/playground/.gitignore b/playground/.gitignore new file mode 100644 index 0000000..41c379e --- /dev/null +++ b/playground/.gitignore @@ -0,0 +1,3 @@ +# The handle builds the graph LIVE in-process — no dump is read or written. +# Ad-hoc scripts live in scratch/ (see scratch/.gitignore). +.DS_Store diff --git a/playground/ANNOTATION-FLEET-FINDINGS.md b/playground/ANNOTATION-FLEET-FINDINGS.md new file mode 100644 index 0000000..9beb16c --- /dev/null +++ b/playground/ANNOTATION-FLEET-FINDINGS.md @@ -0,0 +1,292 @@ +# Annotation Fleet — Curated-Coverage Experiment Findings + +Working-state notes on the fleet run that raised curated PatternGraph coverage of +architecturally-significant modules + edges in `architect-core` and +`architect-projection`. Goal: an AI agent should reach for `architect:query` / the +playground handle instead of grep, and design against architectural slices +(impact, neighborhoods, dependency subgraphs). + +## What was added (by theme) + +36 `@architect-pattern` lines written across 14 batches, plus ~30 explicit edges +(`@architect-uses` + `@architect-implements`). **Verified against the fresh +`--core` snapshot:** 32 of the 36 materialized as graph nodes (293 → **325**, +`+32`); the remaining **4 are currently dead** — all `.feature` spec annotations +in the failure cluster below (2 blocked by a roadmap-target edge policy, 2 by a +package-glob + space-syntax gap). 28 production `.ts` nodes (space-form +`@architect-pattern`/`@architect-status`, colon-form `@architect-role:`/ +`@architect-bounded-context:`, csv `@architect-uses` — all matching the repo's own +convention) + 4 colon-form `.feature` renderer specs materialized cleanly. + +> _Baseline correction:_ an earlier draft of this doc cited before-figures +> (core 46% / 289 patterns) that did not match the measured baseline. The +> verified baseline is **core 36% (34/94), projection 64% (88/138), 293 patterns, +> 0 dangling** — so the real gains are larger than first framed. + +- **Core taxonomy / domain roots** — `StatusNormalization`, `DomainEnumSchemas` + (20-importer Zod enum hub), `BrandedIdentifiers` (~38 `as*` call-sites), + `StatusValueDomain`. Root primitives: fan-in is the weight, no outbound edges by + design. +- **Core config + contracts** — `ArchitectConfigContract`, `PackageMatcherContract`, + `ConfigDefaults`, `ContextInference`, `LintViolationContract` (cross-package + contract shaped by the whole architect-guard lint subsystem), + `DocDirectiveContract`, `GherkinScanResultContract`. +- **Core read/pipeline seams** — `ReadApiResultContract` (ADR-006 structured-answer + envelope over PatternGraph), `PipelineDatasetContract` (extraction→assembly + boundary), `TrustBoundaryParser` (ADR-009 parse-once primitive). +- **Projection spine** — `ProjectionContext` (the envelope every projection + receives), `ProjectionBundle` (sink-agnostic output contract), `ProjectionError`, + `ProjectionTrustBoundary` (ADR-009 realization for projections), `ProjectionFilter`. +- **Routing / disclosure / rendering** — `LogicalRouteId`, `DisclosureSpec`, + `ProgressiveDisclosureLevel`, `DocumentationTypeIdentity`, `RendererOptions`, + `MarkdownRouteProfile`, `ProjectionFilterResolver` (filter-precedence decider). +- **Shared support services** — `ArchitectureGraphSupport` (Mermaid context-map + shared by two projections), `GroupedRoutedBundleSupport`. +- **Executable specs (reverse realization)** — renderer codec family + (`JsonRenderer`, `MarkdownRenderer`, `UiRenderer`, `FragmentRendererDispatch` + realizations) landed cleanly; a second spec batch + (`RoadmapMarkdownExecutableTests`, `FragmentSchemaMirrorExecutableTests`, + `BusinessRuleSetPackageScopeExecutableTests`, `RequirementExecutableDigestExecutableTests`) + is the failure cluster (see below). + +## Coverage + edge deltas + +| Metric | Before (verified) | After (verified) | +| ---------------------------------- | ----------------- | ------------------------ | +| architect-core node coverage | 36% (34/94) | 51% (48/94) | +| architect-projection node coverage | 64% (88/138) | 74% (102/138) | +| Patterns materialized (fleet) | — | 32 (+4 dead annotations) | +| Edges added | — | ~30 | +| Total graph patterns | 293 | 325 | +| Dangling references | 0 | 0 | + +Edge density after (of 325): `uses` 134 (41%), `usedBy` 120 (37%), +`implementedBy` 86 (26%), fully edge-dark 111 (34%). + +**Fan-in shrinkage.** Baseline fan-in snapshot was empty (`{}`), so no +node-for-node before/after diff is possible. After the run the top fan-in hubs +(`utils/errors.ts` 10 importers, several CLI/\_shared 6-8 importer modules) are +_not yet annotated_ — they are the next obvious high-signal targets. No measured +shrinkage; the fleet added new high-fan-in nodes (`DomainEnumSchemas` ~20, +`BrandedIdentifiers` ~38, `ConfigDefaults` `DEFAULT_TAG_PREFIX` 26) rather than +relieving existing hubs. + +## Quality-sample result + +39 verdicts sampled. Pass = significant && conventionOk && edgeOk. + +- **Pass rate: 36/39 (92%).** +- All 36 passes are genuine seams with correct tag syntax, reused + bounded-contexts, roles from the 8-set, and edges that resolve in the graph. + Spot-confirmed live: `ProjectionContext` (uses PatternGraph, PackageResolver; + enables ProjectionTrustBoundary), `DomainEnumSchemas` (in graph, role contract), + `MarkdownRenderer.implementedBy` now includes `MarkdownRendererExecutableTests`. +- **3 failures — all the same failure mode** (dead/invisible spec annotations): + - `RoadmapMarkdownExecutableTests` — not in graph; `@architect-implements:RoadmapTimelineProjection` did NOT land (target `implementedBy` empty). Likely because the target production pattern is `status:roadmap`. + - `RequirementExecutableDigestExecutableTests` — same: not in graph, implements edge did not land. + - `BusinessRuleSetPackageScopeExecutableTests` — invisible to the API: (1) used SPACE syntax `@architect-pattern BusinessRuleSetPackageScopeExecutableTests` instead of the colon form working `.feature` files use, and (2) the dogfood feature glob scans repo-root `tests/features/` only, NOT `packages/*/tests/features/`, so the node never enters the graph. `FragmentSchemaMirrorExecutableTests` is equally invisible as an independent node (only its realization edge surfaces). + +## Dangling / validate regressions + +- **Dangling: 0** (confirmed live: `danglingReferenceCount 0`, `unknownStatusCount 0`, + `warningCount 0`, `patternCount 325`). newDangling vs baseline 0 = **0**. +- **validateNewErrors: none.** Only pre-existing deprecated-tag `projection` + WARNINGs on unrelated renderer `.feature` files; not from this fleet. +- Snapshot OK. No reformat of unrelated code; all edits additive JSDoc / Gherkin tags. + +## Recommendation: KEEP, with a targeted partial revert of 2 spec annotations + +Keep all 32 materialized nodes + all edges. They are real seams, syntactically +clean, dangling-free, and immediately improve agent navigability — `dep-tree`, +`arch neighborhood`, and reverse `implementedBy` slices now resolve where they +were dark. + +**Partial-revert (or fix-forward) the 2 truly-dead source annotations:** +`RoadmapMarkdownExecutableTests` and `RequirementExecutableDigestExecutableTests`. +Their tag syntax is correct but they contribute zero queryable node and zero +edge — pure noise in the curated layer — because the targets are `status:roadmap` +and the realization edge will not project until the target is active. Either +delete these two `@architect-*` blocks now (cleanest under live-state doctrine — +re-add when the target goes active) or leave them only if the roadmap-target edge +projection is fixed in the same pass. + +The 2 `business-rule-set` / `fragment-schemas` cases are NOT a revert decision — +they are blocked by a **glob/syntax tooling gap**, not bad annotations. Fix the +tooling (below) rather than reverting; the annotations become correct the moment +the glob and the colon-syntax land. + +Reasoning: 92% of a deliberately sparse, high-signal set landing clean is a strong +result. The 3 misses are concentrated, diagnosable, and mostly tooling-shaped — +none undermine the materialized core. Reverting the whole fleet would discard 33 +good nodes to avoid 2 dead ones. + +## Follow-ups + +1. **Fix the dogfood feature glob** to include `packages/*/tests/features/` so + package-local executable specs (`BusinessRuleSet*`, `FragmentSchemaMirror*`) + enter the graph as nodes, not just edges. +2. **Standardize spec-tag syntax**: `@architect-pattern:` / `@architect-implements:` + on `.feature` files use COLON; one batch wrote SPACE. Add a lint/validate check + that flags space-form pattern/implements tags on Gherkin. +3. **Decide the `status:roadmap` realization-edge policy**: should + `@architect-implements` against a roadmap-status target project the reverse edge + (and a candidate node) or be silently dropped? Today it is dropped, which + produced 2 dead annotations. +4. **Annotate the next fan-in tier**: top after-run hubs are still dark — + `utils/errors.ts` (10), the CLI `_shared/schemas.ts` / `output.ts` / `runtime.ts` + cluster, `taxonomy/format-types.ts`. High signal-per-node. +5. **Reduce the 34% edge-dark fraction** by adding `@architect-uses` on the + already-annotated contracts that currently have empty `uses[]` only because the + edge was a deliberate root call — re-audit which of those are genuinely + root vs. just unannotated. + +## Playground / query commands that now return richer results + +1. `pnpm architect:query dep-tree ProjectionTrustBoundary` + → `ProjectionTrustBoundary depends on 1 (4 transitive); 0 depend on ProjectionTrustBoundary (0 transitive)` — full upstream chain `ProjectionContext → PatternGraph → ExtractedPattern` + `PackageResolver`, dark before the fleet. + +2. `pnpm architect:query arch neighborhood ProjectionContext` + → returns `"uses":["PatternGraph","PackageResolver"]`, `"usedBy":["ProjectionTrustBoundary"]` plus the full `sameContext` projection cohort. + +3. `pnpm architect:query pattern MarkdownRenderer` + → `"implementedBy":[{...,"name":"MarkdownRendererExecutableTests"}]` — the reverse realization edge from executable Gherkin now resolves (was empty `implementedBy` before). + +## Open decision: status:roadmap realization-edge policy + +**Question.** Should an `@architect-implements:` authored on an executable +`.feature` against a target whose `@architect-status` is non-active (`roadmap` / +`planned` / `candidate` / `deferred`) project the reverse realization edge — i.e. +surface a candidate node and an `implementedBy` edge — or be dropped, as it is +today? Today the edge is silently dropped, so the executable-spec annotation +becomes a dead node/edge in the read model with no projected consequence. + +**Action taken pending the decision.** Two executable-spec annotation blocks were +**removed** (reverting the additive fleet edit), per the live-state / no-dead-context +doctrine — re-add when the implements target is the kind of node whose reverse edge +projects: + +- `RoadmapMarkdownExecutableTests` → `RoadmapTimelineProjection` + (`packages/architect-projection/tests/features/renderers/roadmap-markdown.feature`) +- `RequirementExecutableDigestExecutableTests` → `RequirementExecutableDigestProjection` + (`packages/architect-projection/tests/features/parity/parity-bundle-shape.feature`) + +**Discrepancy noted for the decider.** The cleanup brief framed both targets as +`status:roadmap`, but the source of truth currently annotates both as +`@architect-status completed` (see `projections/delivery-reporting/index.ts` and +`projections/operational-insights/index.ts`). In the live `--core` graph (what +`buildAuthoredCore` builds) both targets show `implementedBy: null` — **but so do all other fleet +implements targets there** (`MarkdownRenderer`, `JsonRenderer`, +`BusinessRuleSet`, etc.), because `-core` does not carry the projection-package +reverse edges. So the core alone does not prove these two edges are _uniquely_ +dead; the removal is nonetheless safe and reversible. The underlying question — +when a realization edge should project — is **read-model semantics, an ADR-level +decision, and is deferred to a human.** Do not change edge-projection behavior in +the read model as part of this cleanup. + +## Follow-up run: fixes + round 2 + +A combined pass landed the four queued fixes from the first run and a second +annotation batch (round 2). Working-state notes below. + +### Fixes landed (all 4) + +1. **glob** — added repo-root-relative `packages/*/tests/features/**/*.feature` + to `PACKAGE_SELF_HOSTING_SOURCES.features` + (`packages/architect-core/src/config/self-hosting.ts`). Brings package-local + executable specs (81 `.feature` files across all five packages) into the + dogfood scan via the canonical relative shape. Additive; the pre-existing + absolute `${workspaceRoot}/...` entries were left in place. **Build note:** + `architect.config.ts` imports the _built_ `@libar-dev/architect-core`, so a + `pnpm build` is required before the src change takes effect in the scan. +2. **spec-syntax** — converted two package-local `.feature` files from the + fleet's space-form Gherkin tags to colon-form so they parse: + `business-rule-set-package-scope.feature` and `fragment-schemas.feature` + (`@architect-pattern:` / `@architect-status:`). +3. **cleanup-roadmap** — reverted the two roadmap-target executable-spec blocks + (`RoadmapMarkdownExecutableTests`, `RequirementExecutableDigestExecutableTests`) + back to their HEAD blobs and recorded the deferred realization-edge policy + question above. Both names are gone from the entire `.feature` corpus. +4. **add-lint** — added an additive `gherkin-tag-space-form` anti-pattern + detector in `packages/architect-guard/src/validation/` that flags space-form + `@architect-pattern`/`@architect-implements` on `.feature` files (the silent + name-drop failure mode that produced the dead specs). Error-level, with a + minimal unit test; `pnpm typecheck` is green. + +### Round-2 deltas + +21 `@architect-pattern` lines written across 6 batches on production `.ts` +(B1 pipeline read-side, B2 configuration, B3 taxonomy domain, B4 +validation-schemas + error boundary, B5 projection agent-bundle, B6 +composition/governance/rendering), with ~80 `@architect-uses` edges total. +All 21 are confirmed present in source (`grep` over the six batch directories); +**every one of the 21 significance verdicts is `significant: true` / +`conventionOk: true` / `edgeOk: true` — a 21/21 pass.** Two verdicts flag +borderline-but-legitimate leaves to revisit first if curation tightens: +`DeterministicFormatUtils` (renderer-shared formatting leaf, no edges) and +`SlugCanonicalization` (route/anchor identity helper, 22 call sites). + +**Two round-2 recipe bugs found & fixed at integration (the gate's "awaits +rebuild" was wrong).** At gate time the 21 round-2 `.ts` annotations did **not** +materialize (snapshot stuck at 327) — not a rebuild-timing issue, but two recipe +defects the workflow-2 doctrine introduced (workflow 1 had a recipe-probe that +discovered the correct form empirically; workflow 2 did not): + +1. **Missing the bare `@architect` marker tag.** The extractor only recognizes a + JSDoc block as a pattern when it leads with `@architect` (then `@architect-pattern …`). + Round-2 blocks omitted it. Fix: insert ` * @architect` as the first tag in each + block (and put the tags ahead of any description prose — a block with the marker + _after_ a description paragraph still failed, e.g. `DeterministicFormatUtils`). +2. **Space-separated `@architect-uses` breaks the whole pattern parse.** Round-2 + wrote `@architect-uses A B C` (space). In this repo only the **comma** form + (`@architect-uses A, B, C`) parses; the space form silently drops the _entire_ + node, not just the edges. (The doctrine/skill text says "space/comma" — that is + wrong here; logged to `FEEDBACK.md`.) Fix: convert all multi-value uses to commas. + +After both fixes (applied mechanically across the round-2 files) + a single +re-snapshot, **all 21 round-2 nodes materialized. Verified final state: 348 +patterns, core 61/94 (65%), projection 110/138 (80%), 0 dangling, typecheck +exit 0.** (Campaign arc, verified: 293 → 325 after round 1 → 348 after round 2 + +fixes; core 36% → 51% → 65%; projection 64% → 74% → 80%.) + +### Dead-spec resolution + +All four originally-dead `.feature` specs are resolved: **2 promoted to live +nodes, 2 removed.** `BusinessRuleSetPackageScopeExecutableTests` and +`FragmentSchemaMirrorExecutableTests` now resolve as live nodes carrying their +realization edges — `architect:query pattern BusinessRuleSetPackageScopeExecutableTests` +returns `"implementsPatterns":["BusinessRuleSet"]`, and the `Fragment...` +twin returns `"implementsPatterns":["ProjectionFragmentSchema"]`. +`RoadmapMarkdownExecutableTests` and `RequirementExecutableDigestExecutableTests` +are removed and absent from the entire `.feature` corpus, pending the +realization-edge policy decision recorded above. + +### Regression check + +No regression (verified post-fix). `arch dangling` returns 0 over +`patternCount: 348` (vs 0 before) — **no new dangling edges.** `pnpm typecheck` +exits 0 across the workspace (the new guard rule and all round-2 annotations +compile clean). No unknown-status or warning counts. + +### Recommendation: KEEP + +Keep all fixes and the full round-2 batch. The fixes are corrective (a real +scan-coverage gap, a parse-failure class, a new guard rail against the same +slip) and the round-2 batch is 21/21 significant with zero dangling and a clean +typecheck. No partial revert is warranted. The two already-reverted roadmap +specs stay out pending the human decision; do not re-add them as part of this. + +### Remaining follow-ups + +- **DONE — round-2 nodes materialized** after the two recipe-bug fixes above + (`@architect` marker + comma-form `@architect-uses`); verified 348 patterns, + core 65%, projection 80%, 0 dangling, typecheck clean. +- **Resolve the realization-edge policy** (ADR-level, deferred to a human): + whether `@architect-implements` against a non-`active`/non-projecting target + should project a reverse edge / candidate node or be dropped — gates whether + the two removed roadmap specs can return. +- **Reconcile the status discrepancy**: the cleanup brief framed the two roadmap + targets as `status:roadmap`, but source annotates both `completed`. Confirm + the intended status before re-adding either spec. +- **Curation watch**: if the curated layer tightens, reconsider + `DeterministicFormatUtils` then `SlugCanonicalization` first (the two + borderline leaves). diff --git a/playground/CONTEXT.md b/playground/CONTEXT.md new file mode 100644 index 0000000..a774c4f --- /dev/null +++ b/playground/CONTEXT.md @@ -0,0 +1,469 @@ +# playground — essential context + +Conceptual + findings context for this folder: the **why**, the **mental model**, and the +**verified findings** of the experiment that produced the graph handle. + +> **GRADUATED (ADR-014).** The experiment concluded: the handle now lives at +> `@libar-dev/architect-core/graph`, with live IO composition behind the `architect` bin +> (`pnpm architect:q` / `pnpm architect:graph` — the old `pnpm playground:q` / +> `pnpm playground:cli` commands in this doc are retired names), the decision is +> `architect/decisions/adr-014-agent-read-surface.feature`, and the operational guide is +> the `architect-graph-handle` skill. This doc remains as the experiment's findings +> record — read it for rationale, not for run commands. + +> Status note: this is _working-state notes_, not a projected read-model artifact. +> It records durable findings and decisions, deliberately without dates/worklog +> (live-state doctrine). When a finding graduates into code or an ADR, prune it here. + +--- + +## TL;DR + +- **The bet:** for the #1 sink (agents), expose the **raw PatternGraph shapes + a few + trusted view functions** and let the agent script ad-hoc, instead of a 30-verb API + that hides the shapes. Freeze a typed contract only when a _second, machine_ consumer + appears (ADR-010's second-caller bar, applied to projections themselves). +- **The model:** **two surfaces, different purposes.** A **curated** graph (authored, + sparse, = the architecture) and a **mechanical substrate** (derived, exhaustive, = the + language server). They are _not_ truth-vs-approximation; they answer different questions. +- **The state:** the substrate + 5 views are built and verified here in `.scratch`-grade + code, ready to graduate to a package. The thesis held on every probe. + +--- + +## 1. Where this came from + +Architect is mid-rearchitecture of its projection pipeline (see root `CLAUDE.md`). Prior +work established, at the byte level, that **~89% of a full PatternGraph snapshot is +precomputed views** — "the current transformations' denormalized opinion," shaped mostly +for markdown (the sink the doctrine ranks _last_). A `--core` snapshot +(`patterns + relationshipIndex + tagRegistry`, ~3.5 MB vs ~30 MB) is the demanding-sink +substrate a live view would actually re-derive from. + +Four "synthesis forks" (`architect/uni-docgen-tmp/06-synth-{A,B,C,D}.md`) explored folding +new doc-generation onto the existing engine — but all four were **doctrine-fenced** +(told "single read model, no temporal state, Zod-first"). This playground is the +**unfenced** experiment: with no constraints, re-derive from the naked core forward. + +--- + +## 2. The thesis under test + +> Ad-hoc derivation over raw state beats a pipeline of frozen, typed projection fragments +> — for the agent sink. Most projections have exactly one consumer (the one doc); by +> ADR-010's own second-caller bar they should never have been frozen. An agent answers +> them on demand from the core. Where determinism is still required — a **machine +> contract**, not an agent-facing view — it is kept by _committed-script + +> committed-snapshot + re-run-diff_ rather than a frozen typed fragment. (Agent-facing +> views freeze nothing; this playground commits no snapshot — see §5/§7.) + +**Success = the flexibility delta** (a cut the frozen pipeline cannot cheaply do), not parity. + +--- + +## 3. The two-surface model — the load-bearing mental model + +This is the part most easily lost. **Read it before changing anything.** + +| | **Curated** (Layer 2) | **Mechanical substrate** (Layer 1) | +| -------------------- | ----------------------------------------------- | ----------------------------------------------------- | +| answers | "what _is_ the architecture" | "what could break / where used at all" | +| virtue | **editorial sparsity** (human judgment) | **exhaustiveness** (derived) | +| source | annotations → live graph (`live.ts`, in-memory) | `extract.ts` tsc walk of `packages/*/src` (in-memory) | +| authored by | a human, deliberately | derived on demand, never curated | +| feeds | API/MCP/Studio/docs projections | blast-radius, impact, find-all-usages | +| vs a language server | **is the differentiator** | **is the language server** | + +### The correction that defines this model (do not regress on it) + +An earlier framing measured the curated graph's _fidelity to the import graph_ and called +the divergence a defect ("76% wrong, blind to 66%"). **That is the wrong yardstick.** +Fidelity-to-imports is exactly what a language server gives for free, and it is the thing +we deliberately do **not** build. The hand-curated layer exists _because_ the mechanical +graph is not the architecture — **architectural significance is an editorial judgment no +import graph can derive.** So: + +- The divergence between the two graphs is **curation, not drift.** +- **Do not derive the architecture from code.** That just rebuilds the language server and + throws away the curation. (This kills the earlier instinct "densify the sparse graph by + deriving edges from imports.") +- The mechanical layer is for the **one** class of question that legitimately wants the + firehose (impact / re-test scope), plus two **assist** roles that never overwrite the + curated layer: propose candidates, flag unambiguous drift. +- In a larger repo you annotate only the **architecturally significant** patterns; the + curated graph staying a ~6–11% selection of the firehose is _correct_, by design. + +### The demand map — what agents start from (freeze vs script) + +The deep insight behind the playground: **grep is an entry-point problem, not a context +problem.** The answer-graph already exists, but it is keyed by _pattern name_ — and real +work starts from a **string** ("rate limiter"), a **file** (`src/foo.ts`), or a **changeset** +(`git diff`). Agents grep only to bridge from what-they-have to what-the-graph-knows. The +catalog of agent questions sorts onto the two surfaces, and each row gets a verdict by +**ADR-010's second-caller bar**: adapters (many consumers) freeze; traversals (one +consumer each) get scripted. + +| row | question | starts from | surface | verdict | +| ------ | -------------------------------------------- | --------------- | ----------------- | -------------------------------------------------------------------- | +| **E1** | "what patterns relate to this concept?" | fuzzy string | curated | **frozen** — `findByConcept` ✓ | +| **E2** | "what owns this file + its neighborhood?" | a file | both | **frozen** — `byFile` ✓ (dark files get the mechanical neighborhood) | +| **E3** | "where is this symbol used architecturally?" | a symbol | substrate | **frozen** — `bySymbol` ✓ | +| **I4** | "blast radius of this diff?" | `git diff` | substrate | **frozen** — `blastRadius` ✓ | +| I1 | "if I change X, what breaks?" | a pattern | both | **script** (in `blastRadius`) | +| I2 | "which specs re-verify (any maturity)?" | pattern/file | curated + Gherkin | **frozen** — `specsReverifying` ✓ (Gherkin was in-core all along) | +| I3 | "which invariants might I break?" | pattern/file | curated + Gherkin | **frozen** — `invariantsOf` ✓ (per-invariant maturity + provenance) | +| A1 | "how is this kind of thing done here?" | intent | curated | **script** (precedent by role/context + ADR edges) | +| A2 | "what context/seam am I extending?" | package/context | curated | **script** (group by boundedContext) | + +**The entry adapters (E1–E3) + I4 are the frozen trusted core** — the universal bridge +that makes "the agent scripts the rest" cheap. **A1/A2 stay scripts** (one consumer each; +freezing them rebuilds the pipeline we are deleting). + +**I2/I3 are now frozen too — and the correction matters.** An earlier note here claimed +"Rule blocks not in the core" and queued a _Gherkin-side extractor_ (a sibling to +`extract.ts`) as the next Layer-1 build. **That was wrong.** The `.feature` files are +**already parsed** by the pipeline (`buildCliContext`) and ride inside the live `--core` graph: 929 +scenarios (3,572 steps), 431 Rule blocks, the `rule:` + `scenarioNames` linkage. The +prior session missed it only because `schema.ts` left `scenarios`/`rules` **untyped** — so +the richest half of the data was invisible to an agent reading the contract. The work was +never an extractor; it was a **join view + the maturity axis**, both now built in +`graph.ts` (`invariantsOf`, `specsReverifying`). Lesson, kept: _for an AI surface, the type +is the discovery surface — under-typing a shape hides a capability._ + +**Why freezing I2/I3 is NOT a regression toward the verb-wall.** The freeze-vs-script bar +has **two** axes, and the catalog collapsed them: _consumer count_ (ADR-010) **and** _join +irreducibility_. I1/A1/A2 stay scripts because each is a thin single-field traversal +(`usedBy` transitive walk; group-by-`role`) an agent won't botch. `invariantsOf` / +`specsReverifying` freeze because they are **irreducible cross-source joins** (the 2-hop +`pattern→implementedBy→featureFile→rules` bridge + maturity/provenance/linkage decode) — the +universal _spec_-bridge, entry-adapter class, same as E1–E3. The discriminator is not "is it +a traversal" but **"would an agent hand-rolling this get it wrong"** — and the proof these +would is that the prior session, reasoning carefully, got the very premise wrong. +**Counter-proof the line still holds:** `maturityLadder` was built, then _removed from the +handle_ — it is `groupBy(g.patterns, p => p.maturity)`, a 3-line script over the exposed +`maturity` field, no irreducible join → it lives inline in `cli.ts`, not on the surface. + +--- + +## 4. What the experiments proved (the shape of the result, not the exact counts) + +> **The figures below are illustrative as-of-a-SHA, not invariants.** The graph builds live and the +> mechanical numbers drift with every annotation; **the graph wins, always re-derive via the CLI** +> (`pnpm playground:cli census` / `diff` / `blast`). What is durable is the **shape** of each result — +> a ~quarter-overlap curated/mechanical Jaccard, a mostly-correct "dark" bucket, a blast radius that +> roughly doubles re-test reach. Read the numbers as orders of magnitude, never as targets (the +> north star is agent usability, not coverage %). + +**Substrate (`extract.ts`)** — complete, barrel-followed, deterministic: +`334 files · 1502 exported symbols · 1878 import edges (451 cross-pkg) · 0 unresolved`. + +**Graph diff (`cli.ts diff`)** — mechanical (barrel-followed) vs authored `uses`: + +``` +mechanical pattern→pattern edges : 334 +authored pattern→pattern edges : 258 + shared (curated selection) : 115 + dark (import, no intent) : 219 ← mostly CORRECT editorial silence + aspirational (intent, no import): 143 + Jaccard similarity : 24% ← curation is a ~24% overlap, BY DESIGN +``` + +**The 143 aspirational, dissected** (this is where the correction was proven): + +- 28 decision-originated (21 ADR→ADR + 7 ADR→code) — genuinely conceptual; _no import can + ever exist_. This is exactly what Layer 2 should carry. +- 115 code→code — of which only ~15 reference the target in actual code; ~100 appear only + in the `@architect-uses` annotation text (file-attribution imprecision or conceptual + wiring). **Not drift** — confirmed next. + +**Scoped drift (`cli.ts drift`) → `0 dangling, 0 orphaned`.** Built to fire _only_ on +unambiguous "code is gone" signals (a `uses` target that is no longer a pattern; a pattern +whose source file was deleted). Zero. So the aspirational bucket is **curation + conceptual +lineage, not rot.** The substrate, asked the narrow honest question, agrees with the +curation. (This number should trend monotonically to zero as the 95% deletion completes — +a useful invariant to watch.) + +**Blast radius (`cli.ts blast HEAD~8`)** — the flexibility delta, concretely: + +``` +changed src files : 90 (52 map to a pattern) +authored-graph downstream : 60 patterns +mechanical-graph downstream : 120 patterns (+28 the curated graph MISSED) +at-risk executable specs : 49 +``` + +The mechanical layer ~doubles re-test coverage and reaches the ~47% of src the curated +graph deliberately omits — the code↔spec↔pattern cut no grep and no single verb produces. + +**Fan-in candidates (`cli.ts fan-in`)** — curation assist working: load-bearing modules +with NO pattern node, ranked by importers. **Run the command for the live list — do not +read a frozen shortlist here.** The original probe's top four (`fragments/base.ts` ≈47 — the +`ProjectionBundle` base every synth fork hand-cited — `context/projection-context.ts` ≈61, +`taxonomy/status-values.ts`, `domain-enums.ts`) have **all since graduated to pattern nodes** +(`ProjectionBundle`, `ProjectionContext`, `StatusValueDomain`, `DomainEnumSchemas`) — the assist +loop closed on its own shortlist, which is the proof the signal is real. That same fact is why a +frozen list here rots within one annotation campaign: as of this writing the live top has moved to +the `architect-cli` `_shared` cluster. The durable claim is the **shape** (there is always a fan-in +tail; the top of it is the next curation target), never the filenames — re-derive with `fan-in`. + +**Census (`cli.ts census`)** — node coverage (non-barrel src → pattern node), as-of-a-SHA: +`cli 15% · core 65% · guard 52% · mcp 57% · projection 80%`. Edge density: +`uses ~43% · usedBy ~43% · implementedBy ~25%`. (`extendedBy` / `enforces*` ≈ 0–2% — dead +taxonomy machinery, deletable per bootstrap doctrine.) Re-derive with `pnpm playground:cli census`. + +**Context efficiency** — the whole multi-experiment session ran in ~127k tokens, ≈⅕ of the +grep/verb-API equivalent. Mechanism: the data stays _in-process_; only conclusions return. +This is the consumer-side mirror of the 89%-baggage finding — freezing answers is expensive +as bytes on disk AND as tokens in context. + +--- + +## 5. Settled vs open + +**Settled (this session):** + +- Two-surface model (curated vs mechanical) is the construction direction. +- Do **not** derive architecture from code; substrate is impact + assist only. +- Expose shapes + small trusted view library; agent scripts the rest. +- The **entry-adapter trio (E1 `findByConcept` · E2 `byFile` · E3 `bySymbol`)** is built and + verified — the grep→graph bridge, the frozen part of the demand map (§3). `byFile` + returns a _mechanical_ neighborhood for dark files (current dark example: + `config/regex-builders.ts` — 4/5 importers are curated patterns). NB: `fragments/base.ts`, + the original dark exemplar, has since been **annotated** (`@architect-pattern:ProjectionBundle`) + — the fan-in assist loop (§4) closing on its own #2 candidate, so it now demos the _mapped_ path. +- The **graph handle (`graph.ts` → `loadGraph()`)** is the AI-native read surface: one typed + object, joins + taxonomy-decode done once at construction, need-shaped accessors that + return plain composable data. It is the answer to "what type is most natural for Claude" — + **not 30 verbs, not raw-JSON-you-rejoin**, but one object whose method list _is_ the docs. + The built core's quirks (tag-encoding, the 2-hop `implementedBy` join, the dead `layer` axis) + stay decode-detail behind it. **Needs drove the surface, not storage.** +- The **maturity axis is first-class and DERIVED** (`@architect-maturity` is stored 0/293 — + derived from status: candidate→idea · roadmap→plan · active→design · completed→executable; + explicit tag wins). **`invariantsOf` / `specsReverifying` span every tier** and label each + result with maturity **and** a ⊥ provenance axis (live test vs authored working-spec) — so + "specs of any maturity, implemented and non-implemented" are surfaced and distinguished, + never dropped. `maturityLadder()` shows where the non-implemented specs (and their authored + invariants) live — a direct input to the annotation push. +- The handle is **live, not snapshotted** (§9.3/§9.6): both cores build in-process each call + (~1.5s), no `data/` dump. `loadGraph()` is **async**; run every entry with `--conditions=source`. + Determinism, **only where a machine contract later needs it**, remains available as + committed-script + committed-snapshot + re-run-diff — `extract.ts` still emits **sorted** + symbols/edges to make that gate possible — but the agent sink freezes nothing, so today there + is nothing to commit, by design. +- Trust boundary lives in the thin IO runner; views stay pure (§6). + +**Open / next probes (recommended order):** _(situated within the cross-effort sequencing in §9.5 once the `DocumentationProjection` epic is in view)_ + +1. **Value-transfer view** — fold the ephemeral-spec-deletion gate (executable-specs skill) + into a handle method over the data we now expose: a pattern is `deletionReady` when its + authored design-spec invariants each have an `executable`-provenance counterpart. This is + the natural next join — it sits exactly on the maturity×provenance grid `invariantsOf` + already computes, and it directly serves the bloat-removal push (find zombie specs: + implemented but not deleted). Mechanizes `architect/specs/value-transfer-state.feature`. +2. **Act on the live `fan-in` shortlist** — curate the current top few into pattern nodes; watch + `blast` coverage rise. (The first round's targets — `base.ts`, `projection-context.ts`, + `status-values.ts`, `domain-enums.ts` — already graduated; re-derive the tail with the command, + don't re-curate the done.) Real dogfood win. +3. **Symbol-level identity** (demoted — precision, not load-bearing) — key nodes on + `file#symbol`, not file. Sharpens the 24% / 143 _measurement_; the substrate already emits + `symbols[]`. Defer until after the annotation push re-authors the curated layer anyway. +4. **Graduate to a package** — lift `schema.ts` + `graph.ts` + `views.ts` into `packages/` + with proper lint/build once the shapes settle. Keep the curated/mechanical split as two + surfaces, and the handle as the typed front door over both. + +> The previously-queued **"Gherkin-side extractor"** is **struck** — it was a phantom (the +> Gherkin is already in-core; see §3). What looked like a Layer-1 build was a join view. + +--- + +## 6. Trust-boundary lesson (from the security review rounds) + +`blast` takes a raw CLI ref. Three review rounds walked down a ladder of trust; the fix is +instructive for the eventual package: + +1. shell injection → `execFileSync` (no shell). +2. option injection → charset guard (no leading `-`) + `--end-of-options`. +3. pathspec semantic injection → **resolve the input to a git-verified commit SHA first** + (`rev-parse --verify … ^{commit}`), then `git diff --`. + +The first two are _filters_ (reject known-bad, always a step behind). The third **collapses +the ambiguity space**: a verified SHA has exactly one meaning to `git diff`. General rule +for the package's trust boundary: **resolve untrusted input to a canonical, validated +identity at the edge — don't sanitize it in place.** Views stay pure; the runner validates. + +--- + +## 7. How to re-enter + +```bash +# front door — eval anything against the live handle (g): +pnpm playground:q 'g.patterns.length' +# named demo commands: +pnpm playground:cli diff # graph diff +pnpm playground:cli blast HEAD~8 # impact + at-risk specs +pnpm playground:cli fan-in # curation candidates +pnpm playground:cli drift # scoped drift (should be ~0) +pnpm playground:cli census # coverage +``` + +- **The `pnpm playground:*` scripts bake in `--conditions=source`** — without that flag the authored + core silently reads stale `dist/` (§9.6). A bare `tsx` invocation (e.g. a standalone scratch module) + must pass it explicitly. +- **No snapshot on disk.** Both cores build live each call (~1.5s); `data/` is deleted. If a view + ever becomes a machine contract needing determinism, re-introduce a committed snapshot then — + `extract.ts` still emits sorted output for a commit+diff gate. +- `playground/**` is excluded from root ESLint + tsconfig, so it is `tsx`-run only and does + not gate CI. When it graduates to a package, that changes. +- New-session orientation: **`USAGE.md`** (road-test / use the handle) · **`ITERATION.md`** (extend it). + +--- + +## 8. Connections to Architect doctrine + +- **ADR-006 (single read model):** the read model is the `PatternGraph`. The curated layer + here _is_ that graph; the substrate is a _separate_ derived structure, not a competing + read model. **The handle (`graph.ts`) is not a third read model either** — it authors and + persists nothing; it is an in-memory _join + decode_ over the existing read model and the + substrate, built fresh each `loadGraph()`. A typed front door, not a store. +- **ADR-005 (decode-only projection codecs):** every view here is a **lossy one-way + function** (a projection), never a round-trip codec. Do not reach for `z.codec` where a + pure function belongs. The handle's taxonomy decode is one-way at the load boundary — + decode-only, parse-once (ADR-009), never re-encoded. Read `role`/`boundedContext` from the + **structured fields** (`p.role`, 195/293; `p.boundedContext`, 176/293), _not_ the value-form + `directive.tags` — for TS patterns the tags array carries only the bare key, so peeling it + drops ~167 (the bug Codex caught). Maturity is the one tag-or-derive case (no structured field). +- **ADR-007 (taxonomy / status→maturity):** the `maturity` axis is **derived**, not stored — + `MATURITY_BY_STATUS` is ADR-007's `DEFAULT_MATURITY_BY_STATUS`, and an explicit + `@architect-maturity:` tag wins ("explicit always wins"). The handle computes it once; the + built core stores 0 of them. +- **ADR-010 (second-caller bar):** the organizing principle — freeze a projection only when + a second machine consumer needs it. +- **Sink priority (CLAUDE.md):** agents first, Studio view-state second, markdown last. This + playground optimizes the sink the old pipeline optimized _least_. +- **No-BC / live-state:** no historical scaffolding; `drift` flags real deletions, it does + not record "what we replaced" (that is a `git log` question). + +--- + +## 9. Session findings — annotation asymmetry, the live-graph gap, and convergence with `DocumentationProjection` + +Findings from probing the live graph against `architect-core` + `architect-projection`, +plus the load-bearing connection to the `architect/specs/documentation-projection/` epic — +the **fenced producer-side** counterpart to this **unfenced consumer-side** experiment. +Durable working-state; prune each as it graduates to code or an ADR. + +### 9.1 Annotation coverage is two OPPOSITE gaps, not one + +"Coverage" is not "annotate everything to 100%." The two target packages fail in opposite +directions, so the work is bidirectional: + +- **`architect-projection` is over-annotated with deletion candidates.** `63` projection-role + patterns; **60 have zero downstream consumers**, 62 have ≤1 (live `g.patterns` filter). Each + is a bespoke `*Projection` codec paired with a `*Digest`/`*Contract` fragment (~54 contracts) + — the documentType-first star. By ADR-010's own second-caller bar, ~95% never qualified to be + frozen. Work here is **subtractive**. +- **`architect-core` was under-annotated on load-bearing modules** (it has since climbed — census + core is ~65% now, up from ~36% as the fan-in loop closed; re-derive). The original shortlist — + `projection-context.ts`, `base.ts`, `status-values.ts`, `domain-enums.ts` — has **all graduated** + to nodes (`ProjectionContext`, `ProjectionBundle`, `StatusValueDomain`, `DomainEnumSchemas`); the + assist loop closed on its own candidates. `fanInCandidates` now names a fresh tail (live: the + `architect-cli` `_shared` cluster) — **run `pnpm playground:cli fan-in` for the current targets, + never trust a filename frozen here.** Work here is **additive**. + +The two-surface model is what makes the asymmetry safe to act on: `blastRadius` over the +substrate keeps re-test coverage exhaustive while the curated layer stays a deliberate ~6–11% +selection. "Useful coverage" = converge the two flows, not chase a percentage. + +### 9.2 Two of the three design-review lenses are decision-only + +`docs-live/design-review/by-layer.md` and `by-theme.md` carry **only the 14 ADR/PDR records** +(grouped by `@architect-adr-layer` / `@architect-adr-theme` — axes populated only on decisions). +They do not lens the implementation graph at all. Only `by-package.md` is the real component +inventory (and it shows the projection-triad explosion at a glance). For "review core/projection +by layer," the other two are empty calories — a concrete instance of the one-consumer projection +the cut-down in §9.1 targets. + +### 9.3 The live-graph linkage gap — RESOLVED (the handle builds live) + +This was the open WIP-API question; it is now closed. `loadGraph()` builds **both** cores +fresh in-process every call — there is **no dump**: + +``` +annotated source ──buildCliContext({noCache})──▶ live PatternGraph (ADR-006) + ▼ live.ts buildAuthoredCore() → AuthoredCoreSchema.parse(live objects) +packages/*/src ──tsc walk────────────────────▶ extract.ts buildMechanicalCore() → MechanicalCore + ▼ + loadGraph() = new Graph(mechanical, authored) ← async, ~1.5s, reflects HEAD +``` + +`buildAuthoredCore` reuses `buildCliContext` (the snapshot script's own path), so the core is +byte-identical to what every verb/codec consumes — but never written to disk. The dump +(`data/pattern-graph-core.json`) and the mechanical dump are **deleted**; `loadGraph` is now +**async**. See §9.6 for the two stale sources this closed and the `--conditions=source` rule. + +### 9.4 Convergence: this experiment and the `DocumentationProjection` epic are ONE effort from two ends + +The epic converges hard with this playground, which sharpens the sequencing: + +- **Both kill the documentType-first star.** Epic's 2026-06-06 synthesis: "the universal engine + already ships" — `ProjectionBundle{root, children, emission}` + one `scope`-parametrized fragment + - the managed-region engine _is_ the universal machinery; `architecture` and `design-review` are + the **same** fragment four booleans apart. Bespoke per-doc projections fold onto it — "the payload + is mostly subtraction." Same conclusion as §9.1's 60/63, reached independently. +- **Both center on the same move: don't flatten the join.** Epic's one cross-cutting build is + **target-neutrality** — projections bake `{name,role,status,level}` into a mermaid label string, so + only `name`+`role` reach JSON and Studio must re-query. Fix: structured slices, labels deferred to + the renderer. This is _why_ the playground works — it reads the **pre-flatten `--core`** and keeps + data structured + in-process (the `~⅕ context` win). "Type the shapes richly" (`schema.ts`) and + "de-flatten the join" are the same principle on the two sides. +- **The handle is the agent-sink emission the epic already names.** Epic: "a View with **no emission + descriptor** is the sink-agnostic baseline — the bundle handed to the API/MCP consumer or the Studio + view-state sink." `loadGraph()` is precisely the agent-sink reader of that no-descriptor View. Not + competitors — the playground is the no-descriptor sink the epic accounts for. +- **ADR-010's second-caller bar is the shared arbiter.** The projections that **survive** the cut are + the ones a second _machine_ consumer needs — and the Studio live-view (Design-Review = pattern + + dependency subgraph + rule-coverage + conflicts) is that consumer. Everything whose only consumer is + one markdown doc collapses to a scriptable View. Agent sink freezes nothing (scripts the rest); the + machine sinks keep typed contracts. Same bar, two answers. + +### 9.5 Natural sequencing (analysis, not an execution plan) + +1. **Wire the handle live (§9.3).** Smallest step; makes census / fan-in / deletionReady reflect HEAD + and proves the agent-sink reader against the live no-descriptor core. De-risks everything after it. +2. **deletionReady / value-transfer view (§5 #1).** Cheap — sits on the maturity×provenance grid + `invariantsOf` already computes. The _instrument_ that says which projections are safe to collapse + (value transferred, no second consumer). Drives step 4. +3. **De-flatten the join (epic, cross-cutting).** Producer-side enabler: you cannot fold a bespoke + projection onto the universal engine while the engine still flattens. Agent sink doesn't need it; the + _surviving_ (Studio) sink does. Bigger; No-BC in place. +4. **The subtraction (§9.1 + epic).** Collapse the ~60 one-consumer projections onto the universal + engine; add the `architect-core` fan-in modules. The taxonomy cluster (`member 05`, completed) is + the proof the fold-down works. +5. **Graduate the handle (§5 #4) + gating decisions (epic).** Handle → package when shapes settle; + read-model-reach (reflexivity) and ADR-011 (facet helper) only when a genuine heterogeneous second + caller ships (the Studio Design-Review view). + +The decision that is the user's, not the tooling's: the cut is **"delete everything whose only consumer +is one markdown doc; keep what Studio view-state will read"** — deletionReady _informs_ that line, it +does not draw it. + +### 9.6 Staleness — the two sources, and the fix (durable; do not re-derive) + +"The graph isn't live" had **two** independent causes; the sandbox now closes both. + +1. **The dump.** `loadGraph()` used to read a gitignored `data/*.json` snapshot — frozen the + moment it was written. **Fix: deleted.** Both cores build in-process every call + (`buildAuthoredCore` via `buildCliContext`+`noCache`; `buildMechanicalCore` via the tsc walk). + ~1.5s; a just-saved annotation shows on the next call. This also means the silent-failure trap + the annotation fleet hit (an annotation that drops to zero nodes) is now **visible**: re-run + `census`/`q.ts 'g.pattern("X")'` and the node is either there or it isn't. +2. **`dist/`.** The authored build imports `@libar-dev/architect-*`. Node's default export + resolution picks the **compiled `dist/`**, which lags `src/` until `pnpm build`. So even with + no dump, you'd silently read stale pipeline code. **Fix: run with `--conditions=source`** — the + `source` export-condition selects `src/*.ts`. This is non-optional for every playground entry + (`q.ts`, `cli.ts`, any `scratch/` script). It is why `live.ts` documents it loudly. + +A **watch command is unnecessary**: build-fresh-per-call already reflects HEAD. A persistent +watch-server would only matter if per-call latency (~1.5s) became the bottleneck — an MVP +non-problem, and a server is explicitly out of scope (that path is the Studio/MCP surface, which +keeps stable verbs, not this eval sandbox). diff --git a/playground/README.md b/playground/README.md new file mode 100644 index 0000000..cd5a33b --- /dev/null +++ b/playground/README.md @@ -0,0 +1,29 @@ +# playground — scratch home + experiment findings + +**The handle graduated (ADR-014).** The two-surface graph handle that was prototyped here +now lives in the product: pure frozen contract at `@libar-dev/architect-core/graph` +(core schemas · trusted views · Graph), live IO composition in `architect-cli`, and the front door at the `architect` bin +(`pnpm architect:q ''` / `pnpm architect:graph `), regression coverage at +`tests/features/cli/graph-handle.feature`. The operational guide is the +**`architect-graph-handle` skill** (`.agents/skills/architect-graph-handle/`), including the +recipe set under its `references/`. + +What remains here: + +- **`scratch/`** (gitignored) — the multi-line ad-hoc script home. Drop a cut here and pipe + it through the front door: + + ```bash + pnpm architect:q < playground/scratch/my-cut.ts + ``` + +- **`CONTEXT.md`** — the experiment findings that proved the direction (the two-surface + model, curation-not-drift, the context-efficiency numbers). Durable working-state notes; + the decision itself is `architect/decisions/adr-014-agent-read-surface.feature`. +- **`REVIEW-NOTES.md`** — the review record + still-open future-session scope (F1 cohort + promotion, the maturity⟺provenance axis split, the annotation push, deletionReady). +- **`ANNOTATION-FLEET-FINDINGS.md`** — the annotation-campaign findings + verified authoring + rules (the `@architect` marker tag, comma-form `@architect-uses`). + +Prune each findings doc as its content graduates to code, an ADR, or a skill (live-state +doctrine — no dead context). diff --git a/playground/REVIEW-NOTES.md b/playground/REVIEW-NOTES.md new file mode 100644 index 0000000..947425e --- /dev/null +++ b/playground/REVIEW-NOTES.md @@ -0,0 +1,159 @@ +# REVIEW & SCOPE — playground (gen-2-alternative read surface) + +> **Living working-state doc** (live-state doctrine: durable findings + open scope, no +> dates/worklog). This is the **consolidated** record for the review of `playground/` — the +> experimental, source-first agent read surface that is the proposed alternative to the gen-1 +> PatternGraph verb API. Prune each item as it graduates to code, an ADR, or `FEEDBACK.md`. + +Status legend: ◻ scoped (ready, not yet executed) · 🔭 future session. + +--- + +## MVP status: shipped + +The handle MVP is shipped and gated. What landed: + +- **The handle works, the thesis holds.** Script-over-shapes spends ≈⅕ the context of the verb + API; pure views, a clean IO/trust boundary, correct taxonomy decode, sound `isatty(0)` / `REPO_ROOT` + hardening, a well-reasoned freeze-vs-script discipline. Independently ratified by gen-2 (§3). +- **`architect-graph-handle` skill is the canonical on-ramp.** A fresh Claude session discovers and + uses the handle through the skill; the playground `*.md` docs are secondary reference behind it. +- **`pnpm playground:smoke`** — opt-in invariant regression check (asserts invariants, never frozen + counts; not a CI gate, since the playground is CI-excluded). +- **Discoverability wired** — the SessionStart hook + AGENTS.md point at the skill; `pnpm check:skills` + green across `.claude` / `.codex` / `.opencode`. +- **`pnpm playground:q` / `pnpm playground:cli`** bake in `--conditions=source` (the staleness fix) and + are the documented default in every doc; the raw `tsx --conditions=source …` form is reserved for + standalone scratch modules that bypass `q.ts`. +- **Spec-bridge is cohort-honest** (`Invariant.cohort` / `AtRiskSpec.cohort` set on multi-pattern + realizing features), **maturity ⟺ provenance is coherent** (the `specMaturity` clamp; the honest + "drift alarm" preserved as the DRIFT recipe), the **multi-statement argv** form works, the + **empty-`invariantsOf` contract note** is honest (code-originated guarantee is the TS type, not a + Rule), the **at-risk-feature-files** view field no longer collides with the handle's `atRiskSpecs`, + and **bare `playground:q` in automation is documented as forbidden** (it hangs on stdin). + +The hardening rounds and the per-gap fix trail that produced this are git history, not carried here +(live-state: no historical scaffolding). + +--- + +## Real-test validated — both north-star verdicts are YES + +A cold, fresh-context agent gathered the **complete design-review slice** for the +`DocumentationProjection` epic (16 patterns · all edge kinds · ~35 invariants across design specs + +executable tests + source TS, each provenance-labeled) **with zero grep** beyond the 6 subject spec +reads — the slice the shipped `docs-live/design-review/by-layer.md` (14 ADR records only) structurally +cannot produce. Both verdicts came back YES with evidence: + +- **(a) Sufficient to gather design/refactor/impact context without manual repo exploration — YES.** + ~16 graph calls, no grep, dual-provenance value-transfer signal read in one call. Replaces a + multi-hour spelunk with a ~5-minute scripted gather. (Artifact: `scratch/design-review-docproj.md`.) +- **(b) Sufficient to drive the mass annotation campaign — YES, all four moves demonstrated.** + ADD-target via `fan-in`; verify-landed via live rebuild + `census`; **REMOVE noise** via the + significance triage; missing-edge via `graphDiff().aspirational`. + +### What the real test fixed (this session) + +- **The one real gap: `@architect-parent` was dropped by the decoder.** Epic→member membership rode + only on the raw authored pattern, absent from `relationshipIndex` and the decoded node — so an epic's + members read as orphans and had to be reconstructed via the escape hatch. **Fixed:** `parent` + + computed `children` are now first-class `PatternNode` fields (`g.pattern(epic).children` IS the member + set). Recipe: `MEMBERS`. +- **Significance lived in untyped edges.** A naive noise filter over `uses`/`usedBy` false-positived + real realizers (`ManagedRegionEngine`). **Fixed:** `implements` / `implementedBy` / `enforcesDecisions` + typed in `schema.ts` and surfaced on the node — the same "type IS the discovery surface" lesson the + Gherkin under-typing taught. The `TRIAGE` recipe now separates REMOVE-noise from ADD-edges safely. +- **Demand-map trap documented:** file-level impact is `blastRadius([file])`, not + `specsReverifying([file])` (which returns 0 for a realizing impl file whose tests live on the cluster + it implements). Recipe: `IMPACT`. +- **Review-pass fixes:** `invariantsOf` empty-case sharpened + `GUARANTEE` recipe (code contracts); + three stale fan-in shortlists in `CONTEXT.md` repointed to live `fan-in`; smoke no longer leaks a + compile-error stack into a passing run. Smoke stays 8/8. + +**Bottom line: the agent interface is proven useful and effectively unblocks the annotation campaign.** +The remaining items (below) are non-blocking polish, not gates. + +--- + +## Gen-2 (LSDP) exploration — the "executable" answer (kept: it grounds the open §5 items) + +Explored `/Users/darkomijic/dev-projects/libar-software-delivery-protocol/`. The question it resolved: +**what does "executable" mean — a working test, or a design to be implemented?** + +**Answer: a working/bound test.** Gen-2 defines "Executable Spec" as _"an `example` that **has a +verifier** (a delivery fact)"_ and explicitly lists _"a readiness rung"_ as the meaning to AVOID. So +"executable" is a **derived binding fact** (`has-verifier`), never a maturity tier and never +"to-be-built-later." This is why the shipped `specMaturity` clamp enforces `executable` maturity ⟺ +`executable` provenance, and why the honest "live test ∧ status0`, so it sees only patterns that + _directly_ carry Rules — production patterns whose invariants live in realizing features count 0, so + the ladder undercounts realized invariants. The realized view is `g.invariantsOf(name)` (it follows + the `implementedBy` hop). Noted inline at `cli.ts maturity`. +- `blastRadius` / `specsReverifying` seed only from `.ts` files (`#fileToPattern`), so editing a + `.feature` yields no impact — correct for "code-change impact," a gap for "I edited a spec." Noted + inline at `views.ts blastRadius`. + +--- + +## Re-verify (any of these) + +```bash +pnpm playground:cli census # node/edge coverage (read the shape, not a frozen count) +pnpm playground:cli invariants AnnotationCoverageProjection # all [✓exec · executable] + ⚠ cohort-wide +pnpm playground:cli invariants ProjectionContext # the honest code-originated "[] is structural" note +pnpm playground:q 'g.specsReverifying(g.patterns.map(p=>p.name)).filter(s=>s.provenance==="executable" && s.maturity!=="executable").length' # → 0 (coherence holds) +pnpm playground:smoke # invariant regression check (exits 1 on any ✗) +# drift alarm: paste the DRIFT recipe from recipes.md into playground/scratch/x.ts, then: +# pnpm playground:q < playground/scratch/x.ts +``` diff --git a/playground/scratch/.gitignore b/playground/scratch/.gitignore new file mode 100644 index 0000000..3f2da98 --- /dev/null +++ b/playground/scratch/.gitignore @@ -0,0 +1,3 @@ +# Saved ad-hoc scripts are scratch — not tracked. +* +!.gitignore diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c4544d..6141461 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,18 @@ importers: eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@9.39.4) + eslint-import-resolver-typescript: + specifier: ^3.7.0 + version: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.59.3(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4) + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^16.2.7 + version: 16.4.0 prettier: specifier: ^3.8.1 version: 3.8.3 @@ -56,7 +68,7 @@ importers: version: 8.59.3(eslint@9.39.4)(typescript@5.9.3) vitest: specifier: ^4.1.4 - version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)) + version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)) zod: specifier: ^4.1.11 version: 4.4.3 @@ -92,6 +104,9 @@ importers: '@libar-dev/architect-projection': specifier: workspace:* version: link:../architect-projection + typescript: + specifier: ^5.8.2 + version: 5.9.3 zod: specifier: ^4.1.11 version: 4.4.3 @@ -105,12 +120,9 @@ importers: eslint: specifier: ^9.17.0 version: 9.39.4 - typescript: - specifier: ^5.8.2 - version: 5.9.3 vitest: specifier: ^4.1.4 - version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)) + version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)) packages/architect-core: dependencies: @@ -141,7 +153,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.4 - version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)) + version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)) packages/architect-guard: dependencies: @@ -169,7 +181,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.4 - version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)) + version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)) packages/architect-mcp: dependencies: @@ -203,7 +215,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.4 - version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)) + version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)) packages/architect-projection: dependencies: @@ -228,7 +240,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.4 - version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)) + version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)) packages: @@ -592,6 +604,9 @@ packages: '@cfworker/json-schema': optional: true + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: @@ -610,6 +625,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + '@oxc-project/types@0.130.0': resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} @@ -709,6 +728,9 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -730,6 +752,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -798,6 +823,101 @@ packages: resolution: {integrity: sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + '@vitest/coverage-v8@4.1.6': resolution: {integrity: sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==} peerDependencies: @@ -868,6 +988,10 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -890,10 +1014,34 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -901,6 +1049,14 @@ packages: ast-v8-to-istanbul@1.0.0: resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -938,6 +1094,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -968,6 +1128,14 @@ packages: class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + code-block-writer@13.0.3: resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} @@ -978,6 +1146,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1012,6 +1187,26 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1024,6 +1219,14 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1040,6 +1243,10 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1050,6 +1257,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1064,6 +1274,14 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1079,6 +1297,18 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + esbuild@0.28.0: resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} engines: {node: '>=18'} @@ -1097,6 +1327,53 @@ packages: peerDependencies: eslint: '>=7.0.0' + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1155,6 +1432,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventsource-parser@3.0.8: resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} engines: {node: '>=18.0.0'} @@ -1235,6 +1515,10 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -1263,6 +1547,21 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1271,6 +1570,13 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1288,6 +1594,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -1299,14 +1609,29 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.3: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} @@ -1326,6 +1651,11 @@ packages: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -1349,6 +1679,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + ip-address@10.2.0: resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} @@ -1357,18 +1691,77 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1376,14 +1769,53 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1431,6 +1863,10 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -1511,6 +1947,15 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lint-staged@16.4.0: + resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -1525,6 +1970,10 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -1566,6 +2015,10 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -1596,6 +2049,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -1603,6 +2061,10 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1611,6 +2073,30 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -1621,6 +2107,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1628,6 +2118,10 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -1684,6 +2178,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} @@ -1717,6 +2214,10 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss@8.5.14: resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} @@ -1772,6 +2273,14 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1784,9 +2293,24 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} rolldown@1.0.1: resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==} @@ -1800,9 +2324,25 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.8.0: resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} engines: {node: '>=10'} @@ -1816,6 +2356,18 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -1854,6 +2406,14 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1864,6 +2424,9 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -1874,6 +2437,14 @@ packages: std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1882,6 +2453,26 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.1: + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} + engines: {node: '>=20'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1902,6 +2493,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -1938,6 +2533,9 @@ packages: ts-morph@28.0.0: resolution: {integrity: sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==} + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1954,6 +2552,22 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + typescript-eslint@8.59.3: resolution: {integrity: sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1966,6 +2580,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -1977,6 +2595,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2072,6 +2693,22 @@ packages: jsdom: optional: true + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2094,9 +2731,18 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2117,7 +2763,7 @@ snapshots: minimist: 1.2.8 parsecurrency: 1.1.1 ts-morph: 28.0.0 - vitest: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)) + vitest: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)) '@babel/helper-string-parser@7.27.1': {} @@ -2513,6 +3159,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -2532,6 +3185,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@nolyfill/is-core-module@1.0.39': {} + '@oxc-project/types@0.130.0': {} '@pkgjs/parseargs@0.11.0': @@ -2588,6 +3243,8 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rtsao/scc@1.1.0': {} + '@standard-schema/spec@1.1.0': {} '@ts-morph/common@0.29.0': @@ -2612,6 +3269,8 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/json5@0.0.29': {} + '@types/node@12.20.55': {} '@types/node@24.12.4': @@ -2711,6 +3370,65 @@ snapshots: '@typescript-eslint/types': 8.59.3 eslint-visitor-keys: 5.0.1 + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + '@vitest/coverage-v8@4.1.6(vitest@4.1.6)': dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -2723,7 +3441,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)) + vitest: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)) '@vitest/expect@4.1.6': dependencies: @@ -2734,13 +3452,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.6(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0))': + '@vitest/mocker@4.1.6(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0) + vite: 8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0) '@vitest/pretty-format@4.1.6': dependencies: @@ -2797,6 +3515,10 @@ snapshots: ansi-colors@4.1.3: {} + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -2813,8 +3535,58 @@ snapshots: argparse@2.0.1: {} + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + array-union@2.1.0: {} + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} ast-v8-to-istanbul@1.0.0: @@ -2823,6 +3595,12 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -2869,6 +3647,13 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2893,6 +3678,15 @@ snapshots: class-transformer@0.5.1: {} + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.1 + code-block-writer@13.0.3: {} color-convert@2.0.1: @@ -2901,6 +3695,10 @@ snapshots: color-name@1.1.4: {} + colorette@2.0.20: {} + + commander@14.0.3: {} + concat-map@0.0.1: {} content-disposition@1.1.0: {} @@ -2926,12 +3724,46 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + debug@4.4.3: dependencies: ms: 2.1.3 deep-is@0.1.4: {} + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + depd@2.0.0: {} detect-indent@6.1.0: {} @@ -2942,6 +3774,10 @@ snapshots: dependencies: path-type: 4.0.0 + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2952,6 +3788,8 @@ snapshots: ee-first@1.1.1: {} + emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -2963,6 +3801,65 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + environment@1.1.0: {} + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -2973,6 +3870,23 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.3 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + esbuild@0.28.0: optionalDependencies: '@esbuild/aix-ppc64': 0.28.0 @@ -3010,6 +3924,69 @@ snapshots: dependencies: eslint: 9.39.4 + eslint-import-resolver-node@0.3.10: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.4 + get-tsconfig: 4.14.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.16 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.3(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.3(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.59.3(eslint@9.39.4)(typescript@5.9.3) + eslint: 9.39.4 + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.4 + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.3(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4) + hasown: 2.0.3 + is-core-module: 2.16.2 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.59.3(eslint@9.39.4)(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -3086,6 +4063,8 @@ snapshots: etag@1.8.1: {} + eventemitter3@5.0.4: {} + eventsource-parser@3.0.8: {} eventsource@3.0.7: @@ -3194,6 +4173,10 @@ snapshots: flatted@3.4.2: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -3220,6 +4203,21 @@ snapshots: function-bind@1.1.2: {} + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.3 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3238,6 +4236,16 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -3257,6 +4265,11 @@ snapshots: globals@14.0.0: {} + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -3270,10 +4283,24 @@ snapshots: graceful-fs@4.2.11: {} + has-bigints@1.1.0: {} + has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.3: dependencies: function-bind: 1.1.2 @@ -3292,6 +4319,8 @@ snapshots: human-id@4.1.3: {} + husky@9.1.7: {} + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -3309,28 +4338,144 @@ snapshots: inherits@2.0.4: {} + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.3 + side-channel: 1.1.0 + ip-address@10.2.0: {} ipaddr.js@1.9.1: {} + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.8.0 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.3 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-extglob@2.1.1: {} + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.6.0 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} is-promise@4.0.0: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-windows@1.0.2: {} + isarray@2.0.5: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -3375,6 +4520,10 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json5@1.0.2: + dependencies: + minimist: 1.2.8 + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -3437,6 +4586,24 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lint-staged@16.4.0: + dependencies: + commander: 14.0.3 + listr2: 9.0.5 + picomatch: 4.0.4 + string-argv: 0.3.2 + tinyexec: 1.1.2 + yaml: 2.9.0 + + listr2@9.0.5: + dependencies: + cli-truncate: 5.2.0 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -3449,6 +4616,14 @@ snapshots: lodash.startcase@4.4.0: {} + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + lru-cache@10.4.3: {} magic-string@0.30.21: @@ -3484,6 +4659,8 @@ snapshots: dependencies: mime-db: 1.54.0 + mimic-function@5.0.1: {} + minimatch@10.2.5: dependencies: brace-expansion: 5.0.6 @@ -3506,14 +4683,61 @@ snapshots: nanoid@3.3.12: {} + napi-postinstall@0.3.4: {} + natural-compare@1.4.0: {} negotiator@1.0.0: {} + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + object-assign@4.1.1: {} object-inspect@1.13.4: {} + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + obug@2.1.1: {} on-finished@2.4.1: @@ -3524,6 +4748,10 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3535,6 +4763,12 @@ snapshots: outdent@0.5.0: {} + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + p-filter@2.1.0: dependencies: p-map: 2.1.0 @@ -3579,6 +4813,8 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 @@ -3600,6 +4836,8 @@ snapshots: pkce-challenge@5.0.1: {} + possible-typed-array-names@1.1.0: {} + postcss@8.5.14: dependencies: nanoid: 3.3.12 @@ -3647,14 +4885,52 @@ snapshots: reflect-metadata@0.2.2: {} + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + require-from-string@2.0.2: {} resolve-from@4.0.0: {} resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + reusify@1.1.0: {} + rfdc@1.4.1: {} + rolldown@1.0.1: dependencies: '@oxc-project/types': 0.130.0 @@ -3690,8 +4966,29 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safer-buffer@2.1.2: {} + semver@6.3.1: {} + semver@7.8.0: {} send@1.2.1: @@ -3719,6 +5016,28 @@ snapshots: transitivePeerDependencies: - supports-color + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + setprototypeof@1.2.0: {} shebang-command@2.0.0: @@ -3761,6 +5080,16 @@ snapshots: slash@3.0.0: {} + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + source-map-js@1.2.1: {} spawndamnit@3.0.1: @@ -3770,12 +5099,21 @@ snapshots: sprintf-js@1.0.3: {} + stable-hash@0.0.5: {} + stackback@0.0.2: {} statuses@2.0.2: {} std-env@4.1.0: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-argv@0.3.2: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -3788,6 +5126,40 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string-width@8.2.1: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -3804,6 +5176,8 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + term-size@2.2.1: {} tinybench@2.9.0: {} @@ -3832,6 +5206,13 @@ snapshots: '@ts-morph/common': 0.29.0 code-block-writer: 13.0.3 + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + tslib@2.8.1: optional: true @@ -3851,6 +5232,39 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + typescript-eslint@8.59.3(eslint@9.39.4)(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.59.3(@typescript-eslint/parser@8.59.3(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) @@ -3864,12 +5278,43 @@ snapshots: typescript@5.9.3: {} + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + undici-types@7.16.0: {} universalify@0.1.2: {} unpipe@1.0.0: {} + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -3878,7 +5323,7 @@ snapshots: vary@1.1.2: {} - vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0): + vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -3890,11 +5335,12 @@ snapshots: esbuild: 0.28.0 fsevents: 2.3.3 tsx: 4.22.0 + yaml: 2.9.0 - vitest@4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)): + vitest@4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.6 - '@vitest/mocker': 4.1.6(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)) + '@vitest/mocker': 4.1.6(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.6 '@vitest/runner': 4.1.6 '@vitest/snapshot': 4.1.6 @@ -3911,7 +5357,7 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0) + vite: 8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.12.4 @@ -3919,6 +5365,47 @@ snapshots: transitivePeerDependencies: - msw + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -3942,8 +5429,16 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.2.0 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrappy@1.0.2: {} + yaml@2.9.0: {} + yocto-queue@0.1.0: {} zod-to-json-schema@3.25.2(zod@4.4.3): diff --git a/scripts/architect-bin-smoke.mjs b/scripts/architect-bin-smoke.mjs new file mode 100644 index 0000000..e7e78ed --- /dev/null +++ b/scripts/architect-bin-smoke.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +// @ts-check +/** + * architect-bin-smoke — the only guard over the @libar-dev/architect umbrella. + * + * packages/architect ("@libar-dev/architect") is a bin-only meta-package: its + * sole load-bearing content is the 7 hand-written shims in + * `packages/architect/bin/*.js`, each a one-line side-effect import of an owner + * package's bin entry (`import '@libar-dev/architect-cli/bin/architect'`, etc.). + * Those imports couple by hardcoded specifier to the owner packages' `exports` + * maps. Nothing else re-checks that coupling: the umbrella declares no + * build/typecheck/lint scripts, so the workspace fan-outs (`pnpm -r --filter + * './packages/**' build|typecheck|lint`) skip it entirely. A rename or drop of + * a `./bin/` export in architect-cli / architect-mcp would keep every + * other gate green and surface only at a published consumer's `npx architect`. + * + * This smoke runs each of the 7 bins through `node --help` and fails on + * any non-zero exit (a broken specifier throws ERR_MODULE_NOT_FOUND). It is the + * umbrella's `test` script (so `pnpm test` covers it) AND its `prepack` (so it + * runs immediately before the tarball is built at publish time). It assumes a + * fresh `dist/` — run `pnpm build` first; `ci:verify` does exactly that. + * + * Restores the guard the source repo shipped as `ci:architect:split-smoke` + * (`architect --help && architect-mcp --help`), extended from 2 bins to all 7. + */ +import { execFileSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { resolve, dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const binDir = join(repoRoot, 'packages', 'architect', 'bin'); + +// The umbrella's published bin map (packages/architect/package.json "bin"). +const BINS = [ + 'architect', + 'architect-generate', + 'architect-guard', + 'architect-lint-patterns', + 'architect-lint-steps', + 'architect-validate', + 'architect-mcp', +]; + +const PER_BIN_TIMEOUT_MS = 30_000; + +/** @type {{ bin: string; ok: boolean; detail: string }[]} */ +const results = []; + +for (const bin of BINS) { + const binPath = join(binDir, `${bin}.js`); + if (!existsSync(binPath)) { + results.push({ bin, ok: false, detail: `missing shim file: ${binPath}` }); + continue; + } + try { + execFileSync(process.execPath, [binPath, '--help'], { + stdio: 'pipe', + timeout: PER_BIN_TIMEOUT_MS, + encoding: 'utf8', + }); + results.push({ bin, ok: true, detail: 'exit 0' }); + } catch (/** @type {any} */ err) { + const stderr = typeof err?.stderr === 'string' ? err.stderr.trim() : ''; + const reason = err?.signal + ? `killed by ${err.signal} (timeout ${PER_BIN_TIMEOUT_MS}ms?)` + : `exit ${err?.status ?? '?'}`; + const head = stderr.split('\n').slice(0, 4).join('\n '); + results.push({ bin, ok: false, detail: `${reason}${head ? `\n ${head}` : ''}` }); + } +} + +const failed = results.filter((r) => !r.ok); + +for (const r of results) { + console.log(`${r.ok ? '✓' : '✗'} architect bin: ${r.bin} — ${r.detail}`); +} + +if (failed.length > 0) { + console.error( + `\n@libar-dev/architect bin smoke FAILED: ${failed.length}/${BINS.length} bin(s) broken.\n` + + `Each umbrella shim re-imports an owner-package bin export — a failure usually means an\n` + + `owner's package.json "exports" ./bin/ subpath was renamed/dropped, or dist/ is\n` + + `stale (run \`pnpm build\`). Broken: ${failed.map((f) => f.bin).join(', ')}.`, + ); + process.exit(1); +} + +console.log( + `\n@libar-dev/architect bin smoke OK — all ${BINS.length} umbrella bins resolve and run.`, +); diff --git a/scripts/assert-deprecated-query-surfaces.ts b/scripts/assert-deprecated-query-surfaces.ts deleted file mode 100644 index c54372a..0000000 --- a/scripts/assert-deprecated-query-surfaces.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { validateCommandInput } from '../../architect-cli/src/cli/pattern-graph-cli-commands.js'; - -interface DeprecatedSurfaceCheck { - readonly name: string; - readonly command: string; - readonly args: readonly string[]; - readonly expectedSnippet: string; -} - -const CHECKS: readonly DeprecatedSurfaceCheck[] = [ - { - name: 'arch layer', - command: 'arch', - args: ['layer'], - expectedSnippet: 'Unknown arch subcommand: layer', - }, - { - name: 'list --phase', - command: 'list', - args: ['--phase', '1'], - expectedSnippet: 'Unknown option: --phase', - }, - { - name: 'list --maturity', - command: 'list', - args: ['--maturity', 'active'], - expectedSnippet: 'Unknown option: --maturity', - }, -]; - -const failures: string[] = []; - -for (const check of CHECKS) { - try { - validateCommandInput(check.command, check.args); - failures.push( - `Deprecated query surface unexpectedly succeeded for ${check.name}: architect ${check.command} ${check.args.join(' ')}` - ); - continue; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if (!message.includes(check.expectedSnippet)) { - failures.push( - `Deprecated query surface for ${check.name} failed without expected output. Expected snippet: ${check.expectedSnippet}\nActual output:\n${message}` - ); - continue; - } - } - - process.stdout.write( - `deprecated query surface ok: ${check.name} still fails with \`${check.expectedSnippet}\`\n` - ); -} - -if (failures.length > 0) { - throw new Error(failures.join('\n\n')); -} diff --git a/scripts/check-build-fresh.mjs b/scripts/check-build-fresh.mjs new file mode 100644 index 0000000..3f2db5e --- /dev/null +++ b/scripts/check-build-fresh.mjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node +// @ts-check +/** + * check-build-fresh — staleness gate for the workspace `dist/` outputs. + * + * The dogfood graph scripts (`architect:q` / `architect:graph`) run + * under `tsx --conditions=source` and resolve the workspace packages from `src/`, + * so they are always live. But the *built bins* still execute from `dist/`: + * + * - `architect-generate` (behind `docs:all` and every `docs:*`) + * - `architect-guard` (behind `architect:guard` / `:guard:all`) + * - `architect-validate` (behind `validate:all` / `validate:patterns`) + * + * If `src/` has moved ahead of `dist/`, those bins run OLD compiled code and + * answer confidently with stale shapes — a silent-wrong-answer, the most + * dangerous failure mode. The determinism gate (`pnpm docs:all && git diff + * --exit-code docs-live`) is only trustworthy once `dist/` matches `src/`. + * + * This gate converts that silent-wrong-answer into a loud stop. For each + * package it compares the newest `src/**\/*.ts` mtime against the newest + * `dist/**\/*.js` mtime; if source is ahead (or `dist/` is missing) it exits + * non-zero and tells you to run `pnpm build`. + * + * Mtime-based, so a `git checkout` that rewrites source mtimes can report a + * false "stale" — but the remedy (`pnpm build`) is an incremental `tsc -b` + * no-op in that case, so a false positive costs ~a second; a false negative in + * the edit-then-run loop is effectively impossible. + * + * CI is immune by construction (a clean checkout always builds before these + * run); this gate earns its keep locally, mid-refactor. + * + * Run via `pnpm check:build`. Exits non-zero with a per-package message on failure. + */ +import { readdirSync, existsSync, statSync } from 'node:fs'; +import { resolve, dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const packagesDir = join(repoRoot, 'packages'); + +/** + * Newest mtime (ms) of any file under `dir` whose name ends in one of `exts`. + * Returns 0 when `dir` is absent or contains no matching files. + * @param {string} dir + * @param {string[]} exts + * @returns {number} + */ +function newestMtime(dir, exts) { + if (!existsSync(dir)) return 0; + let newest = 0; + /** @param {string} d */ + const walk = (d) => { + for (const entry of readdirSync(d, { withFileTypes: true })) { + if (entry.name === 'node_modules') continue; + const p = join(d, entry.name); + if (entry.isDirectory()) { + walk(p); + } else if (exts.some((ext) => entry.name.endsWith(ext))) { + const m = statSync(p).mtimeMs; + if (m > newest) newest = m; + } + } + }; + walk(dir); + return newest; +} + +const packages = readdirSync(packagesDir, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .filter((name) => existsSync(join(packagesDir, name, 'src'))); + +/** @type {string[]} */ +const stale = []; + +for (const pkg of packages) { + const base = join(packagesDir, pkg); + const distDir = join(base, 'dist'); + if (!existsSync(distDir)) { + stale.push(`${pkg}: dist/ is missing — never built`); + continue; + } + const srcNewest = newestMtime(join(base, 'src'), ['.ts']); + const distNewest = newestMtime(distDir, ['.js']); + if (srcNewest > distNewest) { + const lagSec = Math.round((srcNewest - distNewest) / 1000); + stale.push(`${pkg}: src/ is ${lagSec}s ahead of dist/`); + } +} + +if (stale.length > 0) { + console.error('\n✖ Stale build — dist/ is behind src/ in:\n'); + for (const line of stale) console.error(` ${line}`); + console.error( + '\nThe built bins (architect-generate, architect-guard, architect-validate)\n' + + 'run from dist/, so they would project from OLD code and answer wrong.\n' + + '\n Run: pnpm build\n', + ); + process.exit(1); +} + +console.log(`✓ dist/ is fresh for all ${packages.length} packages`); diff --git a/scripts/check-skill-symlinks.mjs b/scripts/check-skill-symlinks.mjs new file mode 100644 index 0000000..fe9eefc --- /dev/null +++ b/scripts/check-skill-symlinks.mjs @@ -0,0 +1,176 @@ +#!/usr/bin/env node +// @ts-check +/** + * check-skill-symlinks — drift guard for the skill wiring. + * + * Canonical skill content lives in `.agents/skills/`. Each harness dir + * (`.codex/skills/`, `.claude/skills/`, `.opencode/skills/`) symlinks into it. Nothing keeps the + * three in sync automatically, so they drift — this asserts the invariants: + * + * 1. `.codex/skills/` is a directory symlink to `.agents/skills/`. + * 2. No dangling symlinks in any per-skill harness skills dir (every target resolves). + * 3. Every per-skill harness entry is a symlink pointing at the matching + * `.agents/skills/` (no stray targets, no orphan names that no longer + * exist in the canonical set). + * 4. `.claude/skills/` MIRRORS the full canonical set — a symlink for every + * skill (Claude is the superset). + * 5. `.opencode/skills/` MIRRORS the canonical `architect-*` domain skills — + * the namespace OmO actually consumes (matches the `architect-*` allow rule + * in `.opencode/opencode.jsonc`). Non-`architect-*` skills (e.g. Claude-side + * authoring tools) are Claude-only by convention and are not required here. + * + * Per-harness "required" sets are derived from the canonical skill names by + * convention (full set / `architect-*` prefix) — no skill name is hardcoded. + * This is what catches the real regression: a domain skill present in + * `.agents/skills/` but missing from a harness it belongs in. + * + * It also validates each canonical SKILL.md's frontmatter against the two + * constraints stricter loaders enforce (Codex CLI rejects skills that violate + * either; Claude Code is lenient, so they slip through unnoticed otherwise): + * + * 5. `description` is ≤ 1024 chars (Agent Skills spec maximum). + * 6. `description` is a YAML-safe single-line scalar — an unquoted `: ` + * (colon-space) is parsed as a mapping indicator and breaks the frontmatter. + * + * Run via `pnpm check:skills`. Exits non-zero with a per-violation message on failure. + */ +import { readdirSync, existsSync, readlinkSync, readFileSync } from 'node:fs'; +import { resolve, dirname, join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const CANON = join(repoRoot, '.agents', 'skills'); +const CODEX_SKILLS = join(repoRoot, '.codex', 'skills'); + +/** + * Each harness declares the canonical skills it MUST carry, derived from the + * canonical names by convention — never an explicit name list. + * @type {{ dir: string, label: string, required: (names: string[]) => string[] }[]} + */ +const HARNESSES = [ + // Claude carries the full canonical set (superset). + { dir: join(repoRoot, '.claude', 'skills'), label: 'full canonical set', required: (names) => names }, + // OmO carries the `architect-*` domain skills (the namespace it consumes); + // non-`architect-*` skills are Claude-only by convention. + { + dir: join(repoRoot, '.opencode', 'skills'), + label: 'the canonical `architect-*` skills', + required: (names) => names.filter((n) => n.startsWith('architect-')), + }, +]; + +const rel = (/** @type {string} */ p) => relative(repoRoot, p) || '.'; +/** @type {string[]} */ +const errors = []; + +// Canonical skills: entries under .agents/skills that carry a SKILL.md. +const canon = new Set( + readdirSync(CANON, { withFileTypes: true }) + .filter((e) => e.isDirectory() || e.isSymbolicLink()) + .map((e) => e.name) + .filter((name) => existsSync(join(CANON, name, 'SKILL.md'))), +); + +if (canon.size === 0) { + console.error(`✗ no canonical skills found under ${rel(CANON)} — wrong repo root?`); + process.exit(1); +} + +if (!existsSync(CODEX_SKILLS)) { + errors.push(`missing Codex skills symlink: ${rel(CODEX_SKILLS)} → ${rel(CANON)}`); +} else { + let target; + try { + target = readlinkSync(CODEX_SKILLS); + } catch { + errors.push(`${rel(CODEX_SKILLS)} is not a symlink (Codex should point directly at .agents/skills/)`); + } + + if (target !== undefined) { + if (!existsSync(CODEX_SKILLS)) { + errors.push(`${rel(CODEX_SKILLS)} → ${target} is DANGLING (target does not exist)`); + } else if (resolve(dirname(CODEX_SKILLS), target) !== CANON) { + errors.push(`${rel(CODEX_SKILLS)} → ${target} should point at ${rel(CANON)}`); + } + } +} + +for (const { dir, label, required } of HARNESSES) { + if (!existsSync(dir)) { + errors.push(`missing harness skills dir: ${rel(dir)}`); + continue; + } + + const present = new Set(); + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.name.startsWith('.')) continue; // .DS_Store etc. + const p = join(dir, entry.name); + + if (!entry.isSymbolicLink()) { + errors.push(`${rel(p)} is not a symlink (harness skill dirs must symlink into .agents/skills/)`); + continue; + } + present.add(entry.name); + + const target = readlinkSync(p); + if (!existsSync(p)) { + errors.push(`${rel(p)} → ${target} is DANGLING (target does not exist)`); + continue; + } + const expected = join(CANON, entry.name); + if (resolve(dir, target) !== expected) { + errors.push(`${rel(p)} → ${target} should point at ${rel(expected)}`); + } + if (!canon.has(entry.name)) { + errors.push(`${rel(p)} symlinks "${entry.name}", which is not a skill in .agents/skills/ (orphaned or renamed?)`); + } + } + + for (const name of required([...canon])) { + if (!present.has(name)) { + errors.push(`${rel(dir)} is missing a symlink for canonical skill "${name}" (must carry ${label})`); + } + } +} + +// Frontmatter validation: assert each canonical SKILL.md's `description` +// stays within the Agent Skills spec limit and is YAML-safe. Lexical check +// (no YAML dependency) targeting exactly the two failure modes strict loaders +// reject — our descriptions are single-line scalars by convention. +const DESCRIPTION_MAX = 1024; + +for (const name of canon) { + const file = join(CANON, name, 'SKILL.md'); + const fmMatch = readFileSync(file, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!fmMatch) { + errors.push(`${rel(file)} has no YAML frontmatter block (expected a leading --- ... --- fence)`); + continue; + } + const descLine = fmMatch[1].split(/\r?\n/).find((line) => line.startsWith('description:')); + if (descLine === undefined) { + errors.push(`${rel(file)} frontmatter has no description field`); + continue; + } + const value = descLine.slice('description:'.length).trim(); + if (value.length > DESCRIPTION_MAX) { + errors.push(`${rel(file)} description is ${value.length} chars (max ${DESCRIPTION_MAX})`); + } + // Quoted / block scalars (" ' | >) carry their own escaping; only plain + // scalars are broken by an unquoted colon-space. + if (!/^["'|>]/.test(value) && value.includes(': ')) { + errors.push(`${rel(file)} description has an unquoted ": " (colon-space) — breaks YAML parsing; rephrase or quote`); + } +} + +if (errors.length > 0) { + console.error(`✗ skill check failed (${errors.length} issue${errors.length === 1 ? '' : 's'}):`); + for (const e of errors) console.error(` - ${e}`); + process.exit(1); +} + +console.log( + `✓ skills OK — ${canon.size} canonical skills; no dangling links; ` + + `.codex points at .agents/skills; .claude mirrors the full set; ` + + `.opencode mirrors the architect-* domain skills; ` + + `all descriptions ≤${DESCRIPTION_MAX} chars and YAML-safe.`, +); diff --git a/scripts/generate-docs.mjs b/scripts/generate-docs.mjs deleted file mode 100644 index 18ee709..0000000 --- a/scripts/generate-docs.mjs +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env node - -import { spawnSync } from 'node:child_process'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const ARCHITECT_GENERATE_BIN = path.resolve(__dirname, '../node_modules/.bin/architect-generate'); - -const child = spawnSync(ARCHITECT_GENERATE_BIN, process.argv.slice(2), { - cwd: process.cwd(), - stdio: 'inherit', - env: process.env, -}); - -if (typeof child.status === 'number') { - process.exit(child.status); -} - -process.exit(1); diff --git a/scripts/guard-no-suppressions.baseline.json b/scripts/guard-no-suppressions.baseline.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/scripts/guard-no-suppressions.baseline.json @@ -0,0 +1 @@ +[] diff --git a/scripts/guard-no-suppressions.mjs b/scripts/guard-no-suppressions.mjs new file mode 100644 index 0000000..e0d5684 --- /dev/null +++ b/scripts/guard-no-suppressions.mjs @@ -0,0 +1,281 @@ +#!/usr/bin/env node +/** + * Out-of-band guard for the no-BC suppression-comment doctrine. + * + * Mirrors the ESLint `architect-local/no-suppression-comments` rule but runs + * standalone, so file-only commits and partial CI lanes still gate the + * doctrine. Scans `packages/*\/src/` for `eslint-disable`, `@ts-ignore`, + * `@ts-expect-error`, and `@ts-nocheck` markers and compares the result to + * `scripts/guard-no-suppressions.baseline.json`. + * + * Regenerate the baseline only with `ALLOW_SUPPRESSION_BASELINE_REGEN=1 + * pnpm guard:no-suppressions -- --regenerate-baseline` after a deliberate + * doctrine carve-out. + */ +import { createHash } from 'node:crypto'; +import { readFile, readdir, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +const ROOT = process.cwd(); +const BASELINE_PATH = path.join(ROOT, 'scripts/guard-no-suppressions.baseline.json'); +const SEARCH_ROOTS = ['packages']; +const SOURCE_SEGMENT = `${path.sep}src${path.sep}`; +const TARGET_EXTENSIONS = new Set(['.ts', '.tsx']); +const SUPPRESSION_PATTERN = /eslint-disable|@ts-ignore|@ts-expect-error|@ts-nocheck/u; +const BASELINE_REGEN_ENV = 'ALLOW_SUPPRESSION_BASELINE_REGEN'; + +export function createSuppressionTextHash(match) { + return createHash('sha256') + .update( + JSON.stringify({ + file: match.file, + line: match.line, + text: match.text, + }), + ) + .digest('hex'); +} + +export function toBaselineEntries(matches) { + return sortBaselineEntries( + matches.map((match) => ({ + file: match.file, + line: match.line, + textHash: createSuppressionTextHash(match), + })), + ); +} + +export function sortBaselineEntries(entries) { + return [...entries].sort( + (left, right) => + left.file.localeCompare(right.file) || + left.line - right.line || + left.textHash.localeCompare(right.textHash), + ); +} + +export function compareSuppressionBaseline(matches, baselineEntries) { + const actualEntries = toBaselineEntries(matches); + const actualSet = new Set(actualEntries.map(formatBaselineKey)); + const baselineSet = new Set(baselineEntries.map(formatBaselineKey)); + + return { + actualEntries, + additions: matches.filter((match) => !baselineSet.has(formatBaselineKey(matchToEntry(match)))), + removals: baselineEntries.filter((entry) => !actualSet.has(formatBaselineKey(entry))), + }; +} + +export async function readSuppressionBaseline(baselinePath) { + const parsed = JSON.parse(await readFile(baselinePath, 'utf8')); + + if (!Array.isArray(parsed)) { + throw new Error(`Invalid suppression baseline in ${baselinePath}: expected an array`); + } + + return sortBaselineEntries( + parsed.map((entry) => { + if ( + entry === null || + typeof entry !== 'object' || + typeof entry.file !== 'string' || + !Number.isInteger(entry.line) || + entry.line < 1 || + typeof entry.textHash !== 'string' || + entry.textHash.length === 0 + ) { + throw new Error(`Invalid suppression baseline entry in ${baselinePath}`); + } + + return { + file: entry.file, + line: entry.line, + textHash: entry.textHash, + }; + }), + ); +} + +export async function writeSuppressionBaseline(baselinePath, entries) { + await writeFile( + baselinePath, + `${JSON.stringify(sortBaselineEntries(entries), null, 2)}\n`, + 'utf8', + ); +} + +export async function collectSuppressionMatches({ + root = ROOT, + searchRoots = SEARCH_ROOTS, + sourceSegment = SOURCE_SEGMENT, +} = {}) { + const matchesByRoot = await Promise.all( + searchRoots.map((searchRoot) => + walk(path.join(root, searchRoot), { + root, + sourceSegment, + }), + ), + ); + const matches = matchesByRoot.flat(); + + matches.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column); + return matches; +} + +export async function runSuppressionGuard({ + root = ROOT, + baselinePath = BASELINE_PATH, + searchRoots = SEARCH_ROOTS, + regenerateBaseline = false, + allowBaselineRegeneration = process.env[BASELINE_REGEN_ENV] === '1', +} = {}) { + const matches = await collectSuppressionMatches({ root, searchRoots }); + const actualEntries = toBaselineEntries(matches); + + if (regenerateBaseline) { + if (!allowBaselineRegeneration) { + return { + ok: false, + matches, + actualEntries, + additions: [], + removals: [], + message: `Refusing to regenerate suppression baseline without ${BASELINE_REGEN_ENV}=1.`, + }; + } + + await writeSuppressionBaseline(baselinePath, actualEntries); + return { + ok: true, + matches, + actualEntries, + additions: [], + removals: [], + message: `No-BC guard baseline regenerated with ${matches.length} suppression marker(s).`, + }; + } + + const baselineEntries = await readSuppressionBaseline(baselinePath); + const comparison = compareSuppressionBaseline(matches, baselineEntries); + const ok = comparison.additions.length === 0 && comparison.removals.length === 0; + + return { + ok, + matches, + actualEntries: comparison.actualEntries, + additions: comparison.additions, + removals: comparison.removals, + message: ok + ? `No-BC guard passed: found ${matches.length} suppression marker(s), all match the path-aware baseline.` + : `No-BC guard failed: found ${comparison.additions.length} added and ${comparison.removals.length} removed suppression marker(s).`, + }; +} + +async function walk(directory, options) { + let entries; + try { + entries = await readdir(directory, { withFileTypes: true }); + } catch (error) { + if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { + return []; + } + throw error; + } + + const matchesByEntry = await Promise.all( + entries.map(async (entry) => { + if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name.startsWith('.')) { + return []; + } + + const absolutePath = path.join(directory, entry.name); + if (entry.isDirectory()) { + return walk(absolutePath, options); + } + + if (!entry.isFile() || !TARGET_EXTENSIONS.has(path.extname(entry.name))) { + return []; + } + + if (!absolutePath.includes(options.sourceSegment)) { + return []; + } + + return scanFile(absolutePath, options.root); + }), + ); + + return matchesByEntry.flat(); +} + +async function scanFile(absolutePath, root) { + const content = await readFile(absolutePath, 'utf8'); + const relativePath = path.relative(root, absolutePath); + const lines = content.split(/\r?\n/u); + const matches = []; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const match = SUPPRESSION_PATTERN.exec(line); + if (match) { + matches.push({ + file: relativePath, + line: index + 1, + column: match.index + 1, + text: line, + }); + } + } + + return matches; +} + +function matchToEntry(match) { + return { + file: match.file, + line: match.line, + textHash: createSuppressionTextHash(match), + }; +} + +function formatBaselineKey(entry) { + return `${entry.file}:${String(entry.line)}:${entry.textHash}`; +} + +function printGuardResult(result) { + if (result.ok) { + console.log(result.message); + return; + } + + console.error(result.message); + + if (result.additions.length > 0) { + console.error('Added suppressions:'); + for (const match of result.additions) { + console.error(`${match.file}:${match.line}:${match.column}: ${match.text.trim()}`); + } + } + + if (result.removals.length > 0) { + console.error('Removed or moved baseline suppressions:'); + for (const entry of result.removals) { + console.error(`${entry.file}:${entry.line}: ${entry.textHash}`); + } + } +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const result = await runSuppressionGuard({ + regenerateBaseline: process.argv.includes('--regenerate-baseline'), + }); + + printGuardResult(result); + if (!result.ok) { + process.exit(1); + } +} diff --git a/scripts/jsdoc-boilerplate-audit.mjs b/scripts/jsdoc-boilerplate-audit.mjs new file mode 100644 index 0000000..4a37743 --- /dev/null +++ b/scripts/jsdoc-boilerplate-audit.mjs @@ -0,0 +1,85 @@ +import { readdir, readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +const boilerplatePhrases = [ + 'As a typed contract', + 'data shape consumed by projection or render layers', + 'Private helpers used exclusively', +]; + +function resolveSrcRoot() { + const srcRootArg = process.argv[2]; + if (srcRootArg === undefined || srcRootArg.length === 0) { + throw new Error( + 'jsdoc-boilerplate-audit: missing required argument (e.g. packages/architect-core/src).' + ); + } + return resolve(process.cwd(), srcRootArg); +} + +async function collectSourceFiles(rootDirectory) { + const entries = await readdir(rootDirectory, { withFileTypes: true }); + const sourceFiles = []; + + for (const entry of entries) { + const entryPath = resolve(rootDirectory, entry.name); + if (entry.isDirectory()) { + sourceFiles.push(...(await collectSourceFiles(entryPath))); + continue; + } + + if (entry.name.endsWith('.ts')) { + sourceFiles.push(entryPath); + } + } + + return sourceFiles.sort(); +} + +export async function auditJsdocBoilerplate(srcRoot) { + const sourceFiles = await collectSourceFiles(srcRoot); + const flaggedFiles = []; + + for (const filePath of sourceFiles) { + const sourceText = await readFile(filePath, 'utf8'); + const matchedPhrases = boilerplatePhrases.filter((phrase) => sourceText.includes(phrase)); + if (matchedPhrases.length > 0) { + flaggedFiles.push({ filePath, matchedPhrases }); + } + } + + return { + srcRoot, + sourceFileCount: sourceFiles.length, + flaggedFiles, + }; +} + +function formatFailure(summary) { + return [ + 'JSDoc boilerplate audit failed.', + `- src root: ${summary.srcRoot}`, + `- scanned source files: ${summary.sourceFileCount}`, + `- flagged files: ${summary.flaggedFiles.map((entry) => entry.filePath).join(', ') || '(none)'}`, + ...summary.flaggedFiles.flatMap((entry) => + entry.matchedPhrases.map((phrase) => `- ${entry.filePath}: ${phrase}`) + ), + ].join('\n'); +} + +async function main() { + const summary = await auditJsdocBoilerplate(resolveSrcRoot()); + + if (summary.flaggedFiles.length > 0) { + throw new Error(formatFailure(summary)); + } + + process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; + }); +} diff --git a/scripts/lint-steps.ts b/scripts/lint-steps.ts index fb60782..5036502 100644 --- a/scripts/lint-steps.ts +++ b/scripts/lint-steps.ts @@ -20,7 +20,7 @@ async function collectFeatureFiles(dir: string): Promise { async function main(): Promise { const baseDir = process.cwd(); - const roots = ['tests/features', 'architect/specs', 'architect/decisions', 'architect/releases']; + const roots = ['tests/features', 'architect/specs', 'architect/decisions']; let parsed = 0; for (const root of roots) { diff --git a/scripts/validate-workspace.ts b/scripts/validate-workspace.ts index 4de0ca8..1b386d8 100644 --- a/scripts/validate-workspace.ts +++ b/scripts/validate-workspace.ts @@ -48,7 +48,7 @@ async function main(): Promise { const warnings = result.value.warnings.length; const diagnostics = result.value.diagnostics.length; process.stdout.write( - `workspace validate ok: ${result.value.graph.patterns.length} patterns, ${warnings} warnings, ${diagnostics} diagnostics\n` + `workspace validate ok: ${result.value.graph.patterns.length} patterns, ${warnings} warnings, ${diagnostics} diagnostics\n`, ); } diff --git a/scripts/workspace-smoke.ts b/scripts/workspace-smoke.ts index b9a26ff..746b3f0 100644 --- a/scripts/workspace-smoke.ts +++ b/scripts/workspace-smoke.ts @@ -32,7 +32,7 @@ async function main(): Promise { } process.stdout.write( - `workspace smoke ok: ${result.value.graph.patterns.length} patterns, ${result.value.diagnostics.length} diagnostics\n` + `workspace smoke ok: ${result.value.graph.patterns.length} patterns, ${result.value.diagnostics.length} diagnostics\n`, ); } diff --git a/scripts/workspace-subtractive-audit.mjs b/scripts/workspace-subtractive-audit.mjs new file mode 100644 index 0000000..2c01423 --- /dev/null +++ b/scripts/workspace-subtractive-audit.mjs @@ -0,0 +1,491 @@ +#!/usr/bin/env node + +import { readFile, readdir } from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import ts from 'typescript'; + +const ROOT = process.cwd(); +const WORKSPACE_CODE_DIRS = ['packages', 'scripts', 'tests']; +const TARGET_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.mjs', '.cjs']); +const DELETION_MARKER_PATTERN = new RegExp( + ['deletion target', ['kept for', 'compat'].join(' '), 'TODO remove', '// removed', 'legacy'].join( + '|', + ), + 'iu', +); +const PROPERTY_NAME_EVASION_PATTERN = /['"][A-Za-z0-9_$-]+['"]\s*\+\s*['"][A-Za-z0-9_$-]+['"]/u; + +async function main() { + const packageDirs = await listPackageDirs(); + const publicEntryFiles = await collectPublicEntryFiles(packageDirs); + const workspaceFiles = await collectWorkspaceFiles(WORKSPACE_CODE_DIRS); + const fileContents = await loadFileContents(workspaceFiles); + + const summary = { + generatedAt: new Date().toISOString(), + root: ROOT, + ruleFamilies: { + zeroConsumerPublicExports: await auditZeroConsumerPublicExports( + publicEntryFiles, + workspaceFiles, + fileContents, + ), + pureConstAliases: auditPureConstAliases(workspaceFiles, fileContents), + pureTypeAliases: auditPureTypeAliases(workspaceFiles, fileContents), + runtimePropertyNameEvasionStrips: auditRuntimePropertyNameEvasionStrips( + workspaceFiles, + fileContents, + ), + staleDeletionTargetMarkers: auditStaleDeletionTargetMarkers(workspaceFiles, fileContents), + dogfoodFilesReachableFromPublicExports: await auditDogfoodReachability(publicEntryFiles), + handwrittenInterfacesShadowingZodInfer: auditInterfaceShadows(workspaceFiles, fileContents), + }, + }; + + process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`); + + if (process.argv.includes('--strict')) { + const totalFindings = Object.values(summary.ruleFamilies).reduce( + (count, family) => count + family.count, + 0, + ); + if (totalFindings > 0) { + process.exitCode = 1; + } + } +} + +async function listPackageDirs() { + const packagesRoot = path.join(ROOT, 'packages'); + const entries = await readdir(packagesRoot, { withFileTypes: true }); + return entries.filter((entry) => entry.isDirectory()).map((entry) => path.join(packagesRoot, entry.name)); +} + +async function collectPublicEntryFiles(packageDirs) { + const publicEntries = []; + + for (const packageDir of packageDirs) { + const packageJsonPath = path.join(packageDir, 'package.json'); + let packageJson; + try { + packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); + } catch { + continue; + } + + if (packageJson.exports === undefined || packageJson.name === '@libar-dev/architect') { + continue; + } + + const packageName = packageJson.name; + for (const [subpath, exportEntry] of Object.entries(packageJson.exports)) { + const resolvedPath = resolvePackageExportToSource(packageDir, exportEntry); + if (resolvedPath === null) { + continue; + } + + publicEntries.push({ + packageDir, + packageName, + subpath, + sourceFile: resolvedPath, + }); + } + } + + return publicEntries; +} + +function resolvePackageExportToSource(packageDir, exportEntry) { + const exportPath = + typeof exportEntry === 'string' + ? exportEntry + : exportEntry !== null && typeof exportEntry === 'object' + ? exportEntry.import ?? exportEntry.types + : null; + + if (typeof exportPath !== 'string' || exportPath.startsWith('./bin/') || exportPath === './package.json') { + return null; + } + + if (exportPath.startsWith('./dist/')) { + return path.join(packageDir, exportPath.replace('./dist/', 'src/').replace(/\.d\.ts$/u, '.ts').replace(/\.js$/u, '.ts')); + } + + return path.join(packageDir, exportPath.replace(/^\.\//u, '').replace(/\.d\.ts$/u, '.ts').replace(/\.js$/u, '.ts')); +} + +async function collectWorkspaceFiles(roots) { + const files = []; + + for (const relativeRoot of roots) { + files.push(...(await walk(path.join(ROOT, relativeRoot)))); + } + + return files.sort(); +} + +async function walk(directory) { + let entries; + try { + entries = await readdir(directory, { withFileTypes: true }); + } catch { + return []; + } + + const files = []; + for (const entry of entries) { + if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name.startsWith('.')) { + continue; + } + const absolutePath = path.join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...(await walk(absolutePath))); + continue; + } + if (entry.isFile() && TARGET_EXTENSIONS.has(path.extname(entry.name))) { + files.push(absolutePath); + } + } + + return files; +} + +async function loadFileContents(files) { + const entries = await Promise.all( + files.map(async (filePath) => [filePath, await readFile(filePath, 'utf8')]), + ); + return new Map(entries); +} + +async function auditZeroConsumerPublicExports(publicEntryFiles, workspaceFiles, fileContents) { + const entryFilesByPackage = new Map(); + for (const entry of publicEntryFiles) { + const packageEntries = entryFilesByPackage.get(entry.packageName) ?? new Set(); + packageEntries.add(entry.sourceFile); + entryFilesByPackage.set(entry.packageName, packageEntries); + } + + const findings = []; + for (const entry of publicEntryFiles) { + const sourceText = fileContents.get(entry.sourceFile); + if (sourceText === undefined) { + findings.push({ + packageName: entry.packageName, + subpath: entry.subpath, + symbol: '(missing-source-file)', + sourceFile: relative(entry.sourceFile), + consumerCount: 0, + }); + continue; + } + + const exportedSymbols = collectExplicitlyExportedSymbols(entry.sourceFile, sourceText); + const excludedFiles = entryFilesByPackage.get(entry.packageName) ?? new Set(); + + for (const symbol of exportedSymbols) { + const consumerCount = countWorkspaceSymbolConsumers(symbol, workspaceFiles, fileContents, excludedFiles); + if (consumerCount === 0) { + findings.push({ + packageName: entry.packageName, + subpath: entry.subpath, + symbol, + sourceFile: relative(entry.sourceFile), + consumerCount, + }); + } + } + } + + return makeRuleFamily('Zero-consumer public exports', findings); +} + +function collectExplicitlyExportedSymbols(filePath, sourceText) { + const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); + const names = new Set(); + + for (const statement of sourceFile.statements) { + if (!hasExportModifier(statement)) { + continue; + } + + if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement)) { + if (statement.name !== undefined) { + names.add(statement.name.text); + } + continue; + } + + if (ts.isVariableStatement(statement)) { + for (const declaration of statement.declarationList.declarations) { + if (ts.isIdentifier(declaration.name)) { + names.add(declaration.name.text); + } + } + continue; + } + + if (ts.isExportDeclaration(statement) && statement.exportClause !== undefined && ts.isNamedExports(statement.exportClause)) { + for (const element of statement.exportClause.elements) { + names.add(element.name.text); + } + } + } + + return [...names].sort(); +} + +function countWorkspaceSymbolConsumers(symbol, workspaceFiles, fileContents, excludedFiles) { + const pattern = new RegExp(`\\b${escapeRegExp(symbol)}\\b`, 'u'); + let count = 0; + + for (const filePath of workspaceFiles) { + if (excludedFiles.has(filePath)) { + continue; + } + const sourceText = fileContents.get(filePath); + if (sourceText !== undefined && pattern.test(sourceText)) { + count += 1; + } + } + + return count; +} + +function auditPureConstAliases(workspaceFiles, fileContents) { + const findings = []; + for (const filePath of workspaceFiles) { + const sourceText = fileContents.get(filePath); + if (sourceText === undefined) { + continue; + } + const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); + for (const statement of sourceFile.statements) { + if (!ts.isVariableStatement(statement) || !hasExportModifier(statement)) { + continue; + } + for (const declaration of statement.declarationList.declarations) { + if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined) { + continue; + } + if (ts.isIdentifier(declaration.initializer)) { + findings.push({ + file: relative(filePath), + name: declaration.name.text, + target: declaration.initializer.text, + }); + } + } + } + } + + return makeRuleFamily('Pure export const aliases', findings); +} + +function auditPureTypeAliases(workspaceFiles, fileContents) { + const findings = []; + for (const filePath of workspaceFiles) { + const sourceText = fileContents.get(filePath); + if (sourceText === undefined) { + continue; + } + const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); + for (const statement of sourceFile.statements) { + if (!ts.isTypeAliasDeclaration(statement) || !hasExportModifier(statement)) { + continue; + } + if (ts.isTypeReferenceNode(statement.type) && ts.isIdentifier(statement.type.typeName)) { + findings.push({ + file: relative(filePath), + name: statement.name.text, + target: statement.type.typeName.text, + }); + } + } + } + + return makeRuleFamily('Pure export type aliases', findings); +} + +function auditRuntimePropertyNameEvasionStrips(workspaceFiles, fileContents) { + const findings = []; + for (const filePath of workspaceFiles) { + const sourceText = fileContents.get(filePath); + if (sourceText === undefined || !PROPERTY_NAME_EVASION_PATTERN.test(sourceText)) { + continue; + } + for (const [index, line] of sourceText.split(/\r?\n/u).entries()) { + if (PROPERTY_NAME_EVASION_PATTERN.test(line)) { + findings.push({ file: relative(filePath), line: index + 1, snippet: line.trim() }); + } + } + } + + return makeRuleFamily('Runtime property-name evasion strips', findings); +} + +function auditStaleDeletionTargetMarkers(workspaceFiles, fileContents) { + const findings = []; + for (const filePath of workspaceFiles) { + const sourceText = fileContents.get(filePath); + if (sourceText === undefined || !DELETION_MARKER_PATTERN.test(sourceText)) { + continue; + } + for (const [index, line] of sourceText.split(/\r?\n/u).entries()) { + if (DELETION_MARKER_PATTERN.test(line)) { + findings.push({ file: relative(filePath), line: index + 1, snippet: line.trim() }); + } + } + } + + return makeRuleFamily('Stale deletion-target markers', findings); +} + +async function auditDogfoodReachability(publicEntryFiles) { + const findings = []; + for (const entry of publicEntryFiles) { + const visited = new Set(); + const stack = [{ filePath: entry.sourceFile, chain: [relative(entry.sourceFile)] }]; + + while (stack.length > 0) { + const current = stack.pop(); + if (current === undefined || visited.has(current.filePath)) { + continue; + } + visited.add(current.filePath); + + const sourceText = await safeRead(current.filePath); + if (sourceText === null) { + continue; + } + + if (isDogfoodFile(current.filePath, sourceText) && current.filePath !== entry.sourceFile) { + findings.push({ + packageName: entry.packageName, + subpath: entry.subpath, + file: relative(current.filePath), + via: current.chain, + }); + } + + for (const importPath of collectRelativeImports(current.filePath, sourceText)) { + stack.push({ + filePath: importPath, + chain: [...current.chain, relative(importPath)], + }); + } + } + } + + return makeRuleFamily('Dogfood files reachable from public exports', findings); +} + +function isDogfoodFile(filePath, sourceText) { + return ( + /self-hosting|tier-[a-z]-baseline/iu.test(filePath) || + sourceText.includes('@architect-bounded-context:dogfood') + ); +} + +function collectRelativeImports(filePath, sourceText) { + const importPattern = /from\s+['"](\.\.?\/[^'"]+)['"]/gu; + const imports = new Set(); + + for (const match of sourceText.matchAll(importPattern)) { + const resolved = resolveRelativeTsImport(path.dirname(filePath), match[1]); + if (resolved !== null) { + imports.add(resolved); + } + } + + return [...imports]; +} + +function resolveRelativeTsImport(directory, specifier) { + const base = path.resolve(directory, specifier); + const candidates = [ + `${base}.ts`, + `${base}.tsx`, + `${base}.js`, + `${base}.mjs`, + path.join(base, 'index.ts'), + path.join(base, 'index.tsx'), + path.join(base, 'index.js'), + ]; + + return candidates.find((candidate) => ts.sys.fileExists(candidate)) ?? null; +} + +function auditInterfaceShadows(workspaceFiles, fileContents) { + const findings = []; + + for (const filePath of workspaceFiles) { + const sourceText = fileContents.get(filePath); + if (sourceText === undefined) { + continue; + } + + const inferredNames = new Set(); + const interfaceNames = new Set(); + const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); + + for (const statement of sourceFile.statements) { + if (ts.isTypeAliasDeclaration(statement) && ts.isTypeReferenceNode(statement.type)) { + if ( + ts.isQualifiedName(statement.type.typeName) && + ts.isIdentifier(statement.type.typeName.left) && + statement.type.typeName.left.text === 'z' && + statement.type.typeName.right.text === 'infer' + ) { + inferredNames.add(statement.name.text); + } + } + + if (ts.isInterfaceDeclaration(statement)) { + interfaceNames.add(statement.name.text); + } + } + + for (const name of inferredNames) { + if (interfaceNames.has(name)) { + findings.push({ file: relative(filePath), name }); + } + } + } + + return makeRuleFamily('Handwritten interfaces shadowing z.infer contracts', findings); +} + +function makeRuleFamily(description, findings) { + return { + description, + count: findings.length, + findings, + }; +} + +function hasExportModifier(node) { + return ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false; +} + +function relative(filePath) { + return path.relative(ROOT, filePath).replace(/\\/gu, '/'); +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); +} + +async function safeRead(filePath) { + try { + return await readFile(filePath, 'utf8'); + } catch { + return null; + } +} + +void main().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exit(1); +}); diff --git a/tests/features/api/canonical-values-sync.feature b/tests/features/api/canonical-values-sync.feature index 28577b3..d3966cc 100644 --- a/tests/features/api/canonical-values-sync.feature +++ b/tests/features/api/canonical-values-sync.feature @@ -122,55 +122,10 @@ Feature: Canonical values stay in sync between ADR-001 and TypeScript constants And I list the values in CANONICAL_FEATURE_ONLY_TAG_SUFFIXES Then both canonical feature-only tag lists contain the same values - Rule: ADR-001 Rule 7 quarter format regex matches QUARTER_PATTERN - - **Invariant:** The quarter format declared in ADR-001 Rule 7 - (`YYYY-QN`, e.g. `2026-Q1`) is the format that the `QUARTER_PATTERN` - regex exported from `@libar-dev/architect-core` accepts. - **Rationale:** Rule 7 has no values table — the rule is the regex - contract itself. The sync test asserts the canonical example accepts - and the previous (anti-pattern) format rejects, proving the regex - encodes Rule 7's contract. - **Verified by:** QUARTER_PATTERN encodes ADR-001 Rule 7's format - - @acceptance-criteria @happy-path - Scenario: QUARTER_PATTERN encodes ADR-001 Rule 7's format - Given the QUARTER_PATTERN regex - Then it accepts the canonical example "2026-Q1" - And it rejects the anti-pattern "Q1-2026" - - Rule: ADR-001 Rule 8 phase names match CANONICAL_PHASE_NAMES - - **Invariant:** The 6 phase names in ADR-001 Rule 8 list the same - names as `CANONICAL_PHASE_NAMES` exported from `@libar-dev/architect-core`. - **Rationale:** Workflow config consumers and roadmap generation read - phase names from the canonical list. Renaming a phase in the ADR - without updating the constant breaks roadmap rendering. - **Verified by:** Phase names match between ADR-001 Rule 8 and CANONICAL_PHASE_NAMES - - @acceptance-criteria @happy-path - Scenario: Phase names match between ADR-001 Rule 8 and CANONICAL_PHASE_NAMES - Given the ADR-001 canonical values feature file - When I extract the phase names from Rule 8 - And I list the names in CANONICAL_PHASE_NAMES - Then both phase-name lists contain the same names - - Rule: ADR-001 Rule 8 phase ordinals match CANONICAL_PHASE_ORDINALS - - **Invariant:** The 6 phase ordinals in ADR-001 Rule 8 list the same - integers as `CANONICAL_PHASE_ORDINALS` exported from - `@libar-dev/architect-core`. - **Rationale:** Ordinals drive sort order in roadmap rendering; an - ordinal shift in the ADR without updating the constant produces - silently misordered output. - **Verified by:** Phase ordinals match between ADR-001 Rule 8 and CANONICAL_PHASE_ORDINALS - - @acceptance-criteria @happy-path - Scenario: Phase ordinals match between ADR-001 Rule 8 and CANONICAL_PHASE_ORDINALS - Given the ADR-001 canonical values feature file - When I extract the phase ordinals from Rule 8 - And I list the ordinals in CANONICAL_PHASE_ORDINALS - Then both phase-ordinal lists contain the same ordinals + # ADR-001 Rule 7 (Quarter Format Convention) and Rule 8 (6-phase USDP + # Canonical Phase Definitions) were retired per ADR-013, along with their + # `QUARTER_PATTERN` / `CANONICAL_PHASE_NAMES` / `CANONICAL_PHASE_ORDINALS` + # constants — so their sync rules are removed here. Rule: ADR-001 Rule 9 matches DELIVERABLE_STATUS_VALUES diff --git a/tests/features/api/cli-mcp-documentation-parity.feature b/tests/features/api/cli-mcp-documentation-parity.feature deleted file mode 100644 index 79f7840..0000000 --- a/tests/features/api/cli-mcp-documentation-parity.feature +++ /dev/null @@ -1,34 +0,0 @@ -@architect -@architect-pattern:DocumentationCommandParityBoundaryTests -@architect-status:active -@architect-product-area:DataAPI -@api @cli @mcp @contracts -Feature: CLI and MCP documentation parity - Verify the CLI documentation command and MCP documentation tool produce the same bundle output for the same inputs. - - Background: - Given the package-hosted documentation parity fixture is initialized - - Rule: CLI and MCP documentation boundaries serialize the same projection bundle - - **Invariant:** The CLI `documentation` command and the MCP `architect_documentation` tool serialize the same projection bundle for the same document type and disclosure/filter inputs. - **Rationale:** Documentation consumers should see the same bundle semantics regardless of whether they enter through the CLI subprocess boundary or the registered MCP tool boundary. - **Verified by:** CLI and MCP produce identical JSON for a bundle, CLI and MCP produce identical JSON for filtered and disclosed business rules - - @happy-path - Scenario Outline: CLI and MCP produce identical JSON for a bundle - When I generate "" via the CLI documentation command as JSON - And I generate "" via the MCP architect_documentation tool - Then the two outputs deep-equal - - Examples: - | docType | - | business-rules | - | requirements-executable | - | decisions | - - @happy-path - Scenario: CLI and MCP produce identical JSON for filtered and disclosed business rules - When I generate "business-rules" via the CLI documentation command as JSON with disclosure "useful" and filter "status=completed" - And I generate "business-rules" via the MCP architect_documentation tool with disclosure "useful" and completed-status filter - Then the two outputs deep-equal diff --git a/tests/features/api/context-assembly/compact-text-renderer.feature b/tests/features/api/context-assembly/compact-text-renderer.feature index c265e0e..c42c1f0 100644 --- a/tests/features/api/context-assembly/compact-text-renderer.feature +++ b/tests/features/api/context-assembly/compact-text-renderer.feature @@ -1,6 +1,7 @@ @architect @architect-pattern:CompactTextRendererTests @architect-status:active +@architect-implements:CompactTextRenderer @architect-product-area:DataAPI Feature: Compact Text Renderer - Plain Text Rendering @@ -34,26 +35,28 @@ Feature: Compact Text Renderer - Plain Text Rendering | === FSM === | And the output contains checkbox markers - Rule: formatDepTree renders indented tree + Rule: formatDependencyContext renders a bidirectional focal view - **Invariant:** The dependency tree compact renderer must render with indentation arrows and a focal pattern marker to visually distinguish the target pattern from its dependencies. - **Rationale:** Visual hierarchy in the dependency tree makes dependency chains scannable at a glance — flat output would require mental parsing to understand depth and relationships. - **Verified by:** Tree renders with arrows and focal marker + **Invariant:** The dependency-context compact renderer must lead with a one-line focal summary, then render an upstream "DEPENDS ON" tree and a downstream "REQUIRED BY" tree, using `-> ` indentation arrows for transitive nodes so the chain depth stays scannable. + **Rationale:** A bidirectional view answers both "what does the focal pattern depend on?" and "what depends on the focal pattern?" in one render — a one-directional tree forces two separate queries, and arrows make transitive depth legible at a glance. + **Verified by:** Context renders the focal summary and bidirectional trees @acceptance-criteria @happy-path - Scenario: Tree renders with arrows and focal marker - Given a dep-tree with root, middle, and focal leaf - When I format the tree + Scenario: Context renders the focal summary and bidirectional trees + Given a dependency context with root, middle, and focal leaf + When I format the dependency context Then the output contains all expected sections - | section | - | -> | - | <- YOU ARE HERE | + | section | + | Leaf depends on 1 | + | === DEPENDS ON (upstream) === | + | === REQUIRED BY (downstream) === | + | -> | Rule: formatOverview renders progress summary - **Invariant:** The overview compact renderer must render a progress summary line showing completion metrics for the project and point users to the current query script name. + **Invariant:** The overview compact renderer must render a progress summary line showing completion metrics for the project and point users to the graph-handle read surface (`pnpm architect:q`). **Rationale:** The progress line is the first thing developers see when starting a session — it provides immediate project health awareness, and the follow-up command guidance must be copy-pasteable. - **Verified by:** Overview renders progress line, Overview renders architect query guidance + **Verified by:** Overview renders progress line, Overview renders read-surface guidance @acceptance-criteria @happy-path Scenario: Overview renders progress line @@ -66,11 +69,11 @@ Feature: Compact Text Renderer - Plain Text Rendering | === PROGRESS ===| @acceptance-criteria @happy-path - Scenario: Overview renders architect query guidance + Scenario: Overview renders read-surface guidance Given an overview with 69 total patterns at 52 percent When I format the overview - Then the output contains "pnpm architect:query -- " - And the output contains "Full reference: pnpm architect:query -- --help" + Then the output contains "pnpm architect:q ''" + And the output contains "Load the `architect-graph-handle` skill" Rule: formatFileReadingList renders categorized file paths diff --git a/tests/features/api/output-shaping/output-pipeline.feature b/tests/features/api/output-shaping/output-pipeline.feature index e81911d..cf35591 100644 --- a/tests/features/api/output-shaping/output-pipeline.feature +++ b/tests/features/api/output-shaping/output-pipeline.feature @@ -3,7 +3,6 @@ @architect-status:completed @architect-implements:DataAPIOutputShaping @architect-unlock-reason:Value-transfer-from-spec -@architect-phase:25a @architect-product-area:DataAPI Feature: Output Modifier Pipeline diff --git a/tests/features/api/stub-integration/taxonomy-tags.feature b/tests/features/api/stub-integration/taxonomy-tags.feature index 911f509..abd07c6 100644 --- a/tests/features/api/stub-integration/taxonomy-tags.feature +++ b/tests/features/api/stub-integration/taxonomy-tags.feature @@ -1,6 +1,7 @@ @architect @architect-pattern:StubTaxonomyTagTests @architect-status:active +@architect-implements:RegistryBuilder @architect-product-area:DataAPI Feature: Stub Integration Taxonomy Tags diff --git a/tests/features/cli/broken-spec-pattern.fixture.feature b/tests/features/cli/broken-spec-pattern.fixture.feature deleted file mode 100644 index 3e4644c..0000000 --- a/tests/features/cli/broken-spec-pattern.fixture.feature +++ /dev/null @@ -1,11 +0,0 @@ -@architect -@architect-pattern:BrokenSpecPattern -@architect-status:completed -Feature: Broken Spec Pattern - - Rule: Parse attribution - - Scenario: Unterminated docstring - Given a broken feature source - """ - missing closing docstring diff --git a/tests/features/cli/data-api-cache.feature b/tests/features/cli/data-api-cache.feature deleted file mode 100644 index 3bc027d..0000000 --- a/tests/features/cli/data-api-cache.feature +++ /dev/null @@ -1,43 +0,0 @@ -@architect -@architect-pattern:PatternGraphCliCache -@architect-implements:DataAPICLIErgonomics -@architect-status:active -@architect-product-area:DataAPI -@cli @pattern-graph-cli @cache -Feature: Pattern Graph CLI - Dataset Cache - PatternGraph caching between CLI invocations: cache hits, mtime invalidation, and --no-cache bypass. - - Background: - Given a temporary working directory - - # ============================================================================ - # RULE 1: Cache Hit on Unchanged Sources - # ============================================================================ - - Rule: PatternGraph is cached between invocations - - **Invariant:** When source files have not changed between CLI invocations, the second invocation must use the cached PatternGraph and report cache.hit as true alongside pipeline timing metadata. - **Rationale:** The pipeline rebuild costs 2-5 seconds per invocation. Caching eliminates this cost for repeated queries against unchanged sources, which is the common case during interactive AI sessions. - - @happy-path - Scenario: Second query uses cached dataset - Given TypeScript files with pattern annotations - When running status and capturing the first result - And running status and capturing the second result - Then the second result metadata has cache.hit true - And both results report pipeline timing metadata - - @happy-path - Scenario: Cache invalidated on source file change - Given TypeScript files with pattern annotations - When running status and capturing the first result - And a source file mtime is updated - And running status and capturing the second result - Then the second result metadata has cache.hit false - - @happy-path - Scenario: No-cache flag bypasses cache - Given TypeScript files with pattern annotations - When running status and capturing the first result - And running status with --no-cache and capturing the second result - Then the second result metadata has cache.hit false diff --git a/tests/features/cli/data-api-dryrun.feature b/tests/features/cli/data-api-dryrun.feature deleted file mode 100644 index ce00547..0000000 --- a/tests/features/cli/data-api-dryrun.feature +++ /dev/null @@ -1,38 +0,0 @@ -@architect -@architect-pattern:PatternGraphCliDryRun -@architect-implements:DataAPICLIErgonomics -@architect-status:active -@architect-product-area:DataAPI -@cli @pattern-graph-cli @dry-run -Feature: Pattern Graph CLI - Dry Run - Dry-run mode shows pipeline scope without processing data. - - Background: - Given a temporary working directory - - # ============================================================================ - # RULE 1: Dry-Run Pipeline Scope - # ============================================================================ - - Rule: Dry-run shows pipeline scope without processing - - **Invariant:** The --dry-run flag must display file counts, config status, and cache status without executing the pipeline. Output must contain the DRY RUN marker and must not contain a JSON success envelope. - **Rationale:** Dry-run enables users to verify their input patterns resolve to expected files before committing to the 2-5s pipeline cost, which is especially valuable when debugging glob patterns or config auto-detection. - **Verified by:** Dry-run shows file counts, Dry-run reports architect.config.js auto-detection - - @happy-path - Scenario: Dry-run shows file counts - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' --dry-run status" - Then exit code is 0 - And stdout contains dry run marker, file counts, config, and cache status - And stdout does not contain "success" - - @happy-path - Scenario: Dry-run reports architect.config.js auto-detection - Given TypeScript files with pattern annotations - And an architect.config.js with TypeScript sources - When running "pattern-graph-cli --dry-run status" - Then exit code is 0 - And stdout contains "architect.config.js (auto-detected)" - And stdout does not contain "success" diff --git a/tests/features/cli/data-api-help.feature b/tests/features/cli/data-api-help.feature deleted file mode 100644 index 49d1e44..0000000 --- a/tests/features/cli/data-api-help.feature +++ /dev/null @@ -1,76 +0,0 @@ -@architect -@architect-pattern:DataAPICLIErgonomics -@architect-status:completed -@architect-unlock-reason:Value-transfer-from-spec -@architect-phase:25d -@architect-product-area:DataAPI -@cli @pattern-graph-cli @help -Feature: Data API CLI Ergonomics - Performance and Interactive Mode - **Problem:** - The pattern-graph-cli CLI runs the full pipeline (scan, extract, transform) on every - invocation, taking 2-5 seconds. During design sessions with 10-20 queries, this - adds up to 1-2 minutes of waiting. There is no way to keep the pipeline loaded - between queries. Per-subcommand help is missing -- `pattern-graph-cli context --help` - does not work. FSM-only queries (like `isValidTransition`) run the full pipeline - even though FSM rules are static. - - **Solution:** - Add performance and ergonomic improvements: - 1. Pipeline caching -- Cache PatternGraph to temp file with mtime invalidation - 2. REPL mode -- `pattern-graph-cli repl` keeps pipeline loaded for interactive queries - 3. FSM short-circuit -- FSM queries skip the scan pipeline entirely - 4. Per-subcommand help -- `pattern-graph-cli --help` with examples - 5. Dry-run mode -- `--dry-run` shows what would be scanned without running - 6. Validation summary -- Include pipeline health in response metadata - - Per-subcommand help displays usage, flags, and examples for individual subcommands. - - Background: - Given a temporary working directory - | Deliverable | Status | Tests | Location | - | Per-subcommand help contract | complete | Yes | packages/architect-cli/src/cli/pattern-graph-cli.ts | - | Public command and flag inventory | complete | Yes | packages/architect/tests/features/cli/data-api-help.feature | - | Structured JSON format compatibility | complete | Yes | packages/architect/tests/steps/cli/data-api-help.steps.ts | - - # ============================================================================ - # RULE 1: Per-Subcommand Help - # ============================================================================ - - Rule: Per-subcommand help shows usage and flags - - **Invariant:** Running any subcommand with --help must display usage information specific to that subcommand, including applicable flags and examples. Unknown subcommands must fall back to a descriptive message. - **Rationale:** Per-subcommand help replaces the need to scroll through full --help output and provides contextual guidance for subcommand-specific flags like --session. - - @acceptance-criteria @happy-path - Scenario: Per-subcommand help for context - When running "pattern-graph-cli context --help" - Then exit code is 0 - And stdout contains context usage and session flag - And stdout contains "Usage:" - - @happy-path - Scenario: Global help still works - When running "pattern-graph-cli --help" - Then exit code is 0 - And stdout contains "Usage:" - - @acceptance-criteria @contract - Scenario: Global help lists the frozen public command and flag inventory - When running "pattern-graph-cli --help" - Then exit code is 0 - And global help lists the frozen command inventory - And global help lists the frozen notable flags - - @validation - Scenario: Unknown subcommand help - When running "pattern-graph-cli foobar --help" - Then exit code is 0 - And stdout contains "No detailed help" - - @acceptance-criteria @contract - Scenario: Structured subcommands accept the public --format json flag - Given TypeScript files with pattern annotations - And TypeScript files with architecture annotations and dependencies - When running the frozen "--format json" contract command set - Then every frozen "--format json" command exits with code 0 - And every frozen "--format json" command returns structured JSON diff --git a/tests/features/cli/data-api-metadata.feature b/tests/features/cli/data-api-metadata.feature deleted file mode 100644 index 8d05af3..0000000 --- a/tests/features/cli/data-api-metadata.feature +++ /dev/null @@ -1,46 +0,0 @@ -@architect -@architect-pattern:PatternGraphCliMetadata -@architect-implements:DataAPICLIErgonomics -@architect-status:active -@architect-product-area:DataAPI -@cli @pattern-graph-cli @metadata -Feature: Pattern Graph CLI - Response Metadata - Response metadata includes validation summary and pipeline timing for diagnostics. - - Background: - Given a temporary working directory - - # ============================================================================ - # RULE 1: Validation Summary in Metadata - # ============================================================================ - - Rule: Response metadata includes validation summary - - **Invariant:** Every JSON response envelope must include a metadata.validation object with danglingReferenceCount, malformedPatternCount, unknownStatusCount, and warningCount fields, plus a numeric pipelineMs timing. - **Rationale:** Consumers use validation counts to detect annotation quality degradation without running a separate validation pass. Pipeline timing enables performance regression detection in CI. - - @acceptance-criteria @happy-path - Scenario: Validation summary in response metadata - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' query getStatusCounts" - Then exit code is 0 - And stdout is valid JSON with key "metadata" - And metadata has a validation object with count fields - And metadata has a numeric pipelineMs field - - @happy-path - Scenario: Pipeline timing in metadata - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' query getStatusCounts" - Then exit code is 0 - And metadata has a numeric pipelineMs field - - @contract - Scenario: QuerySuccess envelope preserves the structured JSON contract - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' query getStatusCounts" - Then exit code is 0 - And stdout is valid JSON with key "data" - And response has success true - And metadata has an ISO timestamp field - And metadata has a numeric patternCount field diff --git a/tests/features/cli/data-api-repl.feature b/tests/features/cli/data-api-repl.feature deleted file mode 100644 index 60025e8..0000000 --- a/tests/features/cli/data-api-repl.feature +++ /dev/null @@ -1,53 +0,0 @@ -@architect -@architect-pattern:PatternGraphCliRepl -@architect-implements:DataAPICLIErgonomics -@architect-status:active -@architect-product-area:DataAPI -@cli @pattern-graph-cli @repl -Feature: Pattern Graph CLI - REPL Mode - Interactive REPL mode keeps the pipeline loaded for multi-query sessions and supports reload. - - Background: - Given a temporary working directory - - # ============================================================================ - # RULE 1: Multi-Query Sessions - # ============================================================================ - - Rule: REPL mode accepts multiple queries on a single pipeline load - - **Invariant:** REPL mode loads the pipeline once and accepts multiple queries on stdin, eliminating per-query pipeline overhead. - **Rationale:** Design sessions involve 10-20 exploratory queries in sequence. REPL mode eliminates per-query pipeline overhead entirely. - **Verified by:** REPL accepts multiple queries, REPL shows help output - - @acceptance-criteria @happy-path - Scenario: REPL accepts multiple queries - Given TypeScript files with pattern annotations - When piping "status" then "list" then "quit" to the REPL - Then the REPL output contains status JSON - And the REPL output contains list JSON - And the REPL exits cleanly - - @acceptance-criteria @happy-path - Scenario: REPL shows help output - Given TypeScript files with pattern annotations - When piping "help" then "quit" to the REPL - Then the REPL output contains available commands - - # ============================================================================ - # RULE 2: Pipeline Reload - # ============================================================================ - - Rule: REPL reload rebuilds the pipeline from fresh sources - - **Invariant:** The reload command rebuilds the pipeline from fresh sources and subsequent queries use the new dataset. - **Rationale:** During implementation sessions, source files change frequently. Reload allows refreshing without restarting the REPL. - **Verified by:** REPL reloads pipeline on command - - @acceptance-criteria @happy-path - Scenario: REPL reloads pipeline on command - Given TypeScript files with pattern annotations - When piping "status" then "reload" then "status" then "quit" to the REPL - Then the REPL stderr contains "Reloading pipeline" - And the REPL stderr contains "Reloaded" - And the REPL output contains two status responses diff --git a/tests/features/cli/generate-docs.feature b/tests/features/cli/generate-docs.feature index abd481f..7316651 100644 --- a/tests/features/cli/generate-docs.feature +++ b/tests/features/cli/generate-docs.feature @@ -3,6 +3,7 @@ @architect-status:completed @architect-unlock-reason:Retroactive-completion-during-rebrand @architect-product-area:DataAPI +@architect-uses:MarkdownRenderer @cli @generate-docs Feature: generate-docs CLI Command-line interface for generating documentation from annotated TypeScript. @@ -81,7 +82,7 @@ Feature: generate-docs CLI **Invariant:** Given valid input patterns and a generator name, the CLI must scan sources, extract patterns, and produce markdown output files. **Rationale:** This is the core pipeline — the CLI is the primary entry point for transforming annotated source code into generated documentation. - **Verified by:** Generate patterns documentation, Generate docs manifest with projection root classification, Use default generator (patterns) when not specified, Generate docs with disclosure override, Generate docs with status filter override, Generate docs with repeated status filters + **Verified by:** Generate patterns documentation, Generate docs manifest with projection root classification, Use default generator (patterns) when not specified, Generate docs with disclosure override, Generate docs with status filter override, Generate docs with repeated status filters, --all runs every registered generator plus index @happy-path Scenario: Generate patterns documentation @@ -134,6 +135,112 @@ Feature: generate-docs CLI And file "docs/PATTERNS.md" contains "CompletedGeneratorPattern" And file "docs/PATTERNS.md" also contains "ActiveGeneratorPattern" + @happy-path + Scenario: --all runs every registered generator plus index + Given an architect.config.js mapping sources to a package + And a TypeScript file "src/pattern.ts" with pattern annotations + When running "generate-docs --all -o docs -f" + Then exit code is 0 + And the working directory contains files: + | path | + | docs/PATTERNS.md | + | docs/API-REFERENCE.md | + | docs/ARCHITECTURE.md | + | docs/INDEX.md | + + # ============================================================================ + # RULE 4b: Determinism check (--check) + # ============================================================================ + + Rule: CLI verifies determinism with --check + + **Invariant:** With --check the CLI re-renders every requested generator and diffs the result against the on-disk files **and the generated-docs manifest**, writing nothing — it exits 0 when they match and non-zero (reporting drift) when an on-disk file or the manifest is absent or stale. + **Rationale:** The git-based determinism gate (`docs:all && git diff --exit-code`) conflates an uncommitted changeset with a non-deterministic generator and is useless on a dirty tree; --check proves idempotency against the working tree independent of git state. It must cover the manifest too, or a manifest-only drift (a changed root classification / file set) would pass --check yet fail the git gate. + **Verified by:** Check passes when generated docs match the working tree, Check reports drift when a generated doc is absent, Check reports drift when only the manifest is stale + + @happy-path + Scenario: Check passes when generated docs match the working tree + Given a TypeScript file "src/pattern.ts" with pattern annotations + When running "generate-docs -i src/pattern.ts -g patterns -o docs -f" + And running "generate-docs -i src/pattern.ts -g patterns -o docs --check" + Then exit code is 0 + And output contains "no drift" + + @validation + Scenario: Check reports drift when a generated doc is absent + Given a TypeScript file "src/pattern.ts" with pattern annotations + When running "generate-docs -i src/pattern.ts -g patterns -o docs --check" + Then exit code is 1 + And output contains "not up to date" + + @validation + Scenario: Check reports drift when only the manifest is stale + Given a TypeScript file "src/pattern.ts" with pattern annotations + When running "generate-docs -i src/pattern.ts -g patterns -o docs -f" + And the generated docs manifest in "docs" is emptied + And running "generate-docs -i src/pattern.ts -g patterns -o docs --check" + Then exit code is 1 + And output contains ".generated-docs-manifest.json" + + # ============================================================================ + # RULE 4c: Embedded-region host generation + gate + # ============================================================================ + + Rule: CLI generates and gates embedded-region hosts + + **Invariant:** An embedded-region generator rewrites only the marker-bounded regions of an authored host `.md` that lives OUTSIDE the output directory, preserving the authored prose. A host that is present but missing its markers fails loud (named host + region, no partial write); a host absent in this project is skipped under `--all` so the run stays portable, but an explicit `-g` request for an absent host fails loud (a named host is requested on purpose, so a silent skip there would let a bad path exit 0 with nothing written); a hand-edited region is caught by `--check` even though the host is out of tree. Authored hosts are written LAST — after every regenerable step and after every routed host has rendered — so a validation failure (missing/malformed markers) aborts the run before any host is committed, leaving every authored host byte-untouched. The commit itself replaces each host by an atomic rename of a fully-staged temp (never a truncating in-place write), so a host is never observed half-written; the batch is staged-then-renamed and idempotent, so an interrupted commit completes on re-run rather than being rolled back (it does NOT guarantee every host stays untouched once renames begin). + **Rationale:** Embedded hosts carry hand-authored, non-regenerable prose, so the engine's promises (fail-loud, never-partial, region-scoped drift, commit-last + atomic-per-host rename) must hold at the CLI boundary — this is what closes the `docs-live`-only coverage hole without ever risking an authored file. + **Verified by:** An embedded host present but missing its markers fails loudly, An embedded host absent in the project is skipped under --all, An explicit -g request for an absent host fails loud, A hand-edited region in an out-of-tree host fails the determinism gate, A validation failure aborts before any embedded host is committed + + @validation + Scenario: An embedded host present but missing its markers fails loudly + Given a TypeScript file "src/pattern.ts" with pattern annotations + And an embedded host "formal-spec/04-tag-registry.md" with no managed-region markers + When running "generate-docs -i src/pattern.ts -g taxonomy-formal-spec -f" + Then exit code is 1 + And output contains all of: + | text | + | taxonomy-classification | + | formal-spec/04-tag-registry.md | + + @boundary + Scenario: An embedded host absent in the project is skipped under --all + Given an architect.config.js mapping sources to a package + And a TypeScript file "src/pattern.ts" with pattern annotations + When running "generate-docs --all -o docs -f" + Then exit code is 0 + And output contains "Skipping embedded generator" + And file "docs/PATTERNS.md" exists in working directory + + @error + Scenario: An explicit -g request for an absent host fails loud + Given a TypeScript file "src/pattern.ts" with pattern annotations + When running "generate-docs -i src/pattern.ts -g taxonomy-formal-spec -f" + Then exit code is 1 + And output contains "not found" + + @validation + Scenario: A hand-edited region in an out-of-tree host fails the determinism gate + Given a TypeScript file "src/pattern.ts" with pattern annotations + And an embedded host "formal-spec/04-tag-registry.md" with an empty "taxonomy-classification" region + And running "generate-docs -i src/pattern.ts -g taxonomy-formal-spec -f" + And the "taxonomy-classification" region in "formal-spec/04-tag-registry.md" is hand-edited + When running "generate-docs -i src/pattern.ts -g taxonomy-formal-spec --check" + Then exit code is 1 + And output contains all of: + | text | + | region drift | + | formal-spec/04-tag-registry.md | + + @boundary + Scenario: A validation failure aborts before any embedded host is committed + Given a TypeScript file "src/pattern.ts" with pattern annotations + And an embedded host "formal-spec/04-tag-registry.md" with an empty "taxonomy-classification" region + And an embedded host ".agents/skills/architect-base/references/taxonomy.md" with no managed-region markers + When running "generate-docs -i src/pattern.ts -g taxonomy-formal-spec -g taxonomy-skill -f" + Then exit code is 1 + And the embedded host "formal-spec/04-tag-registry.md" was left unwritten by the failed run + # ============================================================================ # RULE 5: Unknown Options # ============================================================================ diff --git a/tests/features/cli/graph-handle.feature b/tests/features/cli/graph-handle.feature new file mode 100644 index 0000000..5c3b091 --- /dev/null +++ b/tests/features/cli/graph-handle.feature @@ -0,0 +1,120 @@ +@architect +@architect-pattern:GraphHandleCliExecutableTests +@architect-status:completed +@architect-unlock-reason:Executable-tests-for-the-shipped-graph-handle-born-completed-with-passing-suite +@architect-product-area:DataAPI +@architect-implements:GraphHandleCli +@architect-bounded-context:cli +@cli @graph-handle +Feature: Graph-handle CLI — the agent read surface + + The `architect` bin's q front door evaluates agent-authored JS against the + live graph handle (`g`) and the named commands are runnable documentation + over it. These scenarios assert INVARIANTS that survive annotation growth — + never frozen counts (the graph builds live; exact numbers drift by design). + + Rule: The q front door evaluates agent scripts against the live graph + + **Invariant:** An argv expression, an argv multi-statement body, and a + piped stdin script each evaluate with `g` in scope and print the returned + value; a body using `import` fails loud with a hint naming the injected + globals instead of silently doing nothing. + + **Rationale:** q is the primary agent surface (ADR-014) — the round-trip + forms and the loud failure mode are the contract that makes "script the + rest" dependable enough to replace the verb wall. + + **Verified by:** the four scenarios below. + + @happy-path + Scenario: argv expression round-trips against the live graph + When I run the graph CLI with q expression "g.pattern('GraphHandle')?.name" + Then the exit code is zero + And stdout is "GraphHandle" + + @happy-path + Scenario: the CLI composes the public core Graph + When I load the CLI graph composition + Then the handle is the public core Graph + And the handle has no api field + And the canonical graph and FSM are frozen + And deferred patterns have plan maturity + + @happy-path + Scenario: the migrated handle exposes canonical graph and FSM values + When I run the migrated handle characterization + Then the exit code is zero + And the characterization reports api is absent + And the characterization reports FSM is available + And the characterization reports canonical graph is frozen + And the characterization reports deferred maturity is plan + + @happy-path + Scenario: argv multi-statement body round-trips + When I run the graph CLI with q expression "const p = g.pattern('GraphHandle'); return p?.name" + Then the exit code is zero + And stdout is "GraphHandle" + + @happy-path + Scenario: stdin script round-trips + When I pipe a script returning the GraphHandle sentinel into the graph CLI + Then the exit code is zero + And stdout is "GraphHandle" + + @negative + Scenario: the removed api field fails loud + When I run the graph CLI with q expression "return g.api.getStatusCounts()" + Then the exit code is non-zero + And stderr mentions "getStatusCounts" + + @negative + Scenario: canonical graph mutation cannot corrupt a fresh read + When I attempt canonical graph mutation through q + Then mutation throws or the GraphHandle sentinel remains unchanged + And a fresh q invocation returns the GraphHandle sentinel + + @negative + Scenario: an import in the body fails loud with the injected-globals hint + When I run the graph CLI with q expression "import x from 'y'" + Then the exit code is non-zero + And stderr mentions "injected globals" + + Rule: The decoded graph holds its structural invariants + + **Invariant:** Scoped drift stays at zero dangling `uses` edges; spec + maturity and provenance stay coherent (an executable-provenance spec is + always executable-maturity and vice versa); the entry adapters and the + spec bridge return non-empty results for stable inputs. + + **Rationale:** These are the honesty guarantees the handle's decode adds + over the raw core — if any of them regresses, agents scripting the handle + silently read wrong architecture. + + **Verified by:** one battery script (one graph build, four assertions). + + @happy-path + Scenario: the invariant battery passes against the live graph + When I pipe the invariant battery script into the graph CLI + Then the exit code is zero + And the battery reports zero dangling uses edges + And the battery reports coherent spec maturity and provenance + And the battery reports non-empty entry adapters + And the battery reports a working spec bridge + + Rule: The dangling gate is a deterministic machine contract + + **Invariant:** `architect dangling --baseline --strict` exits + zero when the working tree matches the committed baseline and returns the + exact established JSON document shape. + + **Rationale:** This is the ONE frozen machine contract on the bin (CI is + its second caller, per the second-caller bar); its exit semantics are the + graph-integrity gate `ci:verify` depends on. + + **Verified by:** the scenario below (mirrors the CI invocation). + + @happy-path + Scenario: the strict gate passes against the committed baseline + When I run the graph CLI dangling gate against the committed baseline + Then the exit code is zero + And stdout matches the exact strict dangling JSON shape diff --git a/tests/features/cli/lint-patterns.feature b/tests/features/cli/lint-patterns.feature index 97633b4..44c1a6d 100644 --- a/tests/features/cli/lint-patterns.feature +++ b/tests/features/cli/lint-patterns.feature @@ -2,6 +2,7 @@ @architect-pattern:LintPatternsCliBehavior @architect-status:completed @architect-unlock-reason:Retroactive-completion-during-rebrand +@architect-implements:LintPatternsCLI @architect-product-area:DataAPI @cli @lint-patterns Feature: lint-patterns CLI @@ -38,8 +39,8 @@ Feature: lint-patterns CLI Rule: CLI requires input patterns **Invariant:** The lint-patterns CLI must fail with a clear error when the --input flag is not provided. - **Rationale:** Without input paths, the linter has nothing to validate — failing early prevents confusing "no violations" output that falsely implies clean annotations. - **Verified by:** Fail without --input flag + **Rationale:** Without input paths, the linter has nothing to validate — failing early prevents confusing "no violations" output that falsely implies clean annotations. Invalid argument values must also fail through the canonical CLI error path so users get a human-readable message instead of a raw stack trace. + **Verified by:** Fail without --input flag, Reject invalid output format without stack trace @validation Scenario: Fail without --input flag @@ -47,6 +48,13 @@ Feature: lint-patterns CLI Then exit code is 1 And output contains "No input patterns" + @validation + Scenario: Reject invalid output format without stack trace + When running "lint-patterns --format xml" + Then exit code is 1 + And output contains "Invalid format: xml" + And output does not contain raw stack markers + # ============================================================================ # RULE 3: Lint Passes # ============================================================================ diff --git a/tests/features/cli/lint-process.feature b/tests/features/cli/lint-process.feature index 5ccb270..08623a1 100644 --- a/tests/features/cli/lint-process.feature +++ b/tests/features/cli/lint-process.feature @@ -2,6 +2,7 @@ @architect-pattern:LintProcessCliBehavior @architect-status:completed @architect-unlock-reason:Retroactive-completion-during-rebrand +@architect-implements:LintProcessCLI @architect-product-area:DataAPI @cli @lint-process Feature: lint-process CLI diff --git a/tests/features/cli/list-parent-child-alpha.feature b/tests/features/cli/list-parent-child-alpha.feature deleted file mode 100644 index a6ae687..0000000 --- a/tests/features/cli/list-parent-child-alpha.feature +++ /dev/null @@ -1,25 +0,0 @@ -@architect -@architect-pattern:ChildAlpha -@architect-status:active -@architect-level:slice -@architect-parent:ParentEpic -@architect-uses:ChildBeta -@cli @pattern-graph-cli -Feature: Child Alpha - Package-host seed child for list --parent acceptance coverage. - - **Problem:** Alpha needs a delivery owner. - - **Open Questions:** - - Who owns the alpha follow-up? - - Which signal closes the alpha gap? - - Rule: Alpha bundle data stays grouped - - **Invariant:** Alpha bundle data must keep its open questions and dependencies together. - - **Verified by:** Alpha child exists - - Scenario: Alpha child exists - Given a child pattern - Then it is returned by its parent filter diff --git a/tests/features/cli/list-parent-child-beta.feature b/tests/features/cli/list-parent-child-beta.feature deleted file mode 100644 index a02cbc5..0000000 --- a/tests/features/cli/list-parent-child-beta.feature +++ /dev/null @@ -1,23 +0,0 @@ -@architect -@architect-pattern:ChildBeta -@architect-status:active -@architect-level:slice -@architect-parent:ParentEpic -@cli @pattern-graph-cli -Feature: Child Beta - Package-host seed child for list --parent acceptance coverage. - - **Problem:** Beta still needs a rollout signal. - - **Open Questions:** - - What beta rollout signal is durable? - - Rule: Beta scenarios remain visible - - **Invariant:** Bundle scenario extraction must preserve beta scenario names. - - **Verified by:** Beta child exists - - Scenario: Beta child exists - Given another child pattern - Then it is returned by its parent filter diff --git a/tests/features/cli/list-parent-empty-epic.feature b/tests/features/cli/list-parent-empty-epic.feature deleted file mode 100644 index bb2b0c2..0000000 --- a/tests/features/cli/list-parent-empty-epic.feature +++ /dev/null @@ -1,11 +0,0 @@ -@architect -@architect-pattern:EmptyEpic -@architect-status:active -@architect-level:epic -@cli @pattern-graph-cli -Feature: Empty Epic - Package-host seed parent with no children for list --parent empty-result coverage. - - Scenario: Empty epic exists - Given a parent epic without children - Then parent-scoped list queries return an empty result diff --git a/tests/features/cli/list-parent-parent-epic.feature b/tests/features/cli/list-parent-parent-epic.feature deleted file mode 100644 index 9be61eb..0000000 --- a/tests/features/cli/list-parent-parent-epic.feature +++ /dev/null @@ -1,15 +0,0 @@ -@architect -@architect-pattern:ParentEpic -@architect-status:active -@architect-level:epic -@cli @pattern-graph-cli -Feature: Parent Epic - Package-host seed parent for list --parent acceptance coverage. - - **Problem:** Parent bundles should collapse child lookups into one query. - - **Solution:** Keep immediate child slices grouped under this epic. - - Scenario: Parent epic exists - Given a parent epic - Then child patterns can attach to it diff --git a/tests/features/cli/pattern-graph-cli-arch-health.feature b/tests/features/cli/pattern-graph-cli-arch-health.feature deleted file mode 100644 index ca41073..0000000 --- a/tests/features/cli/pattern-graph-cli-arch-health.feature +++ /dev/null @@ -1,68 +0,0 @@ -@architect -@architect-pattern:PatternGraphCliArchHealth -@architect-implements:PatternGraphAPICLI -@architect-status:completed -@architect-unlock-reason:Split-from-original -@architect-product-area:DataAPI -@cli @pattern-graph-cli -Feature: Pattern Graph CLI - Architecture Health Subcommands - Architecture health subcommands: dangling, orphans, blocking. - - Background: - Given a temporary working directory - - Rule: CLI arch health subcommands detect graph quality issues - - **Invariant:** Health subcommands (dangling, orphans, blocking) operate on the relationship index, not the architecture index, and return results without requiring arch annotations. - - **Rationale:** Graph quality issues (broken references, isolated patterns, blocked dependencies) are relationship-level concerns that should be queryable even when no architecture metadata exists. - - **Verified by:** Arch dangling returns broken references, Arch dangling baseline matches current references, Arch dangling strict baseline drift reports added and removed entries, Arch dangling write-baseline rewrites deterministic JSON, Arch orphans returns isolated patterns, Arch blocking returns blocked patterns - - @happy-path - Scenario: Arch dangling returns broken references - Given TypeScript files with a dangling reference - When running "pattern-graph-cli -i 'src/**/*.ts' arch dangling" - Then exit code is 0 - And stdout JSON data is an array - And stdout JSON data contains an entry with field "missing" - - @happy-path - Scenario: Arch dangling baseline matches current references - Given TypeScript files with a dangling reference - And a dangling baseline file matching current references - When running "pattern-graph-cli -i 'src/**/*.ts' arch dangling --baseline dangling-baseline.json" - Then exit code is 0 - And stdout JSON data reports no dangling baseline drift - - @validation - Scenario: Arch dangling strict baseline drift reports added and removed entries - Given TypeScript files with a dangling reference - And a dangling baseline file with a different reference - When running "pattern-graph-cli -i 'src/**/*.ts' arch dangling --baseline dangling-baseline.json --strict" - Then exit code is 1 - And stdout JSON data reports one added and one removed dangling baseline entry - - @happy-path - Scenario: Arch dangling write-baseline rewrites deterministic JSON - Given TypeScript files with a dangling reference - When running "pattern-graph-cli -i 'src/**/*.ts' arch dangling --baseline dangling-baseline.json --write-baseline" - Then exit code is 0 - And dangling baseline file is deterministic for the current references - - @happy-path - Scenario: Arch orphans returns isolated patterns - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' arch orphans" - Then exit code is 0 - And stdout JSON data is an array - And stdout JSON data contains an entry with field "pattern" - - @happy-path - Scenario: Arch blocking returns blocked patterns - Given TypeScript files with blocked pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' arch blocking" - Then exit code is 0 - And stdout JSON data is an array - And stdout JSON data contains an entry with field "pattern" - And stdout JSON data contains a blocking entry with field "blockedBy" diff --git a/tests/features/cli/pattern-graph-cli-core.feature b/tests/features/cli/pattern-graph-cli-core.feature deleted file mode 100644 index 2ab91b2..0000000 --- a/tests/features/cli/pattern-graph-cli-core.feature +++ /dev/null @@ -1,294 +0,0 @@ -@architect -@architect-pattern:PatternGraphAPICLI -@architect-status:completed -@architect-unlock-reason:Split-from-original -@architect-phase:24 -@architect-product-area:DataAPI -@cli @pattern-graph-cli -Feature: Pattern Graph CLI - Core Infrastructure - - **Problem:** - The PatternGraphAPI provides 27 typed query methods for efficient state queries, but - Claude Code sessions cannot use it directly: - - Import paths require built packages with correct ESM resolution - - No CLI command exposes the API for shell invocation - - Current workaround requires regenerating markdown docs and reading them - - Documentation claims API is "directly usable" but practical usage is blocked - - **Solution:** - Add a CLI command `pnpm architect:query` that exposes key PatternGraphAPI methods - with JSON and text output formats, enabling direct programmatic access from AI sessions. - - Core CLI infrastructure: help, version, input validation, status, query, pattern, arch basics, missing args, edge cases. - - Background: - Given a temporary working directory - | Deliverable | Status | Tests | Location | - | PatternGraph CLI core routing | complete | Yes | packages/architect-cli/src/cli/pattern-graph-cli.ts | - | CLI core behavior specification | complete | Yes | packages/architect/tests/features/cli/pattern-graph-cli-core.feature | - | CLI core step coverage | complete | Yes | packages/architect/tests/steps/cli/pattern-graph-cli-core.steps.ts | - - # ============================================================================ - # RULE 1: Help and Version - # ============================================================================ - - Rule: CLI displays help and version information - - **Invariant:** The CLI must always provide discoverable usage and version information via standard flags. - **Rationale:** Without accessible help and version output, users cannot self-serve CLI usage or report issues with a specific version. - - @acceptance-criteria @happy-path - Scenario: Display help with --help flag - When running "pattern-graph-cli --help" - Then exit code is 0 - And stdout contains "arch roles" - And stdout does not contain "arch-roles" - - @happy-path - Scenario: Display version with -v flag - When running "pattern-graph-cli -v" - Then exit code is 0 - - @validation - Scenario: No subcommand shows help - When running "pattern-graph-cli -i 'src/**/*.ts'" - Then exit code is 1 - And output contains "Usage:" - - # ============================================================================ - # RULE 2: Input Validation - # ============================================================================ - - Rule: CLI requires input flag for subcommands - - **Invariant:** Every data-querying subcommand must receive either an explicit `--input` glob or a project config that provides source globs. - **Rationale:** Without an input source, the pipeline has no files to scan and would produce empty or misleading results instead of a clear error, but project config auto-detection should remove that boilerplate when the repo is configured. - - @validation - Scenario: Fail without --input flag when running status - When running "pattern-graph-cli status" - Then exit code is 1 - And output contains "--input" - - @acceptance-criteria @happy-path - Scenario: Use architect.config.js sources when --input is omitted - Given TypeScript files with pattern annotations - And an architect.config.js with TypeScript sources - When running "pattern-graph-cli status" - Then exit code is 0 - And stdout contains "StatusDistribution" - - @validation - Scenario: Reject unknown options - When running "pattern-graph-cli --unknown-flag" - Then exit code is 1 - And output contains "Unknown option" - - @validation - Scenario: Handoff rejects too many modified-file flags - Given TypeScript files with pattern annotations - When I run handoff for "ActivePattern" with too many modified-file flags - Then exit code is 1 - And output contains "Usage: architect handoff" - - @happy-path - Scenario: Handoff accepts positional pattern with modified file - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' handoff ActivePattern --modified-file src/active.ts" - Then exit code is 0 - And stdout contains "HANDOFF: ActivePattern" - - @validation - Scenario: Scope-validate rejects conflicting scope values - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' scope-validate ActivePattern design --type implement" - Then exit code is 1 - And output contains "Scope type conflict" - - # ============================================================================ - # RULE 3: Status Subcommand - # ============================================================================ - - Rule: CLI status subcommand shows delivery state - - **Invariant:** The status subcommand must return structured JSON containing delivery progress derived from the PatternGraph. - **Rationale:** Consumers depend on machine-readable status output for scripting and CI integration; unstructured output breaks downstream automation. - - @happy-path - Scenario: Status shows counts and completion percentage - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' status" - Then exit code is 0 - And stdout contains "StatusDistribution" - - # ============================================================================ - # RULE 4: Query Subcommand - # ============================================================================ - - Rule: CLI query subcommand executes API methods - - **Invariant:** The query subcommand must dispatch to any public Data API method by name, pass positional arguments through, and reject invalid enum arguments with a clear error. - **Rationale:** The CLI is the primary interface for ad-hoc queries; failing to resolve a valid method name or its arguments silently drops the user's request. - - @acceptance-criteria @happy-path - Scenario: Query getStatusCounts returns count object - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' query getStatusCounts" - Then exit code is 0 - And stdout is valid JSON - - @happy-path - Scenario: Query isValidTransition with arguments - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' query isValidTransition roadmap active" - Then exit code is 0 - And stdout is valid JSON - - @validation - Scenario: Unknown API method shows error - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' query nonExistentMethod" - Then exit code is 1 - And output contains "Unknown" - - @validation - Scenario: Invalid accepted status argument shows error - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' query getPatternsByStatus invalid-status" - Then exit code is 1 - And output contains "accepted status value" - - @validation - Scenario: Invalid phase query argument shows error - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' query getPatternsByPhase not-a-number" - Then exit code is 1 - And output contains "Phase must be an integer" - - # ============================================================================ - # RULE 5: Pattern Subcommand - # ============================================================================ - - Rule: CLI pattern subcommand shows pattern detail - - **Invariant:** The pattern subcommand must return the full JSON detail for an exact pattern name match, or a clear error if not found. - **Rationale:** Pattern lookup is the primary debugging tool for annotation issues; ambiguous or silent failures waste investigation time. - - @happy-path - Scenario: Pattern lookup returns full detail - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' pattern CompletedPattern" - Then exit code is 0 - And stdout contains "CompletedPattern" - - @validation - Scenario: Pattern not found shows error - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' pattern NonExistent" - Then exit code is 1 - And output contains "not found" - - @validation - Scenario: Broken feature-backed pattern reports parser attribution - Given TypeScript files with pattern annotations - And a broken feature spec for BrokenSpecPattern - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'features/**/*.feature' pattern BrokenSpecPattern" - Then exit code is 1 - And output contains parse attribution for "features/broken-spec-pattern.feature" - - @validation - Scenario: Truly missing pattern does not report parser attribution - Given TypeScript files with pattern annotations - And a broken feature spec for BrokenSpecPattern - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'features/**/*.feature' pattern TrulyMissingPattern" - Then exit code is 1 - And output contains "not found" - And output does not contain "spec-parse-failed" - - # ============================================================================ - # RULE 6: Arch Subcommand - # ============================================================================ - - Rule: CLI arch subcommand queries architecture - - **Invariant:** The arch subcommand must expose role and bounded-context queries over the PatternGraph's architecture metadata and reject retired architecture verbs. - **Rationale:** Architecture queries replace manual exploration of annotated sources; stale aliases or incorrect results lead to wrong structural assumptions during design sessions. - **Verified by:** Arch roles lists roles with counts, Arch bounded-context filters to bounded context, Arch layer reports unknown subcommand - - @happy-path - Scenario: Arch roles lists roles with counts - Given TypeScript files with architecture annotations - When running "pattern-graph-cli -i 'src/**/*.ts' arch roles" - Then exit code is 0 - And stdout is valid JSON - - @happy-path - Scenario: Arch bounded-context filters to bounded context - Given TypeScript files with architecture annotations - When running "pattern-graph-cli -i 'src/**/*.ts' arch bounded-context testctx" - Then exit code is 0 - And stdout is valid JSON - - @validation - Scenario: Arch layer reports unknown subcommand - Given TypeScript files with architecture annotations - When running "pattern-graph-cli -i 'src/**/*.ts' arch layer" - Then exit code is 1 - And output contains "Unknown arch subcommand: layer" - - # ============================================================================ - # RULE 7: Error Handling for Missing Arguments - # ============================================================================ - - Rule: CLI shows errors for missing subcommand arguments - - **Invariant:** Subcommands that require arguments must reject invocations with missing arguments and display usage guidance. - **Rationale:** Silent acceptance of incomplete input would produce confusing pipeline errors instead of actionable feedback at the CLI boundary. - - @validation - Scenario: Query without method name shows error - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' query" - Then exit code is 1 - And output contains "Usage:" - - @validation - Scenario: Pattern without name shows error - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' pattern" - Then exit code is 1 - And output contains "Usage:" - - @validation - Scenario: Unknown subcommand shows error - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' foobar" - Then exit code is 1 - And output contains "Unknown subcommand" - - # ============================================================================ - # RULE 8: Edge Cases - # ============================================================================ - - Rule: CLI handles argument edge cases - - **Invariant:** The CLI must gracefully handle non-standard argument forms including numeric coercion and the `--` pnpm separator. - **Rationale:** Real-world invocations via pnpm pass `--` separators and numeric strings; mishandling these causes silent data loss or crashes in automated workflows. - - @edge-case - Scenario: Integer arguments are coerced for phase queries - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' query getPatternsByPhase 1" - Then exit code is 0 - - @edge-case - Scenario: Double-dash separator is handled gracefully - When running "pattern-graph-cli -- --help" - Then exit code is 0 - - @validation - Scenario: Legacy category filter is rejected with role guidance - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' list --category service" - Then exit code is 1 - And output contains "Legacy --category is no longer supported. Use --role instead." diff --git a/tests/features/cli/pattern-graph-cli-output-modifiers.feature b/tests/features/cli/pattern-graph-cli-output-modifiers.feature deleted file mode 100644 index ce3b983..0000000 --- a/tests/features/cli/pattern-graph-cli-output-modifiers.feature +++ /dev/null @@ -1,130 +0,0 @@ -@architect -@architect-pattern:PatternGraphCliOutputModifiers -@architect-implements:PatternGraphAPICLI -@architect-status:completed -@architect-unlock-reason:Split-from-original -@architect-product-area:DataAPI -@cli @pattern-graph-cli -Feature: Pattern Graph CLI - Output Modifiers - Output modifiers (--count, --names-only, --fields), parent filters, open-questions, and bundle composition. - - Background: - Given a temporary working directory - - Rule: Output modifiers work when placed after the subcommand - - **Invariant:** Output modifiers (--count, --names-only, --fields) produce identical results regardless of position relative to the subcommand and its filters. - - **Rationale:** Users should not need to memorize argument ordering rules; the CLI should be forgiving. - - **Verified by:** Count modifier after list subcommand returns count, Names-only modifier after list subcommand returns names, Count modifier combined with list filter, Parent filter with names-only returns child names, Parent filter with count returns child count, Parent filter returns empty for parent without children, Open questions parent filter returns only descendants with questions, Open questions empty parent returns an empty document, Open questions unknown parent fails deterministically, Bundle include blocks return a composite payload, Bundle mode default include set returns heuristic token estimates, Bundle unknown root pattern fails deterministically, Bundle accumulates repeated include flags, Unknown parent filter fails deterministically - - @happy-path - Scenario: Count modifier after list subcommand returns count - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' list --count" - Then exit code is 0 - And stdout is a JSON number - - @happy-path - Scenario: Names-only modifier after list subcommand returns names - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' list --names-only" - Then exit code is 0 - And stdout is a JSON string array - - @happy-path - Scenario: Count modifier combined with list filter - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' list --status completed --count" - Then exit code is 0 - And stdout is a JSON number - - @happy-path - Scenario: Parent filter with names-only returns child names - Given Gherkin feature files with parent hierarchy - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' list --parent ParentEpic --names-only" - Then exit code is 0 - And stdout is a JSON string array - And the list names-only result equals "ChildAlpha, ChildBeta" - - @happy-path - Scenario: Parent filter with count returns child count - Given Gherkin feature files with parent hierarchy - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' list --parent ParentEpic --count" - Then exit code is 0 - And stdout is a JSON number - And the list count equals 2 - - @edge-case - Scenario: Parent filter returns empty for parent without children - Given Gherkin feature files with parent hierarchy - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' list --parent EmptyEpic --names-only" - Then exit code is 0 - And stdout is an empty JSON string array - - @happy-path - Scenario: Open questions parent filter returns only descendants with questions - Given Gherkin feature files with parent hierarchy - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' --format json open-questions --parent ParentEpic" - Then exit code is 0 - And the open question result contains patterns "ChildAlpha, ChildBeta" - And every open question result entry has at least one question - - @edge-case - Scenario: Open questions empty parent returns an empty document - Given Gherkin feature files with parent hierarchy - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' --format json open-questions --parent EmptyEpic" - Then exit code is 0 - And the open question result is empty - - @validation - Scenario: Open questions unknown parent fails deterministically - Given Gherkin feature files with parent hierarchy - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' open-questions --parent UnknownParent" - Then parent filter fails with "Parent pattern not found: UnknownParent" - - @happy-path - Scenario: Bundle include blocks return a composite payload - Given Gherkin feature files with parent hierarchy - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' bundle ParentEpic --include rules,scenarios,deps,open-questions --format json" - Then exit code is 0 - And stdout is valid JSON - And the bundle result contains children "ChildAlpha, ChildBeta" - And the bundle result includes requested block families "rules, scenarios, deps, open-questions" - And the bundle result preserves the ChildAlpha dependency on ChildBeta - - @happy-path - Scenario: Bundle mode default include set returns heuristic token estimates - Given Gherkin feature files with parent hierarchy - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' bundle ParentEpic --mode implement --estimate-tokens --format json" - Then exit code is 0 - And stdout is valid JSON - And the bundle root mode is "implement" - And the bundle result includes requested block families "docstring, rules, scenarios, deps, open-questions" - And the bundle token estimates use the "char/4" heuristic - - @validation - Scenario: Bundle unknown root pattern fails deterministically - Given Gherkin feature files with parent hierarchy - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' bundle NoSuchPattern --include rules" - Then parent filter fails with "Pattern not found:" - - @happy-path - Scenario: Bundle accumulates repeated include flags - Given Gherkin feature files with parent hierarchy - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' bundle ParentEpic --include rules --include deps --format json" - Then exit code is 0 - And stdout is valid JSON - And the bundle result includes requested block families "rules, deps" - - @validation - Scenario: Unknown parent filter fails deterministically - Given Gherkin feature files with parent hierarchy - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' list --parent UnknownParent" - Then parent filter fails with "Parent pattern not found: UnknownParent" - - @validation - Scenario: Malformed projection bundle JSON is rejected - When serializing malformed projection bundle data - Then serialization fails with "Received malformed projection bundle" diff --git a/tests/features/cli/pattern-graph-cli-rules-subcommand.feature b/tests/features/cli/pattern-graph-cli-rules-subcommand.feature deleted file mode 100644 index c20b73d..0000000 --- a/tests/features/cli/pattern-graph-cli-rules-subcommand.feature +++ /dev/null @@ -1,166 +0,0 @@ -@architect -@architect-pattern:PatternGraphCliRulesSubcommand -@architect-implements:PatternGraphAPICLI -@architect-status:completed -@architect-unlock-reason:Split-from-original -@architect-product-area:DataAPI -@cli @pattern-graph-cli -Feature: Pattern Graph CLI - Rules Subcommand - The rules subcommand queries business rules and invariants extracted from Gherkin Rule: blocks. - - Background: - Given a temporary working directory - - Rule: CLI rules subcommand queries business rules and invariants - - **Invariant:** The rules subcommand returns structured business rules extracted from Gherkin Rule: blocks via the projection layer. - - **Rationale:** Live business rule queries replace static generated markdown, enabling on-demand filtering by product area, pattern, package, feature path, and invariant presence. - - **Verified by:** Rules returns business rules from feature files, Rules filters by product area, Rules with names-only returns flat array, Rules with count returns a JSON number, Rules filters by canonical package name, Rules package filter works with count, Rules feature path filter works with count, Rules feature glob filter works with names-only, Rules rejects retired phase filter - - @happy-path - Scenario: Rules returns business rules from feature files - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules" - Then exit code is 0 - And stdout contains "BusinessRuleSet" - - @contract - Scenario: Rules with --format json preserves routed bundle metadata - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' --format json rules" - Then exit code is 0 - And stdout is valid JSON for a routed BusinessRuleSet bundle - And routed rules JSON keeps canonical bundle key ordering - And raw routed rules JSON keeps canonical serializer order on the wire - And the bundle root validates against FragmentSchema - - @happy-path - Scenario: Rules filters by product area - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules --product-area Validation" - Then exit code is 0 - And stdout contains "BusinessRuleSet" - - @happy-path - Scenario: Rules with names-only returns flat array - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules --names-only" - Then exit code is 0 - And stdout is a JSON string array - - @happy-path - Scenario: Rules with count returns a JSON number - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules --count" - Then exit code is 0 - And stdout is a JSON number - And the rules count equals 4 - - @validation - Scenario: Rules filters by pattern name - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules --pattern CoreUtilsTest" - Then exit code is 0 - And stdout contains "BusinessRuleSet" - - @validation - Scenario: Rules with only-invariants excludes rules without invariants - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules --only-invariants" - Then exit code is 0 - And stdout contains "BusinessRuleSet" - - @edge-case - Scenario: Rules product area filter excludes non-matching areas - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules --product-area Validation" - Then exit code is 0 - And stdout contains "BusinessRuleSet" - - @edge-case - Scenario: Rules combines product area and only-invariants filters - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules --product-area CoreTypes --only-invariants" - Then exit code is 0 - And stdout contains "BusinessRuleSet" - - @happy-path - Scenario: Rules filters by canonical package name - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules --package @libar-dev/architect-cli" - Then exit code is 0 - And stdout contains "CoreUtilsTest" - And stdout does not contain "ValidationRulesTest" - - @happy-path - Scenario: Rules package filter works with count - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules --package @libar-dev/architect-cli --count" - Then exit code is 0 - And stdout is a JSON number - And the rules count equals 2 - - @happy-path - Scenario: Rules feature path filter works with count - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules --feature packages/architect-cli/specs/core-utils.feature --count" - Then exit code is 0 - And stdout is a JSON number - And the rules count equals 2 - - @happy-path - Scenario: Rules feature glob filter works with names-only - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules --feature 'packages/architect-core/**/*.feature' --names-only" - Then exit code is 0 - And stdout is a JSON string array - And the rules names-only result has 2 entries - - @happy-path - Scenario: Rules feature path filter accepts package-host repo-relative path - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' rules --feature packages/architect/tests/features/cli/package-host-rules.feature --count" - Then exit code is 0 - And stdout is a JSON number - And the rules count equals 1 - - @happy-path - Scenario: Rules feature glob filter accepts package-host repo-relative glob - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' rules --feature 'packages/architect/tests/features/cli/*.feature' --names-only" - Then exit code is 0 - And stdout is a JSON string array - And the rules names-only result has 1 entries - - @validation - Scenario: Rules rejects retired phase filter - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules --phase 5" - Then exit code is 1 - And output contains "Unknown option: --phase" - - @validation - Scenario: Rules rejects conflicting pattern and product-area filters - Given TypeScript files with pattern annotations - And Gherkin feature files with business rules - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'packages/**/specs/**/*.feature' rules --pattern CoreUtilsTest --product-area Validation" - Then exit code is 1 - And output contains "--pattern, --product-area, --package, and --feature cannot be combined" diff --git a/tests/features/cli/pattern-graph-cli-subcommands.feature b/tests/features/cli/pattern-graph-cli-subcommands.feature deleted file mode 100644 index b553b83..0000000 --- a/tests/features/cli/pattern-graph-cli-subcommands.feature +++ /dev/null @@ -1,231 +0,0 @@ -@architect -@architect-pattern:PatternGraphCliSubcommands -@architect-implements:PatternGraphAPICLI -@architect-status:completed -@architect-unlock-reason:Split-from-original -@architect-product-area:DataAPI -@cli @pattern-graph-cli -Feature: Pattern Graph CLI - Discovery Subcommands - Discovery subcommands: list, search, context assembly, tags/sources, extended arch, unannotated. - - Background: - Given a temporary working directory - - # ============================================================================ - # RULE 9: List Subcommand - # ============================================================================ - - Rule: CLI list subcommand filters patterns - - **Invariant:** The list subcommand must return a valid JSON result for valid filters and a non-zero exit code with a descriptive error for invalid filters. - **Rationale:** Consumers parse list output programmatically; malformed JSON or silent failures cause downstream tooling to break without diagnosis. - **Verified by:** List all patterns returns JSON array, List filters candidate status, List with removed phase flag shows error, List with removed maturity flag shows error - - @happy-path - Scenario: List all patterns returns JSON array - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' list" - Then exit code is 0 - And stdout is valid JSON - - @validation - Scenario: List filters candidate status - Given TypeScript files with candidate and delivery pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' list --status candidate" - Then exit code is 0 - And stdout is valid JSON - And stdout contains "CandidatePattern" - And stdout does not contain "RoadmapPattern" - - @validation - Scenario: List with removed phase flag shows error - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' list --phase abc" - Then exit code is 1 - And output contains "Unknown option: --phase" - - @validation - Scenario: List with removed maturity flag shows error - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' list --maturity plan" - Then exit code is 1 - And output contains "Unknown option: --maturity" - - # ============================================================================ - # RULE 10: Search Subcommand - # ============================================================================ - - Rule: CLI search subcommand finds patterns by fuzzy match - - **Invariant:** The search subcommand must require a query argument and return only patterns whose names match the query. - **Rationale:** Missing query validation would produce unfiltered result sets, defeating the purpose of search and wasting context budget in AI sessions. - **Verified by:** Search returns matching patterns, Search without query shows error - - @happy-path - Scenario: Search returns matching patterns - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' search Completed" - Then exit code is 0 - And stdout is valid JSON - And stdout contains "CompletedPattern" - - @validation - Scenario: Search without query shows error - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' search" - Then exit code is 1 - And output contains "Usage:" - - # ============================================================================ - # RULE 11: Context Assembly Subcommands - # ============================================================================ - - Rule: CLI context assembly subcommands return text output - - **Invariant:** Context assembly subcommands (context, overview, dep-tree) must produce non-empty human-readable text containing the requested pattern or summary, and require a pattern argument where applicable. - **Rationale:** These subcommands replace manual file reads in AI sessions; empty or off-target output forces expensive explore-agent fallbacks that consume 5-10x more context. - **Verified by:** Context returns curated text bundle, Context without pattern name shows error, Overview returns executive summary text, Dep-tree returns dependency tree text - - @happy-path - Scenario: Context returns curated text bundle - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' context CompletedPattern" - Then exit code is 0 - And stdout is non-empty - And stdout contains "CompletedPattern" - - @validation - Scenario: Context without pattern name shows error - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' context" - Then exit code is 1 - And output contains "Usage:" - - @happy-path - Scenario: Overview returns executive summary text - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' overview" - Then exit code is 0 - And stdout is non-empty - And stdout contains "PROGRESS" - - @happy-path - Scenario: Dep-tree returns dependency tree text - Given TypeScript files with architecture annotations and dependencies - When running "pattern-graph-cli -i 'src/**/*.ts' dep-tree ContextFormatterImpl" - Then exit code is 0 - And stdout is non-empty - - # ============================================================================ - # RULE 11B: Diagnostics Subcommand - # ============================================================================ - - Rule: CLI diagnostics subcommand returns extraction diagnostics - - **Invariant:** The diagnostics subcommand must expose structured extraction diagnostics from the current build. - **Rationale:** Missing extraction diagnostics force users to infer silent drops from absent patterns instead of receiving direct pipeline feedback. - **Verified by:** Diagnostics returns extraction failures from feature files - - @happy-path - Scenario: Diagnostics returns extraction failures from feature files - Given TypeScript files with pattern annotations - And feature files with extraction diagnostics - When running "pattern-graph-cli -i 'src/**/*.ts' -f 'architect/specs/**/*.feature' diagnostics" - Then exit code is 0 - And stdout is valid JSON - And stdout contains "missing-status" - - # ============================================================================ - # RULE 12: Tags, Taxonomy, and Sources Subcommands - # ============================================================================ - - Rule: CLI tags, taxonomy, and sources subcommands return JSON - - **Invariant:** The tags, taxonomy, and sources subcommands must return valid JSON with the expected top-level structure. `tags` projects `TagUsageMatrix` (operational-insights), `taxonomy` projects `TaxonomyDigest` (governance) -- they are sibling verbs from sibling DDD subdomains, not aliases. - **Rationale:** Annotation exploration depends on machine-parseable output; invalid JSON prevents automated enrichment workflows from detecting unannotated files and tag gaps. Surfacing `tags` and `taxonomy` as distinct verbs makes the projection package's subdomain split visible at the CLI surface. - **Verified by:** Tags returns tag usage counts, Taxonomy returns taxonomy digest, Taxonomy count returns compact text, Taxonomy JSON count returns four numeric keys, Sources returns file inventory - - @happy-path - Scenario: Tags returns tag usage counts - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' tags" - Then exit code is 0 - And stdout contains "TagUsageMatrix" - - @happy-path - Scenario: Taxonomy returns taxonomy digest - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' taxonomy --format json" - Then exit code is 0 - And stdout contains "TaxonomyDigest" - And stdout is valid JSON - - @happy-path - Scenario: Taxonomy count returns compact text - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' taxonomy --count" - Then exit code is 0 - And stdout is a single taxonomy count line - - @happy-path - Scenario: Taxonomy JSON count returns four numeric keys - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' taxonomy --format json --count" - Then exit code is 0 - And stdout is a taxonomy count JSON object - - @happy-path - Scenario: Sources returns file inventory - Given TypeScript files with pattern annotations - When running "pattern-graph-cli -i 'src/**/*.ts' sources" - Then exit code is 0 - And stdout is valid JSON - - # ============================================================================ - # RULE 13: Extended Arch Subcommands - # ============================================================================ - - Rule: CLI extended arch subcommands query architecture relationships - - **Invariant:** Extended arch subcommands (neighborhood, compare, coverage) must return valid JSON reflecting the actual architecture relationships present in the scanned sources. - **Rationale:** Architecture queries drive design-session decisions; stale or structurally invalid output leads to incorrect dependency analysis and missed coupling between bounded contexts. - **Verified by:** Arch neighborhood returns pattern relationships, Arch compare returns bounded-context comparison, Arch coverage returns annotation coverage - - @happy-path - Scenario: Arch neighborhood returns pattern relationships - Given TypeScript files with architecture annotations and dependencies - When running "pattern-graph-cli -i 'src/**/*.ts' arch neighborhood ContextFormatterImpl" - Then exit code is 0 - And stdout is valid JSON - And stdout contains "ContextFormatterImpl" - - @happy-path - Scenario: Arch compare returns bounded-context comparison - Given TypeScript files with two bounded contexts - When running "pattern-graph-cli -i 'src/**/*.ts' arch compare scanner codec" - Then exit code is 0 - And stdout is valid JSON - - @happy-path - Scenario: Arch coverage returns annotation coverage - Given TypeScript files with architecture annotations - When running "pattern-graph-cli -i 'src/**/*.ts' arch coverage" - Then exit code is 0 - And stdout is valid JSON - - # ============================================================================ - # RULE 14: Unannotated Subcommand - # ============================================================================ - - Rule: CLI unannotated subcommand finds files without annotations - - **Invariant:** The unannotated subcommand must return valid JSON listing every TypeScript file that lacks the `@architect` opt-in marker. - **Rationale:** Files missing the opt-in marker are invisible to the scanner; without this subcommand, unannotated files silently drop out of generated documentation and validation. - **Verified by:** Unannotated finds files missing architect marker - - @happy-path - Scenario: Unannotated finds files missing architect marker - Given TypeScript files with mixed annotations - When running "pattern-graph-cli -i 'src/**/*.ts' unannotated" - Then exit code is 0 - And stdout contains "AnnotationCoverage" diff --git a/tests/features/cli/public-contract.feature b/tests/features/cli/public-contract.feature index 0ac1ebd..167f879 100644 --- a/tests/features/cli/public-contract.feature +++ b/tests/features/cli/public-contract.feature @@ -2,21 +2,24 @@ @architect-pattern:ArchitectPublicContract @architect-status:active @architect-product-area:DataAPI -@cli @pattern-graph-cli @contracts +@cli @contracts Feature: Architect public contract exports - Freeze the canonical public exports that refactors must preserve. + Freeze the canonical public exports that refactors must preserve and reject + removed facade compatibility paths. Rule: architect-core and architect-projection keep canonical exports importable - **Invariant:** Key `@libar-dev/architect-core` query exports and canonical - `@libar-dev/architect-projection` entrypoints remain publicly importable. - **Rationale:** CLI, MCP, and downstream consumers rely on the current package - surface while internals continue to evolve. - **Verified by:** architect-core query contract exports remain available, architect-projection canonical projection entrypoints remain public, architect-projection barrel exposes only the validated architecture entrypoint + **Invariant:** `@libar-dev/architect-core/graph`, graph construction, FSM, + dependency, rule, decision, and package kernels remain public while every + legacy facade and result-envelope export is absent at runtime. + **Rationale:** Callers should use the frozen Graph and named pure kernels, + with no parallel compatibility API that can drift from the canonical graph. + **Verified by:** architect-core graph and pure-kernel exports replace the legacy facade, architect-projection canonical projection entrypoints remain public, architect-projection barrel exposes only the validated architecture entrypoint @contract - Scenario: architect-core query contract exports remain available - Then architect-core query contract exports remain available + Scenario: architect-core graph and pure-kernel exports replace the legacy facade + Then architect-core graph and pure-kernel exports are available + And architect-core legacy facade exports are absent @contract Scenario: architect-projection canonical projection entrypoints remain public diff --git a/tests/features/cli/validate-patterns.feature b/tests/features/cli/validate-patterns.feature index 2f447fa..21f1f11 100644 --- a/tests/features/cli/validate-patterns.feature +++ b/tests/features/cli/validate-patterns.feature @@ -2,7 +2,6 @@ @architect-pattern:ValidatorReadModelConsolidation @architect-status:completed @architect-unlock-reason:Retroactive-completion-during-rebrand -@architect-phase:100 @architect-product-area:Validation @architect-uses:ADR006SingleReadModelArchitecture @cli @validate-patterns @@ -17,7 +16,7 @@ Feature: Validator Read Model Consolidation — validate-patterns CLI **Solution:** Refactored `validate-patterns.ts` to consume the PatternGraph as its data source for cross-source validation. The validator became a feature - consumer like codecs and the PatternGraphAPI — querying pre-computed + consumer like codecs and projections, querying pre-computed views and the relationship index instead of building its own maps. Command-line interface for cross-validating TypeScript patterns vs Gherkin feature files. @@ -26,7 +25,6 @@ Feature: Validator Read Model Consolidation — validate-patterns CLI Given a temporary working directory | Deliverable | Status | Tests | Location | | PatternGraph-backed validation read model | complete | Yes | packages/architect-guard/src/cli/validate-patterns.ts | - | DoD validation integration | complete | Yes | packages/architect-guard/src/validation/dod-validator.ts | | validate-patterns CLI behavior | complete | Yes | packages/architect/tests/features/cli/validate-patterns.feature | # ============================================================================ @@ -102,9 +100,9 @@ Feature: Validator Read Model Consolidation — validate-patterns CLI Then exit code is 0 And stdout contains "All validations passed" - # Wave 1 retired phase-mismatch detection (the @architect-phase tag remains - # but cross-source phase-mismatch reporting was removed). Restoring it is - # tracked as a follow-up; the canonical mismatch signal stays on status. + # Cross-source phase-mismatch detection was retired with the numeric + # @architect-phase tag (ADR-013). The "phase" column in the steps below is + # vestigial (parsed, then discarded); status is the canonical mismatch signal. @validation Scenario: Detect status mismatch between sources @@ -114,6 +112,32 @@ Feature: Validator Read Model Consolidation — validate-patterns CLI Then exit code is 1 And stdout contains "Status mismatch" + Rule: Extraction diagnostics affect validation result + + **Invariant:** Error-severity extraction diagnostics are validation failures and must produce a non-zero exit without claiming all validations passed. + **Rationale:** A malformed gated directive has already been rejected by the extraction boundary; treating that as success hides dropped source facts from CI. + **Verified by:** Extraction diagnostic errors fail validation + + @validation + Scenario: Extraction diagnostic errors fail validation + Given a TypeScript file "src/malformed.ts" with content: + """ + /** @architect */ + + /** + * @architect-status:completed + * @architect-role:utility + */ + export function malformed(): boolean { + return true; + } + """ + And a Gherkin file "features/test.feature" with pattern "CleanFeature" at phase 1 status "completed" + When running "validate-patterns -i src/*.ts -F features/*.feature" + Then exit code is 1 + And stdout contains "invalid-pattern-name" + And stdout does not contain "All validations passed" + # ============================================================================ # RULE 4: Output Formats # ============================================================================ @@ -182,24 +206,6 @@ Feature: Validator Read Model Consolidation — validate-patterns CLI And output contains "Warning" # ============================================================================ - # RULE 7: Definition of Done Validation + # RULE 7 (Definition of Done Validation) was retired per ADR-013 — the + # phase-keyed DoD validator was unpopulated machinery and was removed. # ============================================================================ - - Rule: CLI validates Definition of Done from PatternGraph - - **Invariant:** When `--dod` is enabled, the CLI must validate completed Gherkin patterns using the PatternGraph-backed DoD rules: completed patterns need terminal deliverables and at least one `@acceptance-criteria` scenario. - **Rationale:** The DoD path was migrated from raw Gherkin scans to PatternGraph. CLI coverage must prove the new path stays behaviorally correct. - **Verified by:** DoD passes for completed pattern with deliverables and acceptance criteria, DoD fails for completed pattern without acceptance criteria - - @acceptance-criteria @happy-path - Scenario: DoD passes for completed pattern with deliverables and acceptance criteria - Given a TypeScript file "src/pattern.ts" with pattern "DoDPass" at phase 1 status "completed" - And a completed DoD-ready Gherkin file "features/test.feature" with pattern "DoDPass" at phase 1 - When running "validate-patterns -i src/*.ts -F features/*.feature --dod" - Then exit code is 0 - And stdout contains "DoD Validation Summary" - - # Wave 1 retired phase-grouping for DoD validation, so the - # "completed pattern without acceptance criteria" failure path no longer - # fires through the same code path. Restoring DoD checks against the - # current grouping (status / bounded-context) is tracked as a follow-up. diff --git a/tests/features/generation/architecture-doc-render-budget.feature b/tests/features/generation/architecture-doc-render-budget.feature new file mode 100644 index 0000000..ef10a54 --- /dev/null +++ b/tests/features/generation/architecture-doc-render-budget.feature @@ -0,0 +1,19 @@ +# Tooling regression guard — intentionally NOT an @architect pattern (it has no +# domain identity; it guards the generated artifact, not a behaviour of the +# system). The architecture-splitting behaviour itself is specified by +# DocumentationCompositionProjectionExecutableTests (config-documentation.feature). +Feature: Generated architecture document stays within Mermaid's render budget + + **Business Value:** ARCHITECTURE.md is the entry point for understanding the + repo. If any Mermaid block exceeds the renderer's maximum text size it fails + to render with "Maximum text size in diagram exceeded", leaving readers with + no diagram at all. The architecture projection splits the view into many + bounded diagrams precisely to stay under that budget (see DECISIONS D-14). + + Rule: No single Mermaid block in the generated architecture document exceeds the renderer limit + + Scenario: every mermaid block in the generated architecture document is renderable + Given the generated architecture document at "docs-live/ARCHITECTURE.md" + When I extract its fenced mermaid blocks + Then it should contain more than one mermaid block + And every mermaid block should be smaller than 50000 characters diff --git a/tests/features/generation/load-preamble.feature b/tests/features/generation/load-preamble.feature index bf4c358..3ff9a7e 100644 --- a/tests/features/generation/load-preamble.feature +++ b/tests/features/generation/load-preamble.feature @@ -1,21 +1,22 @@ @architect @architect-pattern:LoadPreambleParser @architect-status:active +@architect-implements:MarkdownBlockParser @architect-product-area:Generation @behavior @load-preamble -Feature: Markdown-to-SectionBlock Parser +Feature: Markdown-to-Block Parser The parseMarkdownToBlocks function converts raw markdown content into - a readonly SectionBlock[] array using a 5-state line-by-line state machine. + a readonly Block[] array using a 5-state line-by-line state machine. This enables preamble content to be authored as markdown files instead of verbose inline TypeScript object literals. **Problem:** - Preamble content authored as inline TypeScript SectionBlock[] literals is + Preamble content authored as inline TypeScript Block[] literals is verbose (540+ lines per codec config) and hard to review. **Solution:** - A shared parser reads markdown and produces the same SectionBlock[] shape + A shared parser reads markdown and produces the same Block[] shape that codecs expect, enabling markdown authoring with TypeScript type safety. Background: @@ -129,6 +130,24 @@ Feature: Markdown-to-SectionBlock Parser When parsing the markdown to blocks Then block 1 is a code block with empty content + Rule: Code-fence language is a single identifier-shaped token + + **Invariant:** The language emitted for a fenced code block is the first whitespace-delimited token of the info string, kept only when it is identifier-shaped (1-64 characters of letters, digits, underscore, plus, hyphen, or dot); a non-conforming or absent token yields a code block with no language. + **Rationale:** The canonical CodeBlockSchema constrains `language` to that identifier shape, and CommonMark treats the first word of a code-fence info string as the language. Normalizing at parse time keeps every emitted code block valid against the one shared block vocabulary. + **Verified by:** Info string with a trailing attribute keeps only the language token, Non-identifier info string yields no language + + @edge-case @code + Scenario: Info string with a trailing attribute keeps only the language token + Given markdown with a code fence info string carrying extra tokens + When parsing the markdown to blocks + Then block 1 is a code block with language "ts" + + @edge-case @code + Scenario: Non-identifier info string yields no language + Given markdown with a non-identifier code fence info string + When parsing the markdown to blocks + Then block 1 is a code block with no language + Rule: Mermaid blocks are parsed into MermaidBlock **Invariant:** Code fences with the info string "mermaid" produce MermaidBlock instead of CodeBlock. @@ -164,3 +183,15 @@ Feature: Markdown-to-SectionBlock Parser Given markdown with bold and code span formatting When parsing the markdown to blocks Then block 1 is a paragraph preserving inline formatting + + Rule: Parser output validates against the canonical block schema + + **Invariant:** Every block parseMarkdownToBlocks emits validates against the canonical BlockSchema from architect-core; the parser shares one block vocabulary with the projection renderers rather than a divergent shape. + **Rationale:** The two former block vocabularies (architect-core SectionBlock, architect-projection BlockSchema) were reconciled to one canonical BlockSchema in architect-core (No-BC). Validating parser output against that schema at the test boundary makes producer/schema drift impossible. + **Verified by:** A mixed markdown document's blocks all validate against the canonical schema + + @happy-path @schema + Scenario: A mixed markdown document's blocks all validate against the canonical schema + Given markdown with heading, paragraph, table, code, and list + When parsing the markdown to blocks + Then every produced block validates against the canonical block schema diff --git a/tests/fixtures/dataset-factories.ts b/tests/fixtures/dataset-factories.ts index cfd71ca..7c6b251 100644 --- a/tests/fixtures/dataset-factories.ts +++ b/tests/fixtures/dataset-factories.ts @@ -60,13 +60,13 @@ export interface TestPatternGraphOptions { withRelationships?: boolean; /** - * Include timeline metadata (phase, quarter, completed, deliverables) + * Include timeline metadata (effort, team, deliverables) * @default false */ withTimeline?: boolean; /** - * Include roadmap phases + * Include a multi-status roadmap (completed/active/roadmap, dependencies) * @default false */ withRoadmap?: boolean; @@ -136,10 +136,10 @@ export function createTestPatternGraph(options: TestPatternGraphOptions = {}): R // Use dependency graph patterns patterns = createDependencyGraph(); } else if (withTimeline) { - // Use timeline patterns (with deliverables, quarters, etc.) + // Use timeline patterns (with deliverables, effort, team) patterns = createTimelinePatterns(); } else if (withRoadmap) { - // Use roadmap patterns (with phases, dependencies) + // Use roadmap patterns (multi-status, with dependencies) patterns = createRoadmapPatterns(); } else if (patternCount > 0) { // Generate specified number of patterns @@ -218,7 +218,7 @@ export function createPatternGraphWithRelationships(): RuntimePatternGraph { /** * Create a PatternGraph with timeline metadata * - * Includes patterns with phases, quarters, completion dates, and deliverables. + * Includes patterns with effort, team, and deliverables. * * @returns PatternGraph with timeline-enriched patterns */ @@ -227,11 +227,11 @@ export function createPatternGraphWithTimeline(): RuntimePatternGraph { } /** - * Create a PatternGraph with roadmap phases + * Create a PatternGraph with a multi-status roadmap * - * Includes patterns across multiple phases with dependencies. + * Includes patterns across completed/active/roadmap with dependencies. * - * @returns PatternGraph with phase-structured patterns + * @returns PatternGraph with a multi-status roadmap */ export function createPatternGraphWithRoadmap(): RuntimePatternGraph { return createTestPatternGraph({ withRoadmap: true }); @@ -257,7 +257,7 @@ export function createPatternGraphWithRoadmap(): RuntimePatternGraph { */ export function createPatternGraphWithCategories( categories: string[], - patternsPerCategory = 2 + patternsPerCategory = 2, ): RuntimePatternGraph { const patterns = createTestPatternSet({ categories, @@ -285,7 +285,7 @@ export function createPatternGraphWithADRs(count = 3): RuntimePatternGraph { category: 'decision', status: i <= count / 2 ? 'completed' : 'active', // ADR-specific fields would go in the directive metadata - }) + }), ); } @@ -315,7 +315,7 @@ function generateValidPatternId(index: number): string { function createPatternsWithStatusDistribution( counts: Partial, - categories: string[] + categories: string[], ): ExtractedPattern[] { const { completed = 0, active = 0, planned = 0 } = counts; const patterns: ExtractedPattern[] = []; @@ -333,7 +333,7 @@ function createPatternsWithStatusDistribution( name: `CompletedPattern${i + 1}`, category: getCategory(), status: 'completed', - }) + }), ); } @@ -345,7 +345,7 @@ function createPatternsWithStatusDistribution( name: `ActivePattern${i + 1}`, category: getCategory(), status: 'active', - }) + }), ); } @@ -357,7 +357,7 @@ function createPatternsWithStatusDistribution( name: `PlannedPattern${i + 1}`, category: getCategory(), status: 'roadmap', - }) + }), ); } diff --git a/tests/fixtures/pattern-factories.ts b/tests/fixtures/pattern-factories.ts index 249691c..15e9be3 100644 --- a/tests/fixtures/pattern-factories.ts +++ b/tests/fixtures/pattern-factories.ts @@ -30,8 +30,7 @@ import { /** * Deliverable structure for timeline testing * - * Matches the Deliverable type from dual-source.ts, with release - * now tracked at the deliverable level (not pattern level). + * Matches the Deliverable type from dual-source.ts. */ export interface TestDeliverable { name: string; @@ -39,8 +38,6 @@ export interface TestDeliverable { tests: number; location: string; finding?: string | undefined; - /** Release version this deliverable belongs to (e.g., "v0.2.0") */ - release?: string | undefined; } /** @@ -71,37 +68,25 @@ export interface TestPatternOptions { lines?: readonly [number, number]; /** Export information (default: single function export) */ exports?: ExportInfo[] | undefined; - /** Use cases (default: none) */ - useCases?: string[] | undefined; /** Scenarios (default: none) */ scenarios?: readonly ScenarioRef[] | undefined; /** Uses relationships (default: none) */ uses?: string[] | undefined; /** Used-by relationships (default: none) */ usedBy?: string[] | undefined; - /** Phase number (default: none) */ - phase?: number | undefined; /** When to use bullets (default: none) */ whenToUse?: string[] | undefined; /** Depends on patterns (default: none) */ dependsOn?: string[] | undefined; /** Enables patterns (default: none) */ enables?: string[] | undefined; - // Timeline-specific fields - /** Completion date in YYYY-MM-DD format (default: none) */ - completed?: string | undefined; - /** Quarter identifier like "Q1-2026" (default: none) */ - quarter?: string | undefined; - /** Effort estimate like "2w", "3d", "1m" (default: none) */ - effort?: string | undefined; + // Process and hierarchy fields /** Team responsible (default: none) */ team?: string | undefined; /** Deliverables list (default: none) */ deliverables?: TestDeliverable[] | undefined; - /** Workflow type for changelog mapping (default: none) */ + /** Workflow label for package requirement documentation (default: none) */ workflow?: string | undefined; - /** Priority level for process tracking (default: none) */ - priority?: 'critical' | 'high' | 'medium' | 'low' | undefined; /** Hierarchy level for grouping (default: none) */ level?: 'epic' | 'phase' | 'task' | undefined; /** Patterns this code implements (realization relationship, default: none) */ @@ -124,12 +109,8 @@ export interface TestPatternOptions { discoveredLearnings?: string[] | undefined; /** Discovered risks during implementation (default: none) */ discoveredRisks?: string[] | undefined; - /** Business value statement (default: none) */ - businessValue?: string | undefined; /** Target implementation path for stub files (default: none) */ targetPath?: string | undefined; - /** Design session that created this pattern (default: none) */ - since?: string | undefined; /** Related patterns for cross-reference (default: none) */ seeAlso?: string[] | undefined; // Architecture fields @@ -163,8 +144,6 @@ export interface PatternSetOptions { patternsPerCategory?: number; /** Include relationship data (default: false) */ withRelationships?: boolean; - /** Include use case data (default: false) */ - withUseCases?: boolean; /** Include all optional features (default: false) */ withAllFeatures?: boolean; } @@ -187,7 +166,7 @@ let patternCounter = 0; * const customPattern = createTestPattern({ * name: "CommandOrchestrator", * category: "core", - * useCases: ["When implementing a new command"], + * description: "Coordinates command execution.", * }); * ``` */ @@ -207,22 +186,16 @@ export function createTestPattern(options: TestPatternOptions = {}): ExtractedPa filePath = `packages/@libar-dev/platform-${category}/src/test.ts`, lines = [1, 10] as const, exports = [{ name: name.replace(/\s+/g, ''), type: 'function' as const }], - useCases, scenarios, uses, usedBy, - phase, whenToUse, dependsOn, enables, - // Timeline-specific fields - completed, - quarter, - effort, + // Process and hierarchy fields team, deliverables, workflow, - priority, level, implementsPatterns, // Display and traceability fields @@ -235,10 +208,8 @@ export function createTestPattern(options: TestPatternOptions = {}): ExtractedPa discoveredImprovements, discoveredLearnings, discoveredRisks, - businessValue, // Stub metadata targetPath, - since, seeAlso, // Architecture fields archRole, @@ -264,20 +235,18 @@ export function createTestPattern(options: TestPatternOptions = {}): ExtractedPa description, examples: [], position: { startLine: lines[0], endLine: lines[1] }, - ...(useCases && useCases.length > 0 ? { useCases } : {}), ...(mergedUses.length > 0 ? { uses: mergedUses } : {}), - ...(phase !== undefined ? { phase } : {}), ...(whenToUse && whenToUse.length > 0 ? { whenToUse } : {}), ...(targetPath ? { target: targetPath } : {}), - ...(since ? { since } : {}), ...(seeAlso && seeAlso.length > 0 ? { seeAlso } : {}), ...(executableSpecs && executableSpecs.length > 0 ? { executableSpecs } : {}), }; // Wave 1: maturity, archContext, archLayer were retired from the schema // (maturity derives from status at projection time; arch* collapsed into - // bounded-context). Options remain accepted for backward-compat but are - // not spread onto the returned ExtractedPattern (schema is strictObject). + // bounded-context). ADR-013 retired the numeric `phase` and the `quarter` + // temporal axis. Options remain accepted for backward-compat but are not + // spread onto the returned ExtractedPattern (schema is strictObject). void maturity; void archContext; void archLayer; @@ -300,19 +269,12 @@ export function createTestPattern(options: TestPatternOptions = {}): ExtractedPa extractedAt: new Date().toISOString(), patternName: patternName ?? name, ...(scenarios && scenarios.length > 0 ? { scenarios } : {}), - ...(useCases && useCases.length > 0 ? { useCases } : {}), ...(mergedUses.length > 0 ? { uses: mergedUses } : {}), - ...(phase !== undefined ? { phase } : {}), ...(whenToUse && whenToUse.length > 0 ? { whenToUse } : {}), - // Timeline-specific fields - ...(completed ? { completed } : {}), - ...(quarter ? { quarter } : {}), - ...(effort ? { effort } : {}), + // Process and hierarchy fields ...(team ? { team } : {}), - // Deliverables with release tracking (release is at deliverable level, not pattern level) ...(deliverables && deliverables.length > 0 ? { deliverables } : {}), ...(workflow ? { workflow } : {}), - ...(priority ? { priority } : {}), ...(level ? { level } : {}), ...(implementsPatterns && implementsPatterns.length > 0 ? { implementsPatterns } : {}), // Display and traceability fields @@ -327,10 +289,8 @@ export function createTestPattern(options: TestPatternOptions = {}): ExtractedPa : {}), ...(discoveredLearnings && discoveredLearnings.length > 0 ? { discoveredLearnings } : {}), ...(discoveredRisks && discoveredRisks.length > 0 ? { discoveredRisks } : {}), - ...(businessValue ? { businessValue } : {}), // Stub metadata ...(targetPath ? { targetPath } : {}), - ...(since ? { since } : {}), ...(seeAlso && seeAlso.length > 0 ? { seeAlso } : {}), // Architecture fields — Wave 1 retired archContext/archLayer; the // options are accepted for backward-compat but no longer set on the @@ -341,7 +301,7 @@ export function createTestPattern(options: TestPatternOptions = {}): ExtractedPa ...(convention && convention.length > 0 ? { convention } : {}), ...(rules && rules.length > 0 ? { rules } : {}), ...(extractedShapes && extractedShapes.length > 0 ? { extractedShapes } : {}), - } as ExtractedPattern; + }; } /** @@ -370,7 +330,6 @@ export function createTestPatternSet(options: PatternSetOptions = {}): Extracted categories = ['core', 'ddd'], patternsPerCategory = 2, withRelationships = false, - withUseCases = false, withAllFeatures = false, } = options; @@ -402,14 +361,6 @@ export function createTestPatternSet(options: PatternSetOptions = {}): Extracted exports: [{ name: name.replace(/\s+/g, ''), type: 'function' as const }], }; - // Add use cases - if (withUseCases || withAllFeatures) { - patternOptions.useCases = [ - `When implementing ${category} logic`, - `When refactoring existing ${category} code`, - ]; - } - // Add relationships if (withRelationships || withAllFeatures) { if (i > 0) { @@ -422,7 +373,6 @@ export function createTestPatternSet(options: PatternSetOptions = {}): Extracted // Add all features if (withAllFeatures) { - patternOptions.phase = Math.floor(i / 2) + 1; patternOptions.whenToUse = [ `When you need ${category} functionality`, `When integrating with external systems`, @@ -509,21 +459,18 @@ export function createRoadmapPatterns(): ExtractedPattern[] { name: 'Foundation Types', category: 'core', status: 'completed', - phase: 1, }), createTestPattern({ id: 'pattern-ba5e0102', name: 'Base Utilities', category: 'core', status: 'completed', - phase: 1, }), createTestPattern({ id: 'pattern-d0da0201', name: 'Domain Model', category: 'ddd', status: 'active', - phase: 2, dependsOn: ['Foundation Types'], }), createTestPattern({ @@ -531,49 +478,17 @@ export function createRoadmapPatterns(): ExtractedPattern[] { name: 'Advanced Features', category: 'saga', status: 'roadmap', - phase: 3, dependsOn: ['Domain Model', 'Base Utilities'], }), ]; } /** - * Create patterns with comprehensive use case coverage - */ -export function createUseCasePatterns(): ExtractedPattern[] { - return [ - createTestPattern({ - id: 'pattern-c0a0d001', - name: 'Command Handler', - category: 'cqrs', - useCases: [ - 'When implementing a new command', - 'When adding validation logic', - 'When orchestrating multiple services', - ], - whenToUse: [ - 'Complex business operations', - 'Operations that modify state', - 'Operations requiring transaction boundaries', - ], - }), - createTestPattern({ - id: 'pattern-00e27002', - name: 'Query Handler', - category: 'cqrs', - useCases: ['When implementing read operations', 'When optimizing for performance'], - whenToUse: ['Read-only operations', 'Operations that benefit from caching'], - }), - ]; -} - -/** - * Create patterns representing completed timeline milestones with deliverables + * Create patterns representing completed delivery milestones with deliverables * * Useful for testing: * - completed-phases section renderer - * - timeline-summary section renderer - * - Status filtering with timeline metadata + * - status filtering with delivery metadata */ export function createTimelinePatterns(): ExtractedPattern[] { return [ @@ -582,10 +497,6 @@ export function createTimelinePatterns(): ExtractedPattern[] { name: 'Foundation Types', category: 'core', status: 'completed', - phase: 1, - completed: '2025-12-15', - quarter: 'Q4-2025', - effort: '2w', team: 'platform', deliverables: [ { name: 'Decider interface', status: 'complete', tests: 1, location: 'src/decider/' }, @@ -597,10 +508,6 @@ export function createTimelinePatterns(): ExtractedPattern[] { name: 'CMS Integration', category: 'core', status: 'completed', - phase: 2, - completed: '2026-01-02', - quarter: 'Q1-2026', - effort: '1w', team: 'platform', deliverables: [{ name: 'CMS types', status: 'complete', tests: 1, location: 'src/cms/' }], }), @@ -609,9 +516,6 @@ export function createTimelinePatterns(): ExtractedPattern[] { name: 'Event Store Enhancement', category: 'event-sourcing', status: 'active', - phase: 3, - quarter: 'Q1-2026', - effort: '3w', team: 'platform', dependsOn: ['Foundation Types', 'CMS Integration'], }), @@ -620,9 +524,6 @@ export function createTimelinePatterns(): ExtractedPattern[] { name: 'Advanced Projections', category: 'projection', status: 'roadmap', - phase: 4, - quarter: 'Q2-2026', - effort: '2w', team: 'platform', dependsOn: ['Event Store Enhancement'], }), @@ -661,7 +562,7 @@ export function mergePatterns(...patternSets: ExtractedPattern[][]): ExtractedPa */ export function filterByCategory( patterns: ExtractedPattern[], - category: string + category: string, ): ExtractedPattern[] { return patterns.filter((p) => p.role === category); } diff --git a/tests/fixtures/scanner-fixtures.ts b/tests/fixtures/scanner-fixtures.ts index 0d5a74e..d3789d2 100644 --- a/tests/fixtures/scanner-fixtures.ts +++ b/tests/fixtures/scanner-fixtures.ts @@ -262,7 +262,7 @@ export function hasTag(tag: string): boolean { * Build content with multiple directives in same file. */ export function buildContentWithMultipleDirectives( - items: Array<{ category: string; name: string; description: string }> + items: Array<{ category: string; name: string; description: string }>, ): string { return items .map( @@ -272,7 +272,7 @@ export function buildContentWithMultipleDirectives( */ export function ${item.name}() { return '${item.name}'; -}` +}`, ) .join('\n\n'); } @@ -385,14 +385,8 @@ export interface GherkinContentOptions { featureName?: string; /** Feature description */ description?: string; - /** Phase number */ - phase?: number; /** Status (completed, in_progress, planned) */ status?: string; - /** Quarter (Q1-2025, etc.) */ - quarter?: string; - /** Effort estimate (1w, 2d, etc.) */ - effort?: string; /** Team (platform, frontend, etc.) */ team?: string; /** Pattern name from @libar-pattern tag */ @@ -418,7 +412,6 @@ export interface GherkinContentOptions { * ```typescript * const content = buildGherkinContent({ * featureName: "Order Processing", - * phase: 1, * status: "completed", * scenarios: [{ name: "Create order" }], * }); @@ -428,10 +421,7 @@ export function buildGherkinContent(options: GherkinContentOptions = {}): string const { featureName = 'Test Feature', description = 'A test feature', - phase, status, - quarter, - effort, team, patternName, dependencies = [], @@ -457,18 +447,9 @@ Scenario: Orphan scenario const lines: string[] = []; // Process metadata tags (using @architect-* prefix per PDR-004) - if (phase !== undefined) { - lines.push(`@architect-phase:${phase}`); - } if (status) { lines.push(`@architect-status:${status}`); } - if (quarter) { - lines.push(`@architect-quarter:${quarter}`); - } - if (effort) { - lines.push(`@architect-effort:${effort}`); - } if (team) { lines.push(`@architect-team:${team}`); } @@ -516,7 +497,7 @@ import type { ScannerScenarioState } from '../support/world.js'; * Used by step definitions to initialize module-level state. */ export function createScannerState( - overrides: Partial = {} + overrides: Partial = {}, ): ScannerScenarioState { return { tempDir: null, diff --git a/tests/planning-stubs/architecture/sequence-diagram.feature b/tests/planning-stubs/architecture/sequence-diagram.feature index 784802c..b707fde 100644 --- a/tests/planning-stubs/architecture/sequence-diagram.feature +++ b/tests/planning-stubs/architecture/sequence-diagram.feature @@ -2,7 +2,6 @@ @architect-pattern:SequenceDiagramGeneration @architect-status:roadmap @architect-implements:ArchitectureDiagramGeneration -@architect-phase:23 @architect-product-area:Process @architecture @future diff --git a/tests/planning-stubs/architecture/sequence-diagram.steps.ts b/tests/planning-stubs/architecture/sequence-diagram.steps.ts index 67f2487..8c59bec 100644 --- a/tests/planning-stubs/architecture/sequence-diagram.steps.ts +++ b/tests/planning-stubs/architecture/sequence-diagram.steps.ts @@ -42,14 +42,14 @@ describeFeature(feature, ({ Background, Rule }) => { 'a decider pattern {string} used by {string}', (_ctx: unknown, _decider: string, _handler: string) => { throw new Error('Not yet implemented: decider pattern relationship'); - } + }, ); Given( 'an event pattern {string} produced by {string}', (_ctx: unknown, _event: string, _decider: string) => { throw new Error('Not yet implemented: event pattern relationship'); - } + }, ); When('the sequence diagram is generated for {string}', (_ctx: unknown, _name: string) => { @@ -88,7 +88,7 @@ describeFeature(feature, ({ Background, Rule }) => { 'compensation for {string} is {string}', (_ctx: unknown, _step: string, _compensation: string) => { throw new Error('Not yet implemented: compensation mapping'); - } + }, ); When('the sequence diagram is generated for {string}', (_ctx: unknown, _name: string) => { diff --git a/tests/steps/api/architect-mcp-integration.steps.ts b/tests/steps/api/architect-mcp-integration.steps.ts index 870622e..2f394c9 100644 --- a/tests/steps/api/architect-mcp-integration.steps.ts +++ b/tests/steps/api/architect-mcp-integration.steps.ts @@ -3,8 +3,8 @@ import { expect } from 'vitest'; import { invokeTool, type RegisteredToolName, -} from '../../../../architect-mcp/src/tool-registry.js'; -import type { PipelineSessionManager } from '../../../../architect-mcp/src/pipeline-session.js'; +} from '../../../packages/architect-mcp/src/tool-registry.js'; +import type { PipelineSessionManager } from '../../../packages/architect-mcp/src/pipeline-session.js'; const feature = await loadFeature('tests/features/api/architect-mcp-integration.feature'); @@ -56,7 +56,7 @@ describeFeature(feature, ({ Rule }) => { expect(state!.error).not.toBeNull(); expect(state!.error?.message).toContain(expected); }); - } + }, ); RuleScenario( @@ -71,7 +71,7 @@ describeFeature(feature, ({ Rule }) => { expect(state!.error).not.toBeNull(); expect(state!.error?.message).toContain(expected); }); - } + }, ); RuleScenario( @@ -86,8 +86,8 @@ describeFeature(feature, ({ Rule }) => { expect(state!.error).not.toBeNull(); expect(state!.error?.message).toContain(expected); }); - } + }, ); - } + }, ); }); diff --git a/tests/steps/api/canonical-values-sync.steps.ts b/tests/steps/api/canonical-values-sync.steps.ts index dd7006b..2140e22 100644 --- a/tests/steps/api/canonical-values-sync.steps.ts +++ b/tests/steps/api/canonical-values-sync.steps.ts @@ -10,11 +10,8 @@ import { ARCHITECT_PACKAGE_PRODUCT_AREAS, ARCHITECT_PACKAGE_ROLES, CANONICAL_FEATURE_ONLY_TAG_SUFFIXES, - CANONICAL_PHASE_NAMES, - CANONICAL_PHASE_ORDINALS, DELIVERABLE_STATUS_VALUES, FORMAT_TYPES, - QUARTER_PATTERN, VALID_TRANSITIONS, parseFeatureFile, parseMarkdownTableRows, @@ -22,7 +19,7 @@ import { const adrPath = resolve( __dirname, - '../../../architect/decisions/adr-001-taxonomy-canonical-values.feature' + '../../../architect/decisions/adr-001-taxonomy-canonical-values.feature', ); function findRule(ruleName: string): { description: string } { @@ -44,7 +41,7 @@ function extractColumn(ruleName: string, columnName: string): string[] { } const feature = await loadFeature( - resolve(__dirname, '../../features/api/canonical-values-sync.feature') + resolve(__dirname, '../../features/api/canonical-values-sync.feature'), ); describeFeature(feature, ({ Rule }) => { @@ -68,7 +65,7 @@ describeFeature(feature, ({ Rule }) => { Then('both product-area lists contain the same values', () => { expect([...adrValues].sort()).toEqual([...constantValues].sort()); }); - } + }, ); }); @@ -92,7 +89,7 @@ describeFeature(feature, ({ Rule }) => { Then('both adr-category lists contain the same values', () => { expect([...adrValues].sort()).toEqual([...constantValues].sort()); }); - } + }, ); }); @@ -116,7 +113,7 @@ describeFeature(feature, ({ Rule }) => { Then('both status lists contain the same values', () => { expect([...adrValues].sort()).toEqual([...constantValues].sort()); }); - } + }, ); }); @@ -138,14 +135,14 @@ describeFeature(feature, ({ Rule }) => { And('I list the pairs in VALID_TRANSITIONS', () => { constantPairs = Object.entries(VALID_TRANSITIONS).flatMap(([from, tos]) => - tos.map((to) => `${from}->${to}`) + tos.map((to) => `${from}->${to}`), ); }); Then('both transition pair lists contain the same pairs', () => { expect([...adrPairs].sort()).toEqual([...constantPairs].sort()); }); - } + }, ); }); @@ -169,7 +166,7 @@ describeFeature(feature, ({ Rule }) => { Then('both format-type lists contain the same values', () => { expect([...adrValues].sort()).toEqual([...constantValues].sort()); }); - } + }, ); }); @@ -199,75 +196,14 @@ describeFeature(feature, ({ Rule }) => { Then('both canonical feature-only tag lists contain the same values', () => { expect([...adrTags].sort()).toEqual([...constantTags].sort()); }); - } + }, ); - } + }, ); - Rule('ADR-001 Rule 7 quarter format regex matches QUARTER_PATTERN', ({ RuleScenario }) => { - RuleScenario("QUARTER_PATTERN encodes ADR-001 Rule 7's format", ({ Given, Then, And }) => { - Given('the QUARTER_PATTERN regex', () => {}); - - Then('it accepts the canonical example "2026-Q1"', () => { - expect(QUARTER_PATTERN.test('2026-Q1')).toBe(true); - }); - - And('it rejects the anti-pattern "Q1-2026"', () => { - expect(QUARTER_PATTERN.test('Q1-2026')).toBe(false); - }); - }); - }); - - Rule('ADR-001 Rule 8 phase names match CANONICAL_PHASE_NAMES', ({ RuleScenario }) => { - RuleScenario( - 'Phase names match between ADR-001 Rule 8 and CANONICAL_PHASE_NAMES', - ({ Given, When, And, Then }) => { - let adrNames: string[] = []; - let constantNames: string[] = []; - - Given('the ADR-001 canonical values feature file', () => {}); - - When('I extract the phase names from Rule 8', () => { - adrNames = extractColumn('Canonical phase definitions (6-phase USDP standard)', 'Phase'); - }); - - And('I list the names in CANONICAL_PHASE_NAMES', () => { - constantNames = [...CANONICAL_PHASE_NAMES]; - }); - - Then('both phase-name lists contain the same names', () => { - expect([...adrNames].sort()).toEqual([...constantNames].sort()); - }); - } - ); - }); - - Rule('ADR-001 Rule 8 phase ordinals match CANONICAL_PHASE_ORDINALS', ({ RuleScenario }) => { - RuleScenario( - 'Phase ordinals match between ADR-001 Rule 8 and CANONICAL_PHASE_ORDINALS', - ({ Given, When, And, Then }) => { - let adrOrdinals: number[] = []; - let constantOrdinals: number[] = []; - - Given('the ADR-001 canonical values feature file', () => {}); - - When('I extract the phase ordinals from Rule 8', () => { - adrOrdinals = extractColumn( - 'Canonical phase definitions (6-phase USDP standard)', - 'Order' - ).map((value) => Number.parseInt(value, 10)); - }); - - And('I list the ordinals in CANONICAL_PHASE_ORDINALS', () => { - constantOrdinals = [...CANONICAL_PHASE_ORDINALS]; - }); - - Then('both phase-ordinal lists contain the same ordinals', () => { - expect([...adrOrdinals].sort()).toEqual([...constantOrdinals].sort()); - }); - } - ); - }); + // ADR-001 Rule 7 (Quarter Format) and Rule 8 (6-phase USDP phase + // definitions) were retired per ADR-013, along with their QUARTER_PATTERN / + // CANONICAL_PHASE_NAMES / CANONICAL_PHASE_ORDINALS constants — sync rules removed. Rule('ADR-001 Rule 9 matches DELIVERABLE_STATUS_VALUES', ({ RuleScenario }) => { RuleScenario( @@ -289,7 +225,7 @@ describeFeature(feature, ({ Rule }) => { Then('both deliverable-status lists contain the same values', () => { expect([...adrValues].sort()).toEqual([...constantValues].sort()); }); - } + }, ); }); @@ -313,7 +249,7 @@ describeFeature(feature, ({ Rule }) => { Then('both lists contain the same tags', () => { expect([...adrTags].sort()).toEqual([...constantTags].sort()); }); - } + }, ); }); }); diff --git a/tests/steps/api/cli-mcp-documentation-parity.steps.ts b/tests/steps/api/cli-mcp-documentation-parity.steps.ts deleted file mode 100644 index b724245..0000000 --- a/tests/steps/api/cli-mcp-documentation-parity.steps.ts +++ /dev/null @@ -1,120 +0,0 @@ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { describeFeature, loadFeature } from '@amiceli/vitest-cucumber'; -import { expect } from 'vitest'; - -import { invokeTool } from '../../../../architect-mcp/src/tool-registry.js'; -import { PipelineSessionManager } from '../../../../architect-mcp/src/pipeline-session.js'; -import { runCLI } from '../../support/helpers/cli-runner.js'; - -const feature = await loadFeature('tests/features/api/cli-mcp-documentation-parity.feature'); - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const PACKAGE_HOST_ROOT = path.resolve(__dirname, '../..', '..'); - -interface DocumentationParityState { - sessionManager: PipelineSessionManager | null; - cliOutput: unknown; - mcpOutput: unknown; -} - -let state: DocumentationParityState | null = null; - -function initState(): DocumentationParityState { - return { - sessionManager: null, - cliOutput: null, - mcpOutput: null, - }; -} - -async function runDocumentationCli( - documentType: string, - options: { disclosure?: string; filter?: string } = {} -): Promise { - const args = ['--base-dir', '.', '--format', 'json', 'documentation', documentType]; - if (options.disclosure !== undefined) { - args.push('--disclosure', options.disclosure); - } - if (options.filter !== undefined) { - args.push('--filter', options.filter); - } - - const result = await runCLI('architect', args, { cwd: PACKAGE_HOST_ROOT }); - if (result.exitCode !== 0) { - throw new Error( - `architect documentation failed (${String(result.exitCode)}): ${result.stderr || result.stdout}` - ); - } - return JSON.parse(result.stdout) as unknown; -} - -describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { - AfterEachScenario(() => { - state = null; - }); - - Background(({ Given }) => { - Given('the package-hosted documentation parity fixture is initialized', async () => { - state = initState(); - state.sessionManager = new PipelineSessionManager(); - await state.sessionManager.initialize({ baseDir: PACKAGE_HOST_ROOT }); - }); - }); - - Rule( - 'CLI and MCP documentation boundaries serialize the same projection bundle', - ({ RuleScenarioOutline, RuleScenario }) => { - RuleScenarioOutline( - 'CLI and MCP produce identical JSON for a bundle', - ({ When, And, Then }, examples: Record) => { - const documentType = String(examples['docType']); - - When('I generate "" via the CLI documentation command as JSON', async () => { - state!.cliOutput = await runDocumentationCli(documentType); - }); - - And('I generate "" via the MCP architect_documentation tool', async () => { - const result = await invokeTool(state!.sessionManager!, 'architect_documentation', { - documentType, - }); - state!.mcpOutput = JSON.parse(result.text) as unknown; - }); - - Then('the two outputs deep-equal', () => { - expect(state!.cliOutput).toEqual(state!.mcpOutput); - }); - } - ); - - RuleScenario( - 'CLI and MCP produce identical JSON for filtered and disclosed business rules', - ({ When, And, Then }) => { - When( - 'I generate {string} via the CLI documentation command as JSON with disclosure {string} and filter {string}', - async (_ctx: unknown, documentType: string, disclosure: string, filter: string) => { - state!.cliOutput = await runDocumentationCli(documentType, { disclosure, filter }); - } - ); - - And( - 'I generate {string} via the MCP architect_documentation tool with disclosure {string} and completed-status filter', - async (_ctx: unknown, documentType: string, disclosure: string) => { - const result = await invokeTool(state!.sessionManager!, 'architect_documentation', { - documentType, - disclosure, - filter: { status: ['completed'] }, - }); - state!.mcpOutput = JSON.parse(result.text) as unknown; - } - ); - - Then('the two outputs deep-equal', () => { - expect(state!.cliOutput).toEqual(state!.mcpOutput); - }); - } - ); - } - ); -}); diff --git a/tests/steps/api/context-assembly/compact-text-renderer.steps.ts b/tests/steps/api/context-assembly/compact-text-renderer.steps.ts index f6b3961..c801e05 100644 --- a/tests/steps/api/context-assembly/compact-text-renderer.steps.ts +++ b/tests/steps/api/context-assembly/compact-text-renderer.steps.ts @@ -11,7 +11,7 @@ import { createPackageResolver, type ExtractedPattern } from '@libar-dev/archite import { type FileReadingList, - parseAndProjectDependencyTree, + parseAndProjectDependencyContext, parseAndProjectFileReadingList, parseAndProjectSessionContext, projectOverviewDigest, @@ -25,7 +25,7 @@ import { import { createTestPattern } from '../../../fixtures/pattern-factories.js'; const feature = await loadFeature( - 'tests/features/api/context-assembly/compact-text-renderer.feature' + 'tests/features/api/context-assembly/compact-text-renderer.feature', ); interface TestState { @@ -51,7 +51,7 @@ function createProjectionContext(graph: ProjectionContext['graph']): ProjectionC function renderSessionContext( patterns: ExtractedPattern[], - sessionType: 'design' | 'implement' + sessionType: 'design' | 'implement', ): string { const dataset = createTestPatternGraph({ patterns }); const focalPattern = patterns.find((pattern) => pattern.implementsPatterns === undefined); @@ -63,18 +63,17 @@ function renderSessionContext( parseAndProjectSessionContext(createProjectionContext(dataset), { patterns: [focalPattern.patternName ?? focalPattern.name], sessionType, - }) + }), ); } -function renderDependencyTreeFor(patterns: ExtractedPattern[], pattern: string): string { +function renderDependencyContextFor(patterns: ExtractedPattern[], pattern: string): string { const dataset = createTestPatternGraph({ patterns }); return renderCompactText( - parseAndProjectDependencyTree(createProjectionContext(dataset), { + parseAndProjectDependencyContext(createProjectionContext(dataset), { pattern, maxDepth: 5, - includeImplementationDeps: true, - }) + }), ); } @@ -117,7 +116,6 @@ describeFeature(feature, ({ Rule }) => { createTestPattern({ name: 'OrderSaga', status: 'roadmap', - phase: 22, role: 'agent', filePath: 'architect/specs/order-saga.feature', description: 'Orchestrates order lifecycle.', @@ -145,7 +143,7 @@ describeFeature(feature, ({ Rule }) => { targetPath: 'src/domain/order-saga.ts', }), ], - 'design' + 'design', ); }); @@ -155,7 +153,7 @@ describeFeature(feature, ({ Rule }) => { for (const row of table) { expect(state!.output).toContain(row.section.trim()); } - } + }, ); }); @@ -170,7 +168,6 @@ describeFeature(feature, ({ Rule }) => { createTestPattern({ name: 'ProcessGuard', status: 'active', - phase: 14, role: 'validation', filePath: 'architect/specs/process-guard.feature', description: 'Validates delivery workflow.', @@ -185,7 +182,7 @@ describeFeature(feature, ({ Rule }) => { ], }), ], - 'implement' + 'implement', ); }); @@ -195,7 +192,7 @@ describeFeature(feature, ({ Rule }) => { for (const row of table) { expect(state!.output).toContain(row.section.trim()); } - } + }, ); And('the output contains checkbox markers', () => { @@ -204,32 +201,35 @@ describeFeature(feature, ({ Rule }) => { }); }); - Rule('formatDepTree renders indented tree', ({ RuleScenario }) => { - RuleScenario('Tree renders with arrows and focal marker', ({ Given, When, Then }) => { - Given('a dep-tree with root, middle, and focal leaf', () => { - state = initState(); - }); + Rule('formatDependencyContext renders a bidirectional focal view', ({ RuleScenario }) => { + RuleScenario( + 'Context renders the focal summary and bidirectional trees', + ({ Given, When, Then }) => { + Given('a dependency context with root, middle, and focal leaf', () => { + state = initState(); + }); - When('I format the tree', () => { - state!.output = renderDependencyTreeFor( - [ - createTestPattern({ name: 'Root', status: 'completed' }), - createTestPattern({ name: 'Middle', status: 'active', dependsOn: ['Root'] }), - createTestPattern({ name: 'Leaf', status: 'roadmap', dependsOn: ['Middle'] }), - ], - 'Leaf' - ); - }); + When('I format the dependency context', () => { + state!.output = renderDependencyContextFor( + [ + createTestPattern({ name: 'Root', status: 'completed' }), + createTestPattern({ name: 'Middle', status: 'active', dependsOn: ['Root'] }), + createTestPattern({ name: 'Leaf', status: 'roadmap', dependsOn: ['Middle'] }), + ], + 'Leaf', + ); + }); - Then( - 'the output contains all expected sections', - (_ctx: unknown, table: Array<{ section: string }>) => { - for (const row of table) { - expect(state!.output).toContain(row.section.trim()); - } - } - ); - }); + Then( + 'the output contains all expected sections', + (_ctx: unknown, table: Array<{ section: string }>) => { + for (const row of table) { + expect(state!.output).toContain(row.section.trim()); + } + }, + ); + }, + ); }); Rule('formatOverview renders progress summary', ({ RuleScenario }) => { @@ -239,7 +239,7 @@ describeFeature(feature, ({ Rule }) => { (_ctx: unknown, total: number, percent: number) => { state = initState(); state.output = renderOverview(total, percent); - } + }, ); When('I format the overview', () => {}); @@ -250,17 +250,17 @@ describeFeature(feature, ({ Rule }) => { for (const row of table) { expect(state!.output).toContain(row.section.trim()); } - } + }, ); }); - RuleScenario('Overview renders architect query guidance', ({ Given, When, Then, And }) => { + RuleScenario('Overview renders read-surface guidance', ({ Given, When, Then, And }) => { Given( 'an overview with {int} total patterns at {int} percent', (_ctx: unknown, total: number, percentage: number) => { state = initState(); state.output = renderOverview(total, percentage); - } + }, ); When('I format the overview', () => {}); @@ -317,7 +317,7 @@ describeFeature(feature, ({ Rule }) => { implementsPatterns: ['OrderSaga'], }), ], - 'OrderSaga' + 'OrderSaga', ); }); @@ -328,7 +328,7 @@ describeFeature(feature, ({ Rule }) => { And('the output contains {string}', (_ctx: unknown, text: string) => { expect(state!.output).toContain(text); }); - } + }, ); RuleScenario('Empty file reading list renders minimal output', ({ Given, When, Then }) => { diff --git a/tests/steps/api/output-shaping/output-pipeline.steps.ts b/tests/steps/api/output-shaping/output-pipeline.steps.ts index 02b5960..f6f704e 100644 --- a/tests/steps/api/output-shaping/output-pipeline.steps.ts +++ b/tests/steps/api/output-shaping/output-pipeline.steps.ts @@ -67,7 +67,7 @@ describeFeature(feature, ({ Background, Rule }) => { name: `Pattern${i}`, status: 'active', filePath: `src/p${i}.ts`, - }) + }), ); }); @@ -86,14 +86,14 @@ describeFeature(feature, ({ Background, Rule }) => { expect(item['patternName']).toBeDefined(); } }); - } + }, ); RuleScenario('Count modifier returns integer', ({ Given, When, Then }) => { Given('{int} patterns in the pipeline', (_ctx: unknown, count: number) => { state = initState(); state.patterns = Array.from({ length: count }, (_, i) => - createTestPattern({ name: `P${i}`, filePath: `src/p${i}.ts` }) + createTestPattern({ name: `P${i}`, filePath: `src/p${i}.ts` }), ); }); @@ -113,9 +113,9 @@ describeFeature(feature, ({ Background, Rule }) => { (_ctx: unknown, _count: number, a: string, b: string, c: string) => { state = initState(); state.patterns = [a, b, c].map((name) => - createTestPattern({ name, filePath: `src/${name.toLowerCase()}.ts` }) + createTestPattern({ name, filePath: `src/${name.toLowerCase()}.ts` }), ); - } + }, ); When('I apply the output pipeline with names-only modifier', () => { @@ -130,7 +130,7 @@ describeFeature(feature, ({ Background, Rule }) => { 'the output is an array of strings {string}, {string}, {string}', (_ctx: unknown, a: string, b: string, c: string) => { expect(state!.output).toEqual([a, b, c]); - } + }, ); }); @@ -142,7 +142,7 @@ describeFeature(feature, ({ Background, Rule }) => { name: `P${i}`, status: 'active', filePath: `src/p${i}.ts`, - }) + }), ); }); @@ -155,7 +155,7 @@ describeFeature(feature, ({ Background, Rule }) => { ...DEFAULT_OUTPUT_MODIFIERS, fields, }); - } + }, ); Then( @@ -165,7 +165,7 @@ describeFeature(feature, ({ Background, Rule }) => { const keys = Object.keys(item); expect(keys.sort()).toEqual([key1, key2].sort()); } - } + }, ); }); @@ -173,7 +173,7 @@ describeFeature(feature, ({ Background, Rule }) => { Given('{int} patterns in the pipeline', (_ctx: unknown, count: number) => { state = initState(); state.patterns = Array.from({ length: count }, (_, i) => - createTestPattern({ name: `P${i}`, filePath: `src/p${i}.ts` }) + createTestPattern({ name: `P${i}`, filePath: `src/p${i}.ts` }), ); }); @@ -215,7 +215,7 @@ describeFeature(feature, ({ Background, Rule }) => { name: `Pattern${i}`, status: 'active', filePath: `src/p${i}.ts`, - }) + }), ); }); @@ -228,7 +228,7 @@ describeFeature(feature, ({ Background, Rule }) => { ...DEFAULT_OUTPUT_MODIFIERS, fields, }); - } + }, ); Then('each result object has exactly {int} key', (_ctx: unknown, keyCount: number) => { @@ -236,7 +236,7 @@ describeFeature(feature, ({ Background, Rule }) => { expect(Object.keys(item).length).toBe(keyCount); } }); - } + }, ); }); @@ -284,7 +284,7 @@ describeFeature(feature, ({ Background, Rule }) => { } catch (e) { state.error = e instanceof Error ? e : new Error(String(e)); } - } + }, ); Then('validation fails with {string}', (_ctx: unknown, expected: string) => { @@ -323,18 +323,18 @@ describeFeature(feature, ({ Background, Rule }) => { name: `Active${i}`, status: 'active', filePath: `src/a${i}.ts`, - }) + }), ), ...Array.from({ length: roadmapCount }, (_, i) => createTestPattern({ name: `Roadmap${i}`, status: 'roadmap', filePath: `src/r${i}.ts`, - }) + }), ), ]; state.dataset = createTestPatternGraph({ patterns }); - } + }, ); When('I apply list filters with status {string}', (_ctx: unknown, status: string) => { @@ -375,7 +375,7 @@ describeFeature(feature, ({ Background, Rule }) => { }), ]; state.dataset = createTestPatternGraph({ patterns }); - } + }, ); When( @@ -386,7 +386,7 @@ describeFeature(feature, ({ Background, Rule }) => { status, role, }); - } + }, ); Then('only core patterns are returned', () => { @@ -411,7 +411,7 @@ describeFeature(feature, ({ Background, Rule }) => { name: `Roadmap${i}`, status: 'roadmap', filePath: `src/r${i}.ts`, - }) + }), ); state.dataset = createTestPatternGraph({ patterns }); }); @@ -424,7 +424,7 @@ describeFeature(feature, ({ Background, Rule }) => { limit, offset, }); - } + }, ); Then( @@ -435,7 +435,7 @@ describeFeature(feature, ({ Background, Rule }) => { // Verify the offset is correct — patterns are named Roadmap0..Roadmap9 const firstPattern = results[0]!; expect(firstPattern.name).toBe(`Roadmap${startIndex}`); - } + }, ); }); @@ -447,7 +447,7 @@ describeFeature(feature, ({ Background, Rule }) => { name: `Roadmap${i}`, status: 'roadmap', filePath: `src/r${i}.ts`, - }) + }), ); state.dataset = createTestPatternGraph({ patterns }); }); @@ -461,7 +461,7 @@ describeFeature(feature, ({ Background, Rule }) => { limit, offset, }); - } + }, ); Then('{int} patterns are returned', (_ctx: unknown, count: number) => { diff --git a/tests/steps/cli/data-api-cache.steps.ts b/tests/steps/cli/data-api-cache.steps.ts deleted file mode 100644 index 7faa20a..0000000 --- a/tests/steps/cli/data-api-cache.steps.ts +++ /dev/null @@ -1,219 +0,0 @@ -/** - * Data API CLI Cache Step Definitions - * - * BDD step definitions for testing PatternGraph caching - * between CLI invocations: cache hits, mtime invalidation, - * and --no-cache bypass. - * - * @architect - * @architect-implements DataAPICLIErgonomics - */ - -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { loadFeature, describeFeature } from '@amiceli/vitest-cucumber'; -import { expect } from 'vitest'; -import { - type CLITestState, - type CLIResult, - initState, - getTempDir, - runCLICommand, - getResult, - writePatternFiles, - createTempDir, -} from '../../support/helpers/pattern-graph-api-state.js'; - -// ============================================================================= -// Extended State for Cache Tests -// ============================================================================= - -interface CacheTestState extends CLITestState { - firstResult: CLIResult | null; - secondResult: CLIResult | null; -} - -function initCacheState(): CacheTestState { - const base = initState(); - return { - ...base, - firstResult: null, - secondResult: null, - }; -} - -function getCacheState(state: CacheTestState | null): CacheTestState { - if (!state) throw new Error('Cache test state not initialized'); - return state; -} - -// ============================================================================= -// JSON Metadata Parsing -// ============================================================================= - -interface ParsedMetadata { - cache?: { - hit: boolean; - ageMs?: number; - }; - pipelineMs?: number; -} - -function parseMetadata(result: CLIResult): ParsedMetadata { - const parsed = JSON.parse(result.stdout) as { metadata?: ParsedMetadata }; - if (!parsed.metadata) { - throw new Error('No metadata in response JSON'); - } - return parsed.metadata; -} - -// ============================================================================= -// Module-level state (reset per scenario) -// ============================================================================= - -let state: CacheTestState | null = null; -const CACHE_QUERY_TIMEOUT_MS = 120000; - -// ============================================================================= -// Feature Definition -// ============================================================================= - -const feature = await loadFeature('tests/features/cli/data-api-cache.feature'); - -describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { - // --------------------------------------------------------------------------- - // Cleanup - // --------------------------------------------------------------------------- - - AfterEachScenario(async () => { - if (state?.tempContext) { - await state.tempContext.cleanup(); - } - state = null; - }); - - // --------------------------------------------------------------------------- - // Background - // --------------------------------------------------------------------------- - - Background(({ Given }) => { - Given('a temporary working directory', async () => { - state = initCacheState(); - state.tempContext = await createTempDir({ prefix: 'cli-cache-test-' }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: PatternGraph is cached between invocations - // --------------------------------------------------------------------------- - - Rule('PatternGraph is cached between invocations', ({ RuleScenario }) => { - RuleScenario('Second query uses cached dataset', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running status and capturing the first result', async () => { - await runCLICommand(state, "pattern-graph-cli -i 'src/**/*.ts' query getStatusCounts", { - timeout: CACHE_QUERY_TIMEOUT_MS, - }); - getCacheState(state).firstResult = getResult(state); - }); - - And('running status and capturing the second result', async () => { - // Reset result before the second run - getCacheState(state).result = null; - await runCLICommand(state, "pattern-graph-cli -i 'src/**/*.ts' query getStatusCounts", { - timeout: CACHE_QUERY_TIMEOUT_MS, - }); - getCacheState(state).secondResult = getResult(state); - }); - - Then('the second result metadata has cache.hit true', () => { - const s = getCacheState(state); - const metadata = parseMetadata(s.secondResult!); - expect(metadata.cache).toBeDefined(); - expect(metadata.cache!.hit).toBe(true); - }); - - And('both results report pipeline timing metadata', () => { - const s = getCacheState(state); - const firstMetadata = parseMetadata(s.firstResult!); - const secondMetadata = parseMetadata(s.secondResult!); - expect(firstMetadata.pipelineMs).toBeDefined(); - expect(secondMetadata.pipelineMs).toBeDefined(); - expect(firstMetadata.pipelineMs!).toBeGreaterThanOrEqual(0); - expect(secondMetadata.pipelineMs!).toBeGreaterThanOrEqual(0); - }); - }); - - RuleScenario('Cache invalidated on source file change', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running status and capturing the first result', async () => { - await runCLICommand(state, "pattern-graph-cli -i 'src/**/*.ts' query getStatusCounts", { - timeout: CACHE_QUERY_TIMEOUT_MS, - }); - getCacheState(state).firstResult = getResult(state); - }); - - And('a source file mtime is updated', () => { - const dir = getTempDir(state); - const filePath = path.join(dir, 'src', 'completed.ts'); - // Advance mtime by 2 seconds to ensure cache key changes - const now = new Date(); - const future = new Date(now.getTime() + 2000); - fs.utimesSync(filePath, future, future); - }); - - And('running status and capturing the second result', async () => { - getCacheState(state).result = null; - await runCLICommand(state, "pattern-graph-cli -i 'src/**/*.ts' query getStatusCounts", { - timeout: CACHE_QUERY_TIMEOUT_MS, - }); - getCacheState(state).secondResult = getResult(state); - }); - - Then('the second result metadata has cache.hit false', () => { - const s = getCacheState(state); - const metadata = parseMetadata(s.secondResult!); - expect(metadata.cache).toBeDefined(); - expect(metadata.cache!.hit).toBe(false); - }); - }); - - RuleScenario('No-cache flag bypasses cache', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running status and capturing the first result', async () => { - await runCLICommand(state, "pattern-graph-cli -i 'src/**/*.ts' query getStatusCounts", { - timeout: CACHE_QUERY_TIMEOUT_MS, - }); - getCacheState(state).firstResult = getResult(state); - }); - - And('running status with --no-cache and capturing the second result', async () => { - getCacheState(state).result = null; - await runCLICommand( - state, - "pattern-graph-cli -i 'src/**/*.ts' --no-cache query getStatusCounts", - { - timeout: CACHE_QUERY_TIMEOUT_MS, - } - ); - getCacheState(state).secondResult = getResult(state); - }); - - Then('the second result metadata has cache.hit false', () => { - const s = getCacheState(state); - const metadata = parseMetadata(s.secondResult!); - expect(metadata.cache).toBeDefined(); - expect(metadata.cache!.hit).toBe(false); - }); - }); - }); -}); diff --git a/tests/steps/cli/data-api-dryrun.steps.ts b/tests/steps/cli/data-api-dryrun.steps.ts deleted file mode 100644 index c955310..0000000 --- a/tests/steps/cli/data-api-dryrun.steps.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Data API CLI Dry Run Step Definitions - * - * BDD step definitions for testing --dry-run mode: - * pipeline scope display without processing. - * - * @architect - * @architect-implements DataAPICLIErgonomics - */ - -import { loadFeature, describeFeature } from '@amiceli/vitest-cucumber'; -import { expect } from 'vitest'; -import { writeTempFile } from '../../support/helpers/file-system.js'; -import { - type CLITestState, - initState, - getResult, - runCLICommand, - writePatternFiles, - createTempDir, -} from '../../support/helpers/pattern-graph-api-state.js'; - -// ============================================================================= -// Module-level state (reset per scenario) -// ============================================================================= - -let state: CLITestState | null = null; - -// ============================================================================= -// Feature Definition -// ============================================================================= - -const feature = await loadFeature('tests/features/cli/data-api-dryrun.feature'); - -function createJsProjectConfig(): string { - return `export default { - sources: { - typescript: ['src/**/*.ts'] - } -}; -`; -} - -describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { - // --------------------------------------------------------------------------- - // Cleanup - // --------------------------------------------------------------------------- - - AfterEachScenario(async () => { - if (state?.tempContext) { - await state.tempContext.cleanup(); - } - state = null; - }); - - // --------------------------------------------------------------------------- - // Background - // --------------------------------------------------------------------------- - - Background(({ Given }) => { - Given('a temporary working directory', async () => { - state = initState(); - state.tempContext = await createTempDir({ prefix: 'cli-dryrun-test-' }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: Dry-run shows pipeline scope without processing - // --------------------------------------------------------------------------- - - Rule('Dry-run shows pipeline scope without processing', ({ RuleScenario }) => { - RuleScenario('Dry-run shows file counts', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains dry run marker, file counts, config, and cache status', () => { - const stdout = getResult(state).stdout; - expect(stdout).toContain('DRY RUN'); - expect(stdout).toContain('TypeScript files:'); - expect(stdout).toContain('Config:'); - expect(stdout).toContain('Cache:'); - }); - - And('stdout does not contain {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).not.toContain(text); - }); - }); - - RuleScenario( - 'Dry-run reports architect.config.js auto-detection', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('an architect.config.js with TypeScript sources', async () => { - await writeTempFile( - state!.tempContext!.tempDir, - 'architect.config.js', - createJsProjectConfig() - ); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - - And('stdout does not contain {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).not.toContain(text); - }); - } - ); - }); -}); diff --git a/tests/steps/cli/data-api-help.steps.ts b/tests/steps/cli/data-api-help.steps.ts deleted file mode 100644 index 30e2d92..0000000 --- a/tests/steps/cli/data-api-help.steps.ts +++ /dev/null @@ -1,320 +0,0 @@ -/** - * Data API CLI Per-Subcommand Help Step Definitions - * - * BDD step definitions for testing per-subcommand help output, - * global help, public command/flag inventory, and the frozen - * `--format json` behavior for text-oriented subcommands. - * - * @architect - * @architect-implements DataAPICLIErgonomics - */ - -import { loadFeature, describeFeature } from '@amiceli/vitest-cucumber'; -import { expect } from 'vitest'; -import { - type CLITestState, - initState, - getResult, - runCLICommand, - createTempDir, - writeArchPatternFilesWithDeps, - writeParentHierarchyFeatureFiles, - writePatternFiles, -} from '../../support/helpers/pattern-graph-api-state.js'; - -const FROZEN_COMMAND_INVENTORY = [ - 'overview', - 'status', - 'context [--session planning|design|implement]', - 'dep-tree [--depth ]', - 'files [--related]', - 'scope-validate [--type ] [--strict]', - 'handoff --pattern [--session planning|design|implement|review] [--modified-file ]...', - 'query [args...]', - 'pattern ', - 'documentation [--disclosure ] [--filter ]...', - 'bundle [--mode ] [--include ] [--estimate-tokens]', - 'list [--status ] [--role ] [--parent ] [--count] [--names-only]', - 'open-questions [--parent ]', - 'search ', - 'arch roles|bounded-context [name]|neighborhood |compare |coverage|dangling [--baseline ] [--write-baseline] [--strict]|orphans|blocking', - 'rules [--product-area ] [--pattern ] [--package ] [--feature ] [--only-invariants] [--count] [--names-only]', - 'diagnostics', - 'tags', - 'taxonomy [--count]', - 'sources', - 'unannotated', - 'repl', - 'help', - 'version', -] as const; - -const FROZEN_GLOBAL_FLAGS = [ - '-b, --base-dir Base directory (default: cwd)', - '-i, --input TypeScript source glob (repeatable)', - '-f, --feature Gherkin feature glob (repeatable)', - '--dry-run Show resolved inputs without running the pipeline', - '--no-cache Bypass CLI cache metadata tracking', - '--session planning, design, or implement', - '--depth Dependency tree depth', - '-h, --help Show help', - '-v, --version Show version', -] as const; - -interface FrozenFormatJsonResult { - readonly command: string; - readonly expectedKind: string; - readonly exitCode: number; - readonly parsed: Record; - readonly expectedDataKeys?: readonly string[]; -} - -interface HelpTestState extends CLITestState { - formatJsonResults: FrozenFormatJsonResult[]; -} - -function initHelpState(): HelpTestState { - return { - ...initState(), - formatJsonResults: [], - }; -} - -function getHelpState(current: HelpTestState | null): HelpTestState { - if (current === null) { - throw new Error('Help test state not initialized'); - } - return current; -} - -function extractSectionLines( - stdout: string, - sectionHeading: string, - nextHeading?: string -): string[] { - const startMarker = `${sectionHeading}\n`; - const startIndex = stdout.indexOf(startMarker); - if (startIndex === -1) { - throw new Error(`Could not find section ${sectionHeading}`); - } - - const sectionStart = startIndex + startMarker.length; - const sectionText = - nextHeading === undefined - ? stdout.slice(sectionStart) - : stdout.slice(sectionStart, stdout.indexOf(`\n${nextHeading}\n`, sectionStart)); - - return sectionText - .split('\n') - .map((line) => line.trimEnd()) - .filter((line) => line.trim().length > 0) - .map((line) => line.replace(/^\s+/, '')); -} - -let state: HelpTestState | null = null; - -const feature = await loadFeature('tests/features/cli/data-api-help.feature'); - -describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { - AfterEachScenario(async () => { - if (state?.tempContext) { - await state.tempContext.cleanup(); - } - state = null; - }); - - Background(({ Given }) => { - Given('a temporary working directory', async () => { - state = initHelpState(); - state.tempContext = await createTempDir({ prefix: 'cli-help-test-' }); - }); - }); - - Rule('Per-subcommand help shows usage and flags', ({ RuleScenario }) => { - RuleScenario('Per-subcommand help for context', ({ When, Then, And }) => { - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains context usage and session flag', () => { - const stdout = getResult(state).stdout; - expect(stdout).toContain('context'); - expect(stdout).toContain('--session'); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - }); - - RuleScenario('Global help still works', ({ When, Then, And }) => { - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - }); - - RuleScenario( - 'Global help lists the frozen public command and flag inventory', - ({ When, Then, And }) => { - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('global help lists the frozen command inventory', () => { - const lines = extractSectionLines( - getResult(state).stdout, - 'Commands:', - 'Global options:' - ); - expect(lines).toEqual(FROZEN_COMMAND_INVENTORY); - }); - - And('global help lists the frozen notable flags', () => { - const lines = extractSectionLines(getResult(state).stdout, 'Global options:'); - expect(lines).toEqual(FROZEN_GLOBAL_FLAGS); - }); - } - ); - - RuleScenario('Unknown subcommand help', ({ When, Then, And }) => { - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - }); - - RuleScenario( - 'Structured subcommands accept the public --format json flag', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('TypeScript files with architecture annotations and dependencies', async () => { - await writeArchPatternFilesWithDeps(state); - await writeParentHierarchyFeatureFiles(state); - }); - - When('running the frozen "--format json" contract command set', async () => { - const commands: ReadonlyArray<{ - command: string; - expectedKind: string; - expectedDataKeys?: readonly string[]; - }> = [ - { - command: "pattern-graph-cli -i 'src/**/*.ts' --format json overview", - expectedKind: 'OverviewDigest', - }, - { - command: - "pattern-graph-cli -i 'src/**/*.ts' --format json context CompletedPattern --session implement", - expectedKind: 'SessionContextBundle', - }, - { - command: - "pattern-graph-cli -i 'src/**/*.ts' --format json files CompletedPattern --related", - expectedKind: 'FileReadingList', - }, - { - command: - "pattern-graph-cli -i 'src/**/*.ts' --format json scope-validate CompletedPattern implement --strict", - expectedKind: 'ScopeReadinessReport', - }, - { - command: - "pattern-graph-cli -i 'src/**/*.ts' --format json handoff --pattern CompletedPattern --session review", - expectedKind: 'HandoffRecord', - }, - { - command: - "pattern-graph-cli -i 'src/**/*.ts' --format json dep-tree ContextFormatterImpl --depth 2", - expectedKind: 'DependencyTree', - }, - { - command: "pattern-graph-cli -i 'src/**/*.ts' --format json arch bounded-context api", - expectedKind: 'BoundedContext', - expectedDataKeys: ['children', 'root'], - }, - { - command: "pattern-graph-cli -i 'src/**/*.ts' --format json open-questions", - expectedKind: 'OpenQuestionList', - }, - { - command: - "pattern-graph-cli -i 'src/**/*.ts' -f 'tests/features/**/*.feature' --format json bundle ParentEpic --include rules,scenarios,deps,open-questions", - expectedKind: 'PatternBundleEntry', - }, - ]; - - const helpState = getHelpState(state); - helpState.formatJsonResults = []; - - for (const entry of commands) { - await runCLICommand(helpState, entry.command); - const result = getResult(helpState); - const expectedDataKeys = - 'expectedDataKeys' in entry ? entry.expectedDataKeys : undefined; - - helpState.formatJsonResults.push({ - command: entry.command, - expectedKind: entry.expectedKind, - exitCode: result.exitCode, - parsed: JSON.parse(result.stdout) as Record, - ...(expectedDataKeys !== undefined ? { expectedDataKeys } : {}), - }); - } - }); - - Then('every frozen "--format json" command exits with code 0', () => { - for (const result of getHelpState(state).formatJsonResults) { - expect(result.exitCode, result.command).toBe(0); - } - }); - - And('every frozen "--format json" command returns structured JSON', () => { - for (const result of getHelpState(state).formatJsonResults) { - const topLevelKind = result.parsed['kind']; - const rootKind = (result.parsed['root'] as { kind?: unknown } | undefined)?.kind; - const dataRootKind = ( - result.parsed['data'] as { root?: { kind?: unknown } } | undefined - )?.root?.kind; - - expect(topLevelKind ?? rootKind ?? dataRootKind, result.command).toBe( - result.expectedKind - ); - - if (result.expectedDataKeys !== undefined) { - expect( - Object.keys(result.parsed['data'] as Record), - result.command - ).toEqual(result.expectedDataKeys); - } - } - }); - } - ); - }); -}); diff --git a/tests/steps/cli/data-api-metadata.steps.ts b/tests/steps/cli/data-api-metadata.steps.ts deleted file mode 100644 index 2e8c30c..0000000 --- a/tests/steps/cli/data-api-metadata.steps.ts +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Data API CLI Metadata Step Definitions - * - * BDD step definitions for testing response metadata: - * validation summary counts, pipeline timing, and the - * frozen QuerySuccess envelope contract. - * - * @architect - * @architect-implements DataAPICLIErgonomics - */ - -import { loadFeature, describeFeature } from '@amiceli/vitest-cucumber'; -import { expect } from 'vitest'; -import { - type CLITestState, - initState, - getResult, - runCLICommand, - writePatternFiles, - createTempDir, -} from '../../support/helpers/pattern-graph-api-state.js'; - -interface ValidationMetadata { - danglingReferenceCount: number; - malformedPatternCount: number; - unknownStatusCount: number; - warningCount: number; -} - -interface ResponseMetadata { - validation?: ValidationMetadata; - pipelineMs?: number; - cache?: { - hit: boolean; - ageMs?: number; - }; - timestamp?: string; - patternCount?: number; -} - -interface ResponseEnvelope { - success?: boolean; - data?: unknown; - metadata?: ResponseMetadata; -} - -function parseResponseEnvelope(stdout: string): ResponseEnvelope { - return JSON.parse(stdout) as ResponseEnvelope; -} - -function parseResponseMetadata(stdout: string): ResponseMetadata { - const parsed = parseResponseEnvelope(stdout); - if (!parsed.metadata) { - throw new Error('No metadata in response JSON'); - } - return parsed.metadata; -} - -let state: CLITestState | null = null; - -const feature = await loadFeature('tests/features/cli/data-api-metadata.feature'); - -describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { - AfterEachScenario(async () => { - if (state?.tempContext) { - await state.tempContext.cleanup(); - } - state = null; - }); - - Background(({ Given }) => { - Given('a temporary working directory', async () => { - state = initState(); - state.tempContext = await createTempDir({ prefix: 'cli-metadata-test-' }); - }); - }); - - Rule('Response metadata includes validation summary', ({ RuleScenario }) => { - RuleScenario('Validation summary in response metadata', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON with key {string}', (_ctx: unknown, key: string) => { - const parsed = parseResponseEnvelope(getResult(state).stdout) as Record; - expect(parsed).toHaveProperty(key); - }); - - And('metadata has a validation object with count fields', () => { - const metadata = parseResponseMetadata(getResult(state).stdout); - expect(metadata.validation).toBeDefined(); - expect(typeof metadata.validation!.danglingReferenceCount).toBe('number'); - expect(typeof metadata.validation!.malformedPatternCount).toBe('number'); - expect(typeof metadata.validation!.unknownStatusCount).toBe('number'); - expect(typeof metadata.validation!.warningCount).toBe('number'); - }); - - And('metadata has a numeric pipelineMs field', () => { - const metadata = parseResponseMetadata(getResult(state).stdout); - expect(metadata.pipelineMs).toBeDefined(); - expect(typeof metadata.pipelineMs).toBe('number'); - expect(metadata.pipelineMs!).toBeGreaterThanOrEqual(0); - }); - }); - - RuleScenario('Pipeline timing in metadata', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('metadata has a numeric pipelineMs field', () => { - const metadata = parseResponseMetadata(getResult(state).stdout); - expect(metadata.pipelineMs).toBeDefined(); - expect(typeof metadata.pipelineMs).toBe('number'); - expect(metadata.pipelineMs!).toBeGreaterThanOrEqual(0); - }); - }); - - RuleScenario( - 'QuerySuccess envelope preserves the structured JSON contract', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON with key {string}', (_ctx: unknown, key: string) => { - const parsed = parseResponseEnvelope(getResult(state).stdout) as Record; - expect(parsed).toHaveProperty(key); - }); - - And('response has success true', () => { - const parsed = parseResponseEnvelope(getResult(state).stdout); - expect(parsed.success).toBe(true); - }); - - And('metadata has an ISO timestamp field', () => { - const metadata = parseResponseMetadata(getResult(state).stdout); - expect(typeof metadata.timestamp).toBe('string'); - expect(Number.isNaN(Date.parse(metadata.timestamp!))).toBe(false); - }); - - And('metadata has a numeric patternCount field', () => { - const metadata = parseResponseMetadata(getResult(state).stdout); - expect(typeof metadata.patternCount).toBe('number'); - expect(metadata.patternCount).toBe(3); - }); - } - ); - }); -}); diff --git a/tests/steps/cli/data-api-repl.steps.ts b/tests/steps/cli/data-api-repl.steps.ts deleted file mode 100644 index 1bb57c5..0000000 --- a/tests/steps/cli/data-api-repl.steps.ts +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Data API CLI REPL Step Definitions - * - * BDD step definitions for testing the interactive REPL mode: - * multi-query sessions, help output, and pipeline reload. - * - * @architect - * @architect-implements DataAPICLIErgonomics - */ - -import { loadFeature, describeFeature } from '@amiceli/vitest-cucumber'; -import { expect } from 'vitest'; -import { - type CLITestState, - type CLIResult, - initState, - writePatternFiles, - createTempDir, -} from '../../support/helpers/pattern-graph-api-state.js'; -import { runCLI } from '../../support/helpers/cli-runner.js'; - -// ============================================================================= -// Extended State for REPL Tests -// ============================================================================= - -interface ReplTestState extends CLITestState { - replResult: CLIResult | null; -} - -function initReplState(): ReplTestState { - const base = initState(); - return { - ...base, - replResult: null, - }; -} - -function getReplState(state: ReplTestState | null): ReplTestState { - if (!state) throw new Error('REPL test state not initialized'); - return state; -} - -function getTempDir(state: ReplTestState | null): string { - const s = getReplState(state); - if (!s.tempContext) throw new Error('Temp context not initialized'); - return s.tempContext.tempDir; -} - -function getReplResult(state: ReplTestState | null): CLIResult { - const s = getReplState(state); - if (!s.replResult) throw new Error('REPL result not available'); - return s.replResult; -} - -// ============================================================================= -// REPL Runner Helper -// ============================================================================= - -async function runRepl(state: ReplTestState | null, commands: string[]): Promise { - const s = getReplState(state); - const stdinData = commands.join('\n') + '\n'; - s.replResult = await runCLI('pattern-graph-cli', ['-i', 'src/**/*.ts', 'repl'], { - cwd: getTempDir(state), - timeout: 30000, - stdin: stdinData, - }); -} - -// ============================================================================= -// Module-level state (reset per scenario) -// ============================================================================= - -let state: ReplTestState | null = null; - -// ============================================================================= -// Feature Definition -// ============================================================================= - -const feature = await loadFeature('tests/features/cli/data-api-repl.feature'); - -describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { - // --------------------------------------------------------------------------- - // Cleanup - // --------------------------------------------------------------------------- - - AfterEachScenario(async () => { - if (state?.tempContext) { - await state.tempContext.cleanup(); - } - state = null; - }); - - // --------------------------------------------------------------------------- - // Background - // --------------------------------------------------------------------------- - - Background(({ Given }) => { - Given('a temporary working directory', async () => { - state = initReplState(); - state.tempContext = await createTempDir({ prefix: 'cli-repl-test-' }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: REPL mode accepts multiple queries on a single pipeline load - // --------------------------------------------------------------------------- - - Rule('REPL mode accepts multiple queries on a single pipeline load', ({ RuleScenario }) => { - RuleScenario('REPL accepts multiple queries', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('piping "status" then "list" then "quit" to the REPL', async () => { - await runRepl(state, ['status', 'list', 'quit']); - }); - - Then('the REPL output contains status JSON', () => { - const result = getReplResult(state); - // status command now outputs a StatusDistribution projection fragment - expect(result.stdout).toContain('StatusDistribution'); - }); - - And('the REPL output contains list JSON', () => { - const result = getReplResult(state); - // list command outputs JSON with pattern names - expect(result.stdout).toContain('"RoadmapPattern"'); - }); - - And('the REPL exits cleanly', () => { - const result = getReplResult(state); - // REPL should exit with code 0 after quit - expect(result.exitCode).toBe(0); - }); - }); - - RuleScenario('REPL shows help output', ({ Given, When, Then }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('piping "help" then "quit" to the REPL', async () => { - await runRepl(state, ['help', 'quit']); - }); - - Then('the REPL output contains available commands', () => { - const result = getReplResult(state); - // help goes to stdout - expect(result.stdout).toContain('status'); - expect(result.stdout).toContain('context'); - expect(result.stdout).toContain('dep-tree'); - }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: REPL reload rebuilds the pipeline from fresh sources - // --------------------------------------------------------------------------- - - Rule('REPL reload rebuilds the pipeline from fresh sources', ({ RuleScenario }) => { - RuleScenario('REPL reloads pipeline on command', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('piping "status" then "reload" then "status" then "quit" to the REPL', async () => { - await runRepl(state, ['status', 'reload', 'status', 'quit']); - }); - - Then('the REPL stderr contains "Reloading pipeline"', () => { - const result = getReplResult(state); - expect(result.stderr).toContain('Reloading pipeline'); - }); - - And('the REPL stderr contains "Reloaded"', () => { - const result = getReplResult(state); - expect(result.stderr).toContain('Reloaded'); - }); - - And('the REPL output contains two status responses', () => { - const result = getReplResult(state); - // Both status commands produce StatusDistribution projection fragments - const matches = result.stdout.match(/StatusDistribution/g); - expect(matches).not.toBeNull(); - expect(matches!.length).toBeGreaterThanOrEqual(2); - }); - }); - }); -}); diff --git a/tests/steps/cli/generate-docs.steps.ts b/tests/steps/cli/generate-docs.steps.ts index 6437e02..be7d84a 100644 --- a/tests/steps/cli/generate-docs.steps.ts +++ b/tests/steps/cli/generate-docs.steps.ts @@ -7,7 +7,7 @@ */ import { loadFeature, describeFeature } from '@amiceli/vitest-cucumber'; -import { readFile } from 'node:fs/promises'; +import { readFile, writeFile } from 'node:fs/promises'; import { expect } from 'vitest'; import { createTempDir, @@ -90,6 +90,51 @@ function createReducedDocsConfigFile(): string { `; } +function createPackageMappedConfigFile(): string { + return `export default { + sources: { + typescript: ['src/**/*.ts'] + }, + packages: [ + { id: 'demo', displayName: 'Demo Package', match: 'src/' } + ] +}; +`; +} + +// An authored embedded host with NO managed-region markers — generation must fail +// loud rather than write. Deliberately free of "product-area" so the "left +// unwritten" assertion (the generated table never landed) is unambiguous. +function embeddedHostWithoutMarkers(): string { + return `# Authored Host + +Authored prose the projection must never overwrite. This host carries no +managed-region markers, so an embedded generator routed at it must fail loud. +`; +} + +// An authored embedded host with one or more empty marker-bounded regions. Generation +// fills each inter-marker span; the authored prose around them is preserved byte-for-byte. +// The host must carry EVERY region its generator writes — the formal-spec generator +// writes two (`taxonomy-classification` + `taxonomy-relationships`), and a host missing a +// routed region's markers fails loud (the engine's "host not region-prepared" guard). +function embeddedHostWithEmptyRegions(regionIds: readonly string[]): string { + const regions = regionIds + .map((id) => `\n`) + .join('\n\n'); + return `# Authored Host + +Authored prose above the region. + +${regions} + +Authored prose below the region. +`; +} + +/** The full region set the `taxonomy-formal-spec` generator writes into its host. */ +const FORMAL_SPEC_HOST_REGIONS = ['taxonomy-classification', 'taxonomy-relationships'] as const; + // ============================================================================= // Feature Definition // ============================================================================= @@ -235,7 +280,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { expect(getResult().stdout).toContain(row.text); } }); - } + }, ); }); @@ -249,7 +294,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} with pattern annotations', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createPatternFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -265,7 +310,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { async (_ctx: unknown, relativePath: string) => { const exists = await fileExists(getTempDir(), relativePath); expect(exists).toBe(true); - } + }, ); }); @@ -276,7 +321,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} with pattern annotations', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createPatternFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -292,7 +337,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { async (_ctx: unknown, relativePath: string) => { const exists = await fileExists(getTempDir(), relativePath); expect(exists).toBe(true); - } + }, ); And( @@ -304,9 +349,9 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { }; expect(manifest.generators?.[generatorName]?.rootPath).toBe(rootPath); - } + }, ); - } + }, ); RuleScenario( @@ -316,7 +361,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} with pattern annotations', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createPatternFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -330,7 +375,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { And('stdout contains {string}', (_ctx: unknown, text: string) => { expect(getResult().stdout).toContain(text); }); - } + }, ); RuleScenario('Generate docs with disclosure override', ({ Given, When, Then, And }) => { @@ -338,7 +383,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} with pattern annotations', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createPatternFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -354,7 +399,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { async (_ctx: unknown, relativePath: string) => { const exists = await fileExists(getTempDir(), relativePath); expect(exists).toBe(true); - } + }, ); }); @@ -363,14 +408,14 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} with completed pattern annotations', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createCompletedPatternFile()); - } + }, ); And( 'a TypeScript file {string} with active pattern annotations', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createActivePatternFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -386,7 +431,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { async (_ctx: unknown, relativePath: string, text: string) => { const content = await readFile(`${getTempDir()}/${relativePath}`, 'utf8'); expect(content).toContain(text); - } + }, ); And( @@ -394,7 +439,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { async (_ctx: unknown, relativePath: string, text: string) => { const content = await readFile(`${getTempDir()}/${relativePath}`, 'utf8'); expect(content).not.toContain(text); - } + }, ); }); @@ -408,14 +453,14 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} with completed pattern annotations', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createCompletedPatternFile()); - } + }, ); And( 'a TypeScript file {string} with active pattern annotations', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createActivePatternFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -431,7 +476,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { async (_ctx: unknown, relativePath: string, text: string) => { const content = await readFile(`${getTempDir()}/${relativePath}`, 'utf8'); expect(content).toContain(text); - } + }, ); And( @@ -439,9 +484,375 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { async (_ctx: unknown, relativePath: string, text: string) => { const content = await readFile(`${getTempDir()}/${relativePath}`, 'utf8'); expect(content).toContain(text); - } + }, ); }); + + RuleScenario( + '--all runs every registered generator plus index', + ({ Given, When, Then, And }) => { + Given('an architect.config.js mapping sources to a package', async () => { + await writeTempFile(getTempDir(), 'architect.config.js', createPackageMappedConfigFile()); + }); + + And( + 'a TypeScript file {string} with pattern annotations', + async (_ctx: unknown, relativePath: string) => { + await writeTempFile(getTempDir(), relativePath, createPatternFile()); + }, + ); + + When('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + Then('exit code is {int}', (_ctx: unknown, code: number) => { + expect(getResult().exitCode).toBe(code); + }); + + And( + 'the working directory contains files:', + async (_ctx: unknown, table: Array<{ path: string }>) => { + for (const row of table) { + const exists = await fileExists(getTempDir(), row.path); + expect(exists, `expected ${row.path} to be generated by --all`).toBe(true); + } + }, + ); + }, + ); + }); + + // --------------------------------------------------------------------------- + // Rule: CLI verifies determinism with --check + // --------------------------------------------------------------------------- + + Rule('CLI verifies determinism with --check', ({ RuleScenario }) => { + RuleScenario( + 'Check passes when generated docs match the working tree', + ({ Given, When, Then, And }) => { + Given( + 'a TypeScript file {string} with pattern annotations', + async (_ctx: unknown, relativePath: string) => { + await writeTempFile(getTempDir(), relativePath, createPatternFile()); + }, + ); + + // First run generates; the second run (--check) overwrites result and is + // what the assertions below observe. + When('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + And('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + Then('exit code is {int}', (_ctx: unknown, code: number) => { + expect(getResult().exitCode).toBe(code); + }); + + And('output contains {string}', (_ctx: unknown, text: string) => { + const combined = getResult().stdout + getResult().stderr; + expect(combined).toContain(text); + }); + }, + ); + + RuleScenario( + 'Check reports drift when a generated doc is absent', + ({ Given, When, Then, And }) => { + Given( + 'a TypeScript file {string} with pattern annotations', + async (_ctx: unknown, relativePath: string) => { + await writeTempFile(getTempDir(), relativePath, createPatternFile()); + }, + ); + + When('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + Then('exit code is {int}', (_ctx: unknown, code: number) => { + expect(getResult().exitCode).toBe(code); + }); + + And('output contains {string}', (_ctx: unknown, text: string) => { + const combined = getResult().stdout + getResult().stderr; + expect(combined).toContain(text); + }); + }, + ); + + RuleScenario( + 'Check reports drift when only the manifest is stale', + ({ Given, When, Then, And }) => { + Given( + 'a TypeScript file {string} with pattern annotations', + async (_ctx: unknown, relativePath: string) => { + await writeTempFile(getTempDir(), relativePath, createPatternFile()); + }, + ); + + // After generation the rendered docs are in sync; emptying the manifest + // leaves every .md byte-identical but makes the manifest stale — drift the + // files-only check would miss. + When('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + And( + 'the generated docs manifest in {string} is emptied', + async (_ctx: unknown, dir: string) => { + await writeTempFile( + getTempDir(), + `${dir}/.generated-docs-manifest.json`, + '{\n "version": 1,\n "generators": {}\n}\n', + ); + }, + ); + + And('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + Then('exit code is {int}', (_ctx: unknown, code: number) => { + expect(getResult().exitCode).toBe(code); + }); + + And('output contains {string}', (_ctx: unknown, text: string) => { + const combined = getResult().stdout + getResult().stderr; + expect(combined).toContain(text); + }); + }, + ); + }); + + // --------------------------------------------------------------------------- + // Rule: CLI generates and gates embedded-region hosts + // --------------------------------------------------------------------------- + + Rule('CLI generates and gates embedded-region hosts', ({ RuleScenario }) => { + RuleScenario( + 'An embedded host present but missing its markers fails loudly', + ({ Given, And, When, Then }) => { + Given( + 'a TypeScript file {string} with pattern annotations', + async (_ctx: unknown, relativePath: string) => { + await writeTempFile(getTempDir(), relativePath, createPatternFile()); + }, + ); + + And( + 'an embedded host {string} with no managed-region markers', + async (_ctx: unknown, relativePath: string) => { + await writeTempFile(getTempDir(), relativePath, embeddedHostWithoutMarkers()); + }, + ); + + When('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + Then('exit code is {int}', (_ctx: unknown, code: number) => { + expect(getResult().exitCode).toBe(code); + }); + + And('output contains all of:', (_ctx: unknown, table: Array<{ text: string }>) => { + const combined = getResult().stdout + getResult().stderr; + for (const row of table) { + expect(combined).toContain(row.text); + } + }); + }, + ); + + RuleScenario( + 'An embedded host absent in the project is skipped under --all', + ({ Given, And, When, Then }) => { + Given('an architect.config.js mapping sources to a package', async () => { + await writeTempFile(getTempDir(), 'architect.config.js', createPackageMappedConfigFile()); + }); + + And( + 'a TypeScript file {string} with pattern annotations', + async (_ctx: unknown, relativePath: string) => { + await writeTempFile(getTempDir(), relativePath, createPatternFile()); + }, + ); + + When('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + Then('exit code is {int}', (_ctx: unknown, code: number) => { + expect(getResult().exitCode).toBe(code); + }); + + And('output contains {string}', (_ctx: unknown, text: string) => { + expect(getResult().stdout + getResult().stderr).toContain(text); + }); + + And( + 'file {string} exists in working directory', + async (_ctx: unknown, relativePath: string) => { + expect(await fileExists(getTempDir(), relativePath)).toBe(true); + }, + ); + }, + ); + + RuleScenario( + 'An explicit -g request for an absent host fails loud', + ({ Given, When, Then, And }) => { + Given( + 'a TypeScript file {string} with pattern annotations', + async (_ctx: unknown, relativePath: string) => { + await writeTempFile(getTempDir(), relativePath, createPatternFile()); + }, + ); + + When('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + Then('exit code is {int}', (_ctx: unknown, code: number) => { + expect(getResult().exitCode).toBe(code); + }); + + And('output contains {string}', (_ctx: unknown, text: string) => { + expect(getResult().stdout + getResult().stderr).toContain(text); + }); + }, + ); + + RuleScenario( + 'A hand-edited region in an out-of-tree host fails the determinism gate', + ({ Given, And, When, Then }) => { + Given( + 'a TypeScript file {string} with pattern annotations', + async (_ctx: unknown, relativePath: string) => { + await writeTempFile(getTempDir(), relativePath, createPatternFile()); + }, + ); + + And( + 'an embedded host {string} with an empty {string} region', + async (_ctx: unknown, relativePath: string, regionId: string) => { + // The formal-spec host must carry every region its generator writes, not + // only the one the scenario names — else generation fails loud on the + // unprepared sibling region. + const regionIds = + relativePath === 'formal-spec/04-tag-registry.md' + ? FORMAL_SPEC_HOST_REGIONS + : [regionId]; + await writeTempFile( + getTempDir(), + relativePath, + embeddedHostWithEmptyRegions(regionIds), + ); + }, + ); + + // First run fills the region; the assertions observe the later --check run. + And('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + And( + 'the {string} region in {string} is hand-edited', + async (_ctx: unknown, regionId: string, relativePath: string) => { + const full = `${getTempDir()}/${relativePath}`; + const content = await readFile(full, 'utf8'); + const beginMarker = ``; + expect(content).toContain(beginMarker); + // Insert a line INSIDE the region (immediately after the begin marker), + // so a regeneration no longer matches — a region-scoped drift the gate + // must catch even though the host lives outside the output directory. + await writeFile( + full, + content.replace(beginMarker, `${beginMarker}\nHAND-EDITED DRIFT LINE`), + 'utf8', + ); + }, + ); + + When('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + Then('exit code is {int}', (_ctx: unknown, code: number) => { + expect(getResult().exitCode).toBe(code); + }); + + And('output contains all of:', (_ctx: unknown, table: Array<{ text: string }>) => { + const combined = getResult().stdout + getResult().stderr; + for (const row of table) { + expect(combined).toContain(row.text); + } + }); + }, + ); + + RuleScenario( + 'A validation failure aborts before any embedded host is committed', + ({ Given, And, When, Then }) => { + Given( + 'a TypeScript file {string} with pattern annotations', + async (_ctx: unknown, relativePath: string) => { + await writeTempFile(getTempDir(), relativePath, createPatternFile()); + }, + ); + + And( + 'an embedded host {string} with an empty {string} region', + async (_ctx: unknown, relativePath: string, regionId: string) => { + // The formal-spec host must carry every region its generator writes, not + // only the one the scenario names — else generation fails loud on the + // unprepared sibling region. + const regionIds = + relativePath === 'formal-spec/04-tag-registry.md' + ? FORMAL_SPEC_HOST_REGIONS + : [regionId]; + await writeTempFile( + getTempDir(), + relativePath, + embeddedHostWithEmptyRegions(regionIds), + ); + }, + ); + + And( + 'an embedded host {string} with no managed-region markers', + async (_ctx: unknown, relativePath: string) => { + await writeTempFile(getTempDir(), relativePath, embeddedHostWithoutMarkers()); + }, + ); + + When('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + Then('exit code is {int}', (_ctx: unknown, code: number) => { + expect(getResult().exitCode).toBe(code); + }); + + And( + 'the embedded host {string} was left unwritten by the failed run', + async (_ctx: unknown, relativePath: string) => { + const content = await readFile(`${getTempDir()}/${relativePath}`, 'utf8'); + // The failing sibling (no markers) throws during the RENDER phase, before + // the commit phase runs — so no host is ever written. This is the + // before-commit abort guarantee (NOT a rollback of in-progress renames): + // the generated table never landed, and the empty markers + authored prose + // are byte-intact. + expect(content).not.toContain('product-area'); + expect(content).toContain(''); + expect(content).toContain('Authored prose above the region.'); + }, + ); + }, + ); }); // --------------------------------------------------------------------------- diff --git a/tests/steps/cli/graph-handle.steps.ts b/tests/steps/cli/graph-handle.steps.ts new file mode 100644 index 0000000..485a6e3 --- /dev/null +++ b/tests/steps/cli/graph-handle.steps.ts @@ -0,0 +1,244 @@ +import { describeFeature, loadFeature } from '@amiceli/vitest-cucumber'; +import { Graph as CoreGraph } from '@libar-dev/architect-core/graph'; +import { expect } from 'vitest'; + +import { loadGraph } from '../../../packages/architect-cli/src/handle/graph.js'; +import { runCLI, type CLIResult } from '../../support/helpers/cli-runner.js'; +import { + EXPECTED_STRICT_DANGLING, + GRAPH_HANDLE_BATTERY_SCRIPT, + parseBattery, + parseMigratedHandle, + parseStrictDangling, +} from '../../support/helpers/graph-handle-contract.js'; + +const feature = await loadFeature('tests/features/cli/graph-handle.feature'); + +const GRAPH_CLI = 'graph-cli'; +const BASE = ['--base-dir', '.']; +let lastResult: CLIResult | null = null; +let composedGraph: CoreGraph | null = null; +let mutationResult: CLIResult | null = null; +let freshResult: CLIResult | null = null; +const battery = () => parseBattery((lastResult?.stdout ?? '').trim()); + +describeFeature(feature, ({ AfterEachScenario, Rule }) => { + AfterEachScenario(() => { + lastResult = null; + composedGraph = null; + mutationResult = null; + freshResult = null; + }); + + Rule('The q front door evaluates agent scripts against the live graph', ({ RuleScenario }) => { + RuleScenario('argv expression round-trips against the live graph', ({ When, Then, And }) => { + When('I run the graph CLI with q expression "g.pattern(\'GraphHandle\')?.name"', async () => { + lastResult = await runCLI(GRAPH_CLI, [...BASE, 'q', "g.pattern('GraphHandle')?.name"], { + timeout: 120000, + }); + }); + Then('the exit code is zero', () => { + expect(lastResult?.exitCode).toBe(0); + }); + And('stdout is "GraphHandle"', () => { + expect((lastResult?.stdout ?? '').trim()).toBe('GraphHandle'); + }); + }); + + RuleScenario('the CLI composes the public core Graph', ({ When, Then, And }) => { + When('I load the CLI graph composition', async () => { + composedGraph = await loadGraph(process.cwd()); + }); + Then('the handle is the public core Graph', () => { + expect(composedGraph).toBeInstanceOf(CoreGraph); + }); + And('the handle has no api field', () => { + expect(composedGraph === null ? undefined : 'api' in composedGraph).toBe(false); + }); + And('the canonical graph and FSM are frozen', () => { + expect(Object.isFrozen(composedGraph?.graph)).toBe(true); + expect(Object.isFrozen(composedGraph?.fsm)).toBe(true); + }); + And('deferred patterns have plan maturity', () => { + expect( + composedGraph?.patterns + .filter((pattern) => pattern.status === 'deferred') + .every((pattern) => pattern.maturity === 'plan'), + ).toBe(true); + }); + }); + + RuleScenario( + 'the migrated handle exposes canonical graph and FSM values', + ({ When, Then, And }) => { + When('I run the migrated handle characterization', async () => { + const script = `return JSON.stringify({ + hasApi: 'api' in g, + hasFsm: typeof g.fsm?.isValidTransition, + frozen: Object.isFrozen(g.graph), + deferred: g.patterns + .filter((p) => p.status === 'deferred') + .every((p) => p.maturity === 'plan'), +})`; + lastResult = await runCLI(GRAPH_CLI, [...BASE, 'q', script], { timeout: 120000 }); + }); + Then('the exit code is zero', () => { + expect(lastResult?.exitCode).toBe(0); + }); + And('the characterization reports api is absent', () => { + expect(parseMigratedHandle(lastResult?.stdout ?? '{}').hasApi).toBe(false); + }); + And('the characterization reports FSM is available', () => { + expect(parseMigratedHandle(lastResult?.stdout ?? '{}').hasFsm).toBe('function'); + }); + And('the characterization reports canonical graph is frozen', () => { + expect(parseMigratedHandle(lastResult?.stdout ?? '{}').frozen).toBe(true); + }); + And('the characterization reports deferred maturity is plan', () => { + expect(parseMigratedHandle(lastResult?.stdout ?? '{}').deferred).toBe(true); + }); + }, + ); + + RuleScenario('argv multi-statement body round-trips', ({ When, Then, And }) => { + When( + 'I run the graph CLI with q expression "const p = g.pattern(\'GraphHandle\'); return p?.name"', + async () => { + lastResult = await runCLI( + GRAPH_CLI, + [...BASE, 'q', "const p = g.pattern('GraphHandle'); return p?.name"], + { timeout: 120000 }, + ); + }, + ); + Then('the exit code is zero', () => { + expect(lastResult?.exitCode).toBe(0); + }); + And('stdout is "GraphHandle"', () => { + expect((lastResult?.stdout ?? '').trim()).toBe('GraphHandle'); + }); + }); + + RuleScenario('stdin script round-trips', ({ When, Then, And }) => { + When('I pipe a script returning the GraphHandle sentinel into the graph CLI', async () => { + lastResult = await runCLI(GRAPH_CLI, [...BASE, 'q'], { + timeout: 120000, + stdin: "return g.pattern('GraphHandle')?.name;\n", + }); + }); + Then('the exit code is zero', () => { + expect(lastResult?.exitCode).toBe(0); + }); + And('stdout is "GraphHandle"', () => { + expect((lastResult?.stdout ?? '').trim()).toBe('GraphHandle'); + }); + }); + + RuleScenario('the removed api field fails loud', ({ When, Then, And }) => { + When('I run the graph CLI with q expression "return g.api.getStatusCounts()"', async () => { + lastResult = await runCLI(GRAPH_CLI, [...BASE, 'q', 'return g.api.getStatusCounts()'], { + timeout: 120000, + }); + }); + Then('the exit code is non-zero', () => { + expect(lastResult?.exitCode).not.toBe(0); + }); + And('stderr mentions "getStatusCounts"', () => { + expect(lastResult?.stderr ?? '').toContain('getStatusCounts'); + }); + }); + + RuleScenario('canonical graph mutation cannot corrupt a fresh read', ({ When, Then, And }) => { + When('I attempt canonical graph mutation through q', async () => { + mutationResult = await runCLI( + GRAPH_CLI, + [ + ...BASE, + 'q', + 'g.graph.patterns[0].name="Mutated"; return g.pattern("GraphHandle")?.name', + ], + { timeout: 120000 }, + ); + freshResult = await runCLI(GRAPH_CLI, [...BASE, 'q', 'g.pattern("GraphHandle")?.name'], { + timeout: 120000, + }); + }); + Then('mutation throws or the GraphHandle sentinel remains unchanged', () => { + expect( + mutationResult?.exitCode !== 0 || mutationResult.stdout.trim() === 'GraphHandle', + ).toBe(true); + }); + And('a fresh q invocation returns the GraphHandle sentinel', () => { + expect(freshResult).toEqual({ exitCode: 0, stdout: 'GraphHandle\n', stderr: '' }); + }); + }); + + RuleScenario( + 'an import in the body fails loud with the injected-globals hint', + ({ When, Then, And }) => { + When('I run the graph CLI with q expression "import x from \'y\'"', async () => { + lastResult = await runCLI(GRAPH_CLI, [...BASE, 'q', "import x from 'y'"], { + timeout: 120000, + }); + }); + Then('the exit code is non-zero', () => { + expect(lastResult?.exitCode).not.toBe(0); + }); + And('stderr mentions "injected globals"', () => { + expect(lastResult?.stderr ?? '').toContain('injected globals'); + }); + }, + ); + }); + + Rule('The decoded graph holds its structural invariants', ({ RuleScenario }) => { + RuleScenario('the invariant battery passes against the live graph', ({ When, Then, And }) => { + When('I pipe the invariant battery script into the graph CLI', async () => { + lastResult = await runCLI(GRAPH_CLI, [...BASE, 'q'], { + timeout: 120000, + stdin: GRAPH_HANDLE_BATTERY_SCRIPT, + }); + }); + Then('the exit code is zero', () => { + expect(lastResult?.exitCode).toBe(0); + }); + And('the battery reports zero dangling uses edges', () => { + expect(battery().dangling).toBe(0); + }); + And('the battery reports coherent spec maturity and provenance', () => { + expect(battery().incoherent).toBe(0); + }); + And('the battery reports non-empty entry adapters', () => { + expect(battery().adapters).toBe(true); + }); + And('the battery reports a working spec bridge', () => { + expect(battery().specBridge).toBe(true); + }); + }); + }); + + Rule('The dangling gate is a deterministic machine contract', ({ RuleScenario }) => { + RuleScenario('the strict gate passes against the committed baseline', ({ When, Then, And }) => { + When('I run the graph CLI dangling gate against the committed baseline', async () => { + lastResult = await runCLI( + GRAPH_CLI, + [ + ...BASE, + 'dangling', + '--baseline', + 'packages/architect-guard/src/lint/dangling-baseline.json', + '--strict', + ], + { timeout: 120000 }, + ); + }); + Then('the exit code is zero', () => { + expect(lastResult?.exitCode).toBe(0); + }); + And('stdout matches the exact strict dangling JSON shape', () => { + const doc = parseStrictDangling((lastResult?.stdout ?? '').trim()); + expect(doc).toEqual(EXPECTED_STRICT_DANGLING); + }); + }); + }); +}); diff --git a/tests/steps/cli/lint-patterns.steps.ts b/tests/steps/cli/lint-patterns.steps.ts index 982cc7e..ccff412 100644 --- a/tests/steps/cli/lint-patterns.steps.ts +++ b/tests/steps/cli/lint-patterns.steps.ts @@ -216,7 +216,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { async function copyLegacyFixture(fixtureName: string, relativePath: string): Promise { const fixturePath = path.resolve('tests/fixtures/legacy-taxonomy', fixtureName); const fixture = await import('node:fs/promises').then((fs) => - fs.readFile(fixturePath, 'utf-8') + fs.readFile(fixturePath, 'utf-8'), ); await writeTempFile(getTempDir(), relativePath, fixture); } @@ -270,6 +270,27 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { expect(combined).toContain(text); }); }); + + RuleScenario('Reject invalid output format without stack trace', ({ When, Then, And }) => { + When('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + Then('exit code is {int}', (_ctx: unknown, code: number) => { + expect(getResult().exitCode).toBe(code); + }); + + And('output contains {string}', (_ctx: unknown, text: string) => { + const combined = getResult().stdout + getResult().stderr; + expect(combined).toContain(text); + }); + + And('output does not contain raw stack markers', () => { + const combined = getResult().stdout + getResult().stderr; + expect(combined).not.toContain('at parseArgs'); + expect(combined).not.toContain('Node.js v'); + }); + }); }); // --------------------------------------------------------------------------- @@ -282,7 +303,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} with complete annotations', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createCompletePatternFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -309,7 +330,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} without pattern name', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createMissingPatternNameFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -330,7 +351,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} with unresolved uses', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createUnresolvedUsesFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -351,7 +372,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} with invalid pattern name', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createInvalidPatternNameFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -372,7 +393,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'the legacy taxonomy fixture {string} is copied to {string}', async (_ctx: unknown, fixtureName: string, relativePath: string) => { await copyLegacyFixture(fixtureName, relativePath); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -395,7 +416,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'the legacy taxonomy fixture {string} is copied to {string}', async (_ctx: unknown, fixtureName: string, relativePath: string) => { await copyLegacyFixture(fixtureName, relativePath); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -409,7 +430,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { And('stdout contains {string}', (_ctx: unknown, text: string) => { expect(getResult().stdout).toContain(text); }); - } + }, ); }); @@ -423,7 +444,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} with complete annotations', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createCompletePatternFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -453,7 +474,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} with complete annotations', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createCompletePatternFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -480,7 +501,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} with missing status', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createMissingStatusFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -497,7 +518,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a TypeScript file {string} with missing status', async (_ctx: unknown, relativePath: string) => { await writeTempFile(getTempDir(), relativePath, createMissingStatusFile()); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { diff --git a/tests/steps/cli/lint-process.steps.ts b/tests/steps/cli/lint-process.steps.ts index c765336..18cde0c 100644 --- a/tests/steps/cli/lint-process.steps.ts +++ b/tests/steps/cli/lint-process.steps.ts @@ -55,12 +55,7 @@ function initGitRepo(dir: string): void { // ============================================================================= function createFeatureFile(status: string, unlockReason?: string): string { - const lines = [ - '@architect', - '@architect-pattern:TestPattern', - '@architect-phase:1', - `@architect-status:${status}`, - ]; + const lines = ['@architect', '@architect-pattern:TestPattern', `@architect-status:${status}`]; if (unlockReason) { lines.push(`@architect-unlock-reason:${unlockReason}`); @@ -74,7 +69,7 @@ function createFeatureFile(status: string, unlockReason?: string): string { ' Given a test condition', ' When an action occurs', ' Then a result is expected', - '' + '', ); return lines.join('\n'); @@ -273,7 +268,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a feature file {string} with status {string}', async (_ctx: unknown, filePath: string, status: string) => { await writeTempFile(getTempDir(), filePath, createFeatureFile(status)); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -294,7 +289,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { 'a feature file {string} with status {string}', async (_ctx: unknown, filePath: string, status: string) => { await writeTempFile(getTempDir(), filePath, createFeatureFile(status)); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -436,9 +431,9 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { await writeTempFile( getTempDir(), 'architect.config.ts', - createArchitectConfig(featurePattern) + createArchitectConfig(featurePattern), ); - } + }, ); And( @@ -447,9 +442,9 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { await writeTempFile( getTempDir(), filePath, - createFeatureFile('completed', unlockReason) + createFeatureFile('completed', unlockReason), ); - } + }, ); And('all files are staged', () => { @@ -468,7 +463,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { const combined = getResult().stdout + getResult().stderr; expect(combined).not.toContain(text); }); - } + }, ); RuleScenario( @@ -484,16 +479,16 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { await writeTempFile( getTempDir(), 'architect.config.ts', - createArchitectConfig(featurePattern) + createArchitectConfig(featurePattern), ); - } + }, ); And( 'a markdown file {string} containing {string}', async (_ctx: unknown, filePath: string, content: string) => { await writeTempFile(getTempDir(), filePath, `# Example\n\n${content}\n`); - } + }, ); And('all files are staged', () => { @@ -512,7 +507,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { const combined = getResult().stdout + getResult().stderr; expect(combined).not.toContain(text); }); - } + }, ); }); }); diff --git a/tests/steps/cli/pattern-graph-cli-core.steps.ts b/tests/steps/cli/pattern-graph-cli-core.steps.ts deleted file mode 100644 index 729c756..0000000 --- a/tests/steps/cli/pattern-graph-cli-core.steps.ts +++ /dev/null @@ -1,695 +0,0 @@ -/** - * pattern-graph CLI Core Step Definitions - * - * BDD step definitions for testing the pattern-graph CLI - * core infrastructure: help, version, input validation, - * status, query, pattern, arch basics, missing args, edge cases. - * - * @architect - * @architect-implements PatternGraphAPICLI - */ - -import { loadFeature, describeFeature } from '@amiceli/vitest-cucumber'; -import { expect } from 'vitest'; -import { writeTempFile } from '../../support/helpers/file-system.js'; -import { - type CLITestState, - initState, - getResult, - runCLICommand, - writePatternFiles, - writeArchPatternFiles, - createTempDir, -} from '../../support/helpers/pattern-graph-api-state.js'; - -// ============================================================================= -// Module-level state (reset per scenario) -// ============================================================================= - -let state: CLITestState | null = null; - -// ============================================================================= -// Feature Definition -// ============================================================================= - -const feature = await loadFeature('tests/features/cli/pattern-graph-cli-core.feature'); - -function createJsProjectConfig(): string { - return `export default { - sources: { - typescript: ['src/**/*.ts'] - } -}; -`; -} - -describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { - // --------------------------------------------------------------------------- - // Cleanup - // --------------------------------------------------------------------------- - - AfterEachScenario(async () => { - if (state?.tempContext) { - await state.tempContext.cleanup(); - } - state = null; - }); - - // --------------------------------------------------------------------------- - // Background - // --------------------------------------------------------------------------- - - Background(({ Given }) => { - Given('a temporary working directory', async () => { - state = initState(); - state.tempContext = await createTempDir({ prefix: 'cli-pattern-graph-test-' }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI displays help and version information - // --------------------------------------------------------------------------- - - Rule('CLI displays help and version information', ({ RuleScenario }) => { - RuleScenario('Display help with --help flag', ({ When, Then, And }) => { - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - - And('stdout does not contain {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).not.toContain(text); - }); - }); - - RuleScenario('Display version with -v flag', ({ When, Then }) => { - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - }); - - RuleScenario('No subcommand shows help', ({ When, Then, And }) => { - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI requires input flag for subcommands - // --------------------------------------------------------------------------- - - Rule('CLI requires input flag for subcommands', ({ RuleScenario }) => { - RuleScenario('Fail without --input flag when running status', ({ When, Then, And }) => { - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - - RuleScenario( - 'Use architect.config.js sources when --input is omitted', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('an architect.config.js with TypeScript sources', async () => { - await writeTempFile( - state!.tempContext!.tempDir, - 'architect.config.js', - createJsProjectConfig() - ); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - } - ); - - RuleScenario('Reject unknown options', ({ When, Then, And }) => { - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - - RuleScenario('Handoff rejects too many modified-file flags', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When( - 'I run handoff for {string} with too many modified-file flags', - async (_ctx: unknown, patternName: string) => { - const modifiedFiles = Array.from({ length: 201 }, (_value, index) => { - return `--modified-file src/file-${String(index)}.ts`; - }).join(' '); - - await runCLICommand( - state, - `pattern-graph-cli -i 'src/**/*.ts' handoff --pattern ${patternName} ${modifiedFiles}`, - { timeout: 60000 } - ); - } - ); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - - RuleScenario( - 'Handoff accepts positional pattern with modified file', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - } - ); - - RuleScenario( - 'Scope-validate rejects conflicting scope values', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - } - ); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI status subcommand shows delivery state - // --------------------------------------------------------------------------- - - Rule('CLI status subcommand shows delivery state', ({ RuleScenario }) => { - RuleScenario('Status shows counts and completion percentage', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI query subcommand executes API methods - // --------------------------------------------------------------------------- - - Rule('CLI query subcommand executes API methods', ({ RuleScenario }) => { - RuleScenario('Query getStatusCounts returns count object', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - const result = getResult(state); - expect(() => JSON.parse(result.stdout) as unknown).not.toThrow(); - }); - }); - - RuleScenario('Query isValidTransition with arguments', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - const result = getResult(state); - expect(() => JSON.parse(result.stdout) as unknown).not.toThrow(); - }); - }); - - RuleScenario('Unknown API method shows error', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - - RuleScenario('Invalid accepted status argument shows error', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - - RuleScenario('Invalid phase query argument shows error', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI pattern subcommand shows pattern detail - // --------------------------------------------------------------------------- - - Rule('CLI pattern subcommand shows pattern detail', ({ RuleScenario }) => { - RuleScenario('Pattern lookup returns full detail', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - }); - - RuleScenario('Pattern not found shows error', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - - RuleScenario( - 'Broken feature-backed pattern reports parser attribution', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('a broken feature spec for BrokenSpecPattern', async () => { - await writeTempFile( - state!.tempContext!.tempDir, - 'features/broken-spec-pattern.feature', - [ - '@architect', - '@architect-pattern:BrokenSpecPattern', - '@architect-status:completed', - 'Feature: Broken Spec Pattern', - '', - ' Rule: Parse attribution', - '', - ' Scenario: Unterminated docstring', - ' Given a broken feature source', - ' """', - ' missing closing docstring', - ].join('\n') - ); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains parse attribution for {string}', (_ctx: unknown, filePath: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain('spec-parse-failed'); - expect(combined).toContain(filePath); - expect(combined).toContain('line'); - }); - } - ); - - RuleScenario( - 'Truly missing pattern does not report parser attribution', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('a broken feature spec for BrokenSpecPattern', async () => { - await writeTempFile( - state!.tempContext!.tempDir, - 'features/broken-spec-pattern.feature', - [ - '@architect', - '@architect-pattern:BrokenSpecPattern', - '@architect-status:completed', - 'Feature: Broken Spec Pattern', - '', - ' Rule: Parse attribution', - '', - ' Scenario: Unterminated docstring', - ' Given a broken feature source', - ' """', - ' missing closing docstring', - ].join('\n') - ); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - - And('output does not contain {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).not.toContain(text); - }); - } - ); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI arch subcommand queries architecture - // --------------------------------------------------------------------------- - - Rule('CLI arch subcommand queries architecture', ({ RuleScenario }) => { - RuleScenario('Arch roles lists roles with counts', ({ Given, When, Then, And }) => { - Given('TypeScript files with architecture annotations', async () => { - await writeArchPatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - const result = getResult(state); - expect(() => JSON.parse(result.stdout) as unknown).not.toThrow(); - }); - }); - - RuleScenario( - 'Arch bounded-context filters to bounded context', - ({ Given, When, Then, And }) => { - Given('TypeScript files with architecture annotations', async () => { - await writeArchPatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - const result = getResult(state); - expect(() => JSON.parse(result.stdout) as unknown).not.toThrow(); - }); - } - ); - - RuleScenario('Arch layer reports unknown subcommand', ({ Given, When, Then, And }) => { - Given('TypeScript files with architecture annotations', async () => { - await writeArchPatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI shows errors for missing subcommand arguments - // --------------------------------------------------------------------------- - - Rule('CLI shows errors for missing subcommand arguments', ({ RuleScenario }) => { - RuleScenario('Query without method name shows error', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - - RuleScenario('Pattern without name shows error', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - - RuleScenario('Unknown subcommand shows error', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI handles argument edge cases - // --------------------------------------------------------------------------- - - Rule('CLI handles argument edge cases', ({ RuleScenario }) => { - RuleScenario('Integer arguments are coerced for phase queries', ({ Given, When, Then }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - }); - - RuleScenario('Double-dash separator is handled gracefully', ({ When, Then }) => { - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - }); - - RuleScenario( - 'Legacy category filter is rejected with role guidance', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - } - ); - }); -}); diff --git a/tests/steps/cli/pattern-graph-cli-modifiers-rules.steps.ts b/tests/steps/cli/pattern-graph-cli-modifiers-rules.steps.ts deleted file mode 100644 index 099d898..0000000 --- a/tests/steps/cli/pattern-graph-cli-modifiers-rules.steps.ts +++ /dev/null @@ -1,1277 +0,0 @@ -/** - * pattern-graph CLI Modifiers and Rules Step Definitions - * - * BDD step definitions for testing the pattern-graph CLI - * output modifiers, arch health, and rules subcommand. - * - * @architect - * @architect-implements PatternGraphAPICLI - */ - -import { readFile } from 'node:fs/promises'; -import path from 'node:path'; - -import { loadFeature, describeFeature } from '@amiceli/vitest-cucumber'; -import { expect } from 'vitest'; -import { z } from 'zod'; -import { FragmentSchema } from '@libar-dev/architect-projection'; -import { writeJson } from '../../../../architect-cli/src/cli/commands/_shared/output.js'; -import { - type CLITestState, - initState, - getTempDir, - getResult, - runCLICommand, - writeBlockedPatternFiles, - writePatternFiles, - writeDanglingRefFiles, - writeFeatureFilesWithRules, - writeParentHierarchyFeatureFiles, - createTempDir, -} from '../../support/helpers/pattern-graph-api-state.js'; -import { writeTempFile } from '../../support/helpers/file-system.js'; - -// ============================================================================= -// Module-level state (reset per scenario) -// ============================================================================= - -let state: CLITestState | null = null; -let serializationError: unknown = null; - -function parseJsonStdout(): Record { - return JSON.parse(getResult(state).stdout) as Record; -} - -function parseStdoutArray(): unknown[] { - const parsed = JSON.parse(getResult(state).stdout) as unknown; - expect(Array.isArray(parsed)).toBe(true); - return parsed as unknown[]; -} - -function parseProjectionRoot(): Record { - const parsed = JSON.parse(getResult(state).stdout) as { root?: unknown }; - expect(typeof parsed.root).toBe('object'); - expect(parsed.root).not.toBeNull(); - return parsed.root as Record; -} - -function parseBundleStdout(): { - readonly root: Record; - readonly children: Record>; -} { - const parsed = JSON.parse(getResult(state).stdout) as { - root?: unknown; - children?: unknown; - }; - expect(typeof parsed.root).toBe('object'); - expect(parsed.root).not.toBeNull(); - expect(typeof parsed.children).toBe('object'); - expect(parsed.children).not.toBeNull(); - return { - root: parsed.root as Record, - children: parsed.children as Record>, - }; -} - -function createBaselineContent(entries: readonly Record[]): string { - return `${JSON.stringify(entries, null, 2)}\n`; -} - -const CURRENT_DANGLING_BASELINE_ENTRY = { - pattern: 'ConsumerPattern', - field: 'uses', - missing: 'NonExistentDep', -}; - -const REMOVED_DANGLING_BASELINE_ENTRY = { - pattern: 'RemovedPattern', - field: 'uses', - missing: 'RemovedDependency', -}; - -function expectOrderedSubstrings(haystack: string, needles: readonly string[]): void { - let lastIndex = -1; - - for (const needle of needles) { - const index = haystack.indexOf(needle); - expect(index, `Expected stdout to contain ${needle}`).toBeGreaterThanOrEqual(0); - expect(index, `Expected ${needle} to appear after the previous serialized key`).toBeGreaterThan( - lastIndex - ); - lastIndex = index; - } -} - -// ============================================================================= -// Feature Definition -// ============================================================================= - -const feature = await loadFeature('tests/features/cli/pattern-graph-cli-modifiers-rules.feature'); - -describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { - // --------------------------------------------------------------------------- - // Cleanup - // --------------------------------------------------------------------------- - - AfterEachScenario(async () => { - if (state?.tempContext) { - await state.tempContext.cleanup(); - } - state = null; - serializationError = null; - }); - - // --------------------------------------------------------------------------- - // Background - // --------------------------------------------------------------------------- - - Background(({ Given }) => { - Given('a temporary working directory', async () => { - state = initState(); - state.tempContext = await createTempDir({ prefix: 'cli-pattern-graph-test-' }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: Output modifiers work when placed after the subcommand - // --------------------------------------------------------------------------- - - Rule('Output modifiers work when placed after the subcommand', ({ RuleScenario }) => { - RuleScenario( - 'Count modifier after list subcommand returns count', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a JSON number', () => { - const result = getResult(state); - const parsed = JSON.parse(result.stdout) as unknown; - expect(typeof parsed).toBe('number'); - }); - } - ); - - RuleScenario( - 'Names-only modifier after list subcommand returns names', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a JSON string array', () => { - const result = getResult(state); - const parsed = JSON.parse(result.stdout) as unknown; - expect(Array.isArray(parsed)).toBe(true); - const arr = parsed as unknown[]; - expect(arr.length).toBeGreaterThan(0); - expect(typeof arr[0]).toBe('string'); - }); - } - ); - - RuleScenario('Count modifier combined with list filter', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a JSON number', () => { - const result = getResult(state); - const parsed = JSON.parse(result.stdout) as unknown; - expect(typeof parsed).toBe('number'); - }); - }); - - RuleScenario( - 'Parent filter with names-only returns child names', - ({ Given, When, Then, And }) => { - Given('Gherkin feature files with parent hierarchy', async () => { - await writeParentHierarchyFeatureFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a JSON string array', () => { - const arr = parseStdoutArray(); - expect(arr.every((entry) => typeof entry === 'string')).toBe(true); - }); - - And('the list names-only result equals {string}', (_ctx: unknown, names: string) => { - expect(parseStdoutArray()).toEqual(names.split(',').map((name) => name.trim())); - }); - } - ); - - RuleScenario('Parent filter with count returns child count', ({ Given, When, Then, And }) => { - Given('Gherkin feature files with parent hierarchy', async () => { - await writeParentHierarchyFeatureFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a JSON number', () => { - const parsed = JSON.parse(getResult(state).stdout) as unknown; - expect(typeof parsed).toBe('number'); - }); - - And('the list count equals {int}', (_ctx: unknown, count: number) => { - expect(JSON.parse(getResult(state).stdout) as unknown).toBe(count); - }); - }); - - RuleScenario( - 'Parent filter returns empty for parent without children', - ({ Given, When, Then, And }) => { - Given('Gherkin feature files with parent hierarchy', async () => { - await writeParentHierarchyFeatureFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is an empty JSON string array', () => { - expect(parseStdoutArray()).toEqual([]); - }); - } - ); - - RuleScenario( - 'Open questions parent filter returns only descendants with questions', - ({ Given, When, Then, And }) => { - Given('Gherkin feature files with parent hierarchy', async () => { - await writeParentHierarchyFeatureFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And( - 'the open question result contains patterns {string}', - (_ctx: unknown, names: string) => { - const root = parseProjectionRoot(); - const items = root['items'] as Array<{ pattern: string }>; - expect(items.map((item) => item.pattern)).toEqual( - names.split(',').map((name) => name.trim()) - ); - } - ); - - And('every open question result entry has at least one question', () => { - const root = parseProjectionRoot(); - const items = root['items'] as Array<{ questions: string[] }>; - expect(items.length).toBeGreaterThan(0); - expect(items.every((item) => item.questions.length > 0)).toBe(true); - }); - } - ); - - RuleScenario( - 'Open questions empty parent returns an empty document', - ({ Given, When, Then, And }) => { - Given('Gherkin feature files with parent hierarchy', async () => { - await writeParentHierarchyFeatureFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('the open question result is empty', () => { - const root = parseProjectionRoot(); - expect(root['count']).toBe(0); - expect(root['items']).toEqual([]); - }); - } - ); - - RuleScenario( - 'Open questions unknown parent fails deterministically', - ({ Given, When, Then }) => { - Given('Gherkin feature files with parent hierarchy', async () => { - await writeParentHierarchyFeatureFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('parent filter fails with {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).exitCode).toBe(1); - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - } - ); - - RuleScenario( - 'Bundle include blocks return a composite payload', - ({ Given, When, Then, And }) => { - Given('Gherkin feature files with parent hierarchy', async () => { - await writeParentHierarchyFeatureFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - expect(() => JSON.parse(getResult(state).stdout) as unknown).not.toThrow(); - }); - - And('the bundle result contains children {string}', (_ctx: unknown, names: string) => { - expect(Object.keys(parseBundleStdout().children)).toEqual( - names.split(',').map((name) => name.trim()) - ); - }); - - And( - 'the bundle result includes requested block families {string}', - (_ctx: unknown, names: string) => { - const expected = names.split(',').map((name) => name.trim()); - const { root, children } = parseBundleStdout(); - const rootIncludes = root['includes'] as string[]; - expect(rootIncludes).toEqual(expected); - for (const child of Object.values(children)) { - expect(child['includes']).toEqual(expected); - const blocks = child['blocks'] as Record; - for (const expectedInclude of expected) { - const blockKey = - expectedInclude === 'open-questions' ? 'openQuestions' : expectedInclude; - expect(blocks).toHaveProperty(blockKey); - } - } - } - ); - - And('the bundle result preserves the ChildAlpha dependency on ChildBeta', () => { - const childAlpha = parseBundleStdout().children['ChildAlpha']; - if (childAlpha === undefined) { - throw new Error('Expected ChildAlpha bundle entry to exist'); - } - const deps = childAlpha['blocks'] as { deps?: { uses?: string[] } }; - expect(deps.deps?.uses).toContain('ChildBeta'); - }); - } - ); - - RuleScenario( - 'Bundle mode default include set returns heuristic token estimates', - ({ Given, When, Then, And }) => { - Given('Gherkin feature files with parent hierarchy', async () => { - await writeParentHierarchyFeatureFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - expect(() => JSON.parse(getResult(state).stdout) as unknown).not.toThrow(); - }); - - And('the bundle root mode is {string}', (_ctx: unknown, mode: string) => { - expect(parseBundleStdout().root['mode']).toBe(mode); - }); - - And( - 'the bundle result includes requested block families {string}', - (_ctx: unknown, names: string) => { - const expected = names.split(',').map((name) => name.trim()); - const { root, children } = parseBundleStdout(); - expect(root['includes']).toEqual(expected); - for (const child of Object.values(children)) { - expect(child['includes']).toEqual(expected); - const blocks = child['blocks'] as Record; - for (const expectedInclude of expected) { - const blockKey = - expectedInclude === 'open-questions' ? 'openQuestions' : expectedInclude; - expect(blocks).toHaveProperty(blockKey); - } - } - } - ); - - And( - 'the bundle token estimates use the {string} heuristic', - (_ctx: unknown, method: string) => { - const { root, children } = parseBundleStdout(); - expect((root['bundleTokenEstimate'] as { method?: string }).method).toBe(method); - expect((root['tokenEstimate'] as { method?: string }).method).toBe(method); - for (const child of Object.values(children)) { - expect((child['tokenEstimate'] as { method?: string }).method).toBe(method); - for (const blockEstimate of child['blockTokenEstimates'] as Array<{ - estimate: { method?: string }; - }>) { - expect(blockEstimate.estimate.method).toBe(method); - } - } - } - ); - } - ); - - RuleScenario('Bundle unknown root pattern fails deterministically', ({ Given, When, Then }) => { - Given('Gherkin feature files with parent hierarchy', async () => { - await writeParentHierarchyFeatureFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('parent filter fails with {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).exitCode).toBe(1); - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - - RuleScenario('Bundle accumulates repeated include flags', ({ Given, When, Then, And }) => { - Given('Gherkin feature files with parent hierarchy', async () => { - await writeParentHierarchyFeatureFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - expect(() => JSON.parse(getResult(state).stdout) as unknown).not.toThrow(); - }); - - And( - 'the bundle result includes requested block families {string}', - (_ctx: unknown, names: string) => { - const expected = names.split(',').map((name) => name.trim()); - const { root, children } = parseBundleStdout(); - const rootIncludes = root['includes'] as string[]; - expect(rootIncludes).toEqual(expected); - for (const child of Object.values(children)) { - expect(child['includes']).toEqual(expected); - const blocks = child['blocks'] as Record; - for (const expectedInclude of expected) { - const blockKey = - expectedInclude === 'open-questions' ? 'openQuestions' : expectedInclude; - expect(blocks).toHaveProperty(blockKey); - } - } - } - ); - }); - - RuleScenario('Unknown parent filter fails deterministically', ({ Given, When, Then }) => { - Given('Gherkin feature files with parent hierarchy', async () => { - await writeParentHierarchyFeatureFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('parent filter fails with {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).exitCode).toBe(1); - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - - RuleScenario('Malformed projection bundle JSON is rejected', ({ When, Then }) => { - When('serializing malformed projection bundle data', () => { - try { - writeJson({ - data: { - root: null, - }, - }); - } catch (error) { - serializationError = error; - } - }); - - Then('serialization fails with {string}', (_ctx: unknown, text: string) => { - expect(serializationError).toBeInstanceOf(Error); - expect((serializationError as Error).message).toContain(text); - }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI arch health subcommands detect graph quality issues - // --------------------------------------------------------------------------- - - Rule('CLI arch health subcommands detect graph quality issues', ({ RuleScenario }) => { - RuleScenario('Arch dangling returns broken references', ({ Given, When, Then, And }) => { - Given('TypeScript files with a dangling reference', async () => { - await writeDanglingRefFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout JSON data is an array', () => { - const result = getResult(state); - const parsed = JSON.parse(result.stdout) as { data: unknown }; - expect(Array.isArray(parsed.data)).toBe(true); - }); - - And( - 'stdout JSON data contains an entry with field {string}', - (_ctx: unknown, field: string) => { - const result = getResult(state); - const parsed = JSON.parse(result.stdout) as { data: Array> }; - const arr = parsed.data; - expect(arr.length).toBeGreaterThan(0); - expect(arr[0]).toHaveProperty(field); - } - ); - }); - - RuleScenario( - 'Arch dangling baseline matches current references', - ({ Given, When, Then, And }) => { - Given('TypeScript files with a dangling reference', async () => { - await writeDanglingRefFiles(state); - }); - - And('a dangling baseline file matching current references', async () => { - await writeTempFile( - getTempDir(state), - 'dangling-baseline.json', - createBaselineContent([CURRENT_DANGLING_BASELINE_ENTRY]) - ); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout JSON data reports no dangling baseline drift', () => { - const parsed = parseJsonStdout() as { data: Record }; - expect(parsed.data['drift']).toBe(false); - expect(parsed.data['baselineCount']).toBe(1); - expect(parsed.data['currentCount']).toBe(1); - expect(parsed.data['addedCount']).toBe(0); - expect(parsed.data['removedCount']).toBe(0); - }); - } - ); - - RuleScenario( - 'Arch dangling strict baseline drift reports added and removed entries', - ({ Given, When, Then, And }) => { - Given('TypeScript files with a dangling reference', async () => { - await writeDanglingRefFiles(state); - }); - - And('a dangling baseline file with a different reference', async () => { - await writeTempFile( - getTempDir(state), - 'dangling-baseline.json', - createBaselineContent([REMOVED_DANGLING_BASELINE_ENTRY]) - ); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout JSON data reports one added and one removed dangling baseline entry', () => { - const parsed = parseJsonStdout() as { - data: { - drift: boolean; - addedCount: number; - removedCount: number; - added: Array>; - removed: Array>; - }; - }; - expect(parsed.data.drift).toBe(true); - expect(parsed.data.addedCount).toBe(1); - expect(parsed.data.removedCount).toBe(1); - expect(parsed.data.added[0]).toEqual(CURRENT_DANGLING_BASELINE_ENTRY); - expect(parsed.data.removed[0]).toEqual(REMOVED_DANGLING_BASELINE_ENTRY); - }); - } - ); - - RuleScenario( - 'Arch dangling write-baseline rewrites deterministic JSON', - ({ Given, When, Then, And }) => { - Given('TypeScript files with a dangling reference', async () => { - await writeDanglingRefFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('dangling baseline file is deterministic for the current references', async () => { - const baselinePath = path.join(getTempDir(state), 'dangling-baseline.json'); - const content = await readFile(baselinePath, 'utf8'); - expect(content).toBe(createBaselineContent([CURRENT_DANGLING_BASELINE_ENTRY])); - }); - } - ); - - RuleScenario('Arch orphans returns isolated patterns', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout JSON data is an array', () => { - const result = getResult(state); - const parsed = JSON.parse(result.stdout) as { data: unknown }; - expect(Array.isArray(parsed.data)).toBe(true); - }); - - And( - 'stdout JSON data contains an entry with field {string}', - (_ctx: unknown, field: string) => { - const result = getResult(state); - const parsed = JSON.parse(result.stdout) as { data: Array> }; - const arr = parsed.data; - expect(arr.length).toBeGreaterThan(0); - expect(arr[0]).toHaveProperty(field); - } - ); - }); - - RuleScenario('Arch blocking returns blocked patterns', ({ Given, When, Then, And }) => { - Given('TypeScript files with blocked pattern annotations', async () => { - await writeBlockedPatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout JSON data is an array', () => { - const result = getResult(state); - const parsed = JSON.parse(result.stdout) as { data: unknown }; - expect(Array.isArray(parsed.data)).toBe(true); - }); - - And( - 'stdout JSON data contains an entry with field {string}', - (_ctx: unknown, field: string) => { - const result = getResult(state); - const parsed = JSON.parse(result.stdout) as { data: Array> }; - const arr = parsed.data; - expect(arr.length).toBeGreaterThan(0); - expect(arr[0]).toHaveProperty(field); - } - ); - - And( - 'stdout JSON data contains a blocking entry with field {string}', - (_ctx: unknown, field: string) => { - const result = getResult(state); - const parsed = JSON.parse(result.stdout) as { data: Array> }; - const arr = parsed.data; - expect(arr.length).toBeGreaterThan(0); - expect(arr[0]).toHaveProperty(field); - } - ); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI rules subcommand queries business rules and invariants - // --------------------------------------------------------------------------- - - Rule('CLI rules subcommand queries business rules and invariants', ({ RuleScenario }) => { - RuleScenario( - 'Rules returns business rules from feature files', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - } - ); - - RuleScenario( - 'Rules with --format json preserves routed bundle metadata', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON for a routed BusinessRuleSet bundle', () => { - const parsed = parseJsonStdout(); - - expect(Object.keys(parsed)).toEqual(['children', 'root', 'routing']); - expect((parsed['root'] as { kind?: unknown }).kind).toBe('BusinessRuleSet'); - expect(Object.keys(parsed['children'] as Record)).toEqual([ - 'coreutilstest', - 'validationrulestest', - ]); - expect(parsed['routing']).toEqual({ - anchorStrategy: 'heading-slug', - childRouteIds: { - coreutilstest: 'business-rules:coreutilstest', - validationrulestest: 'business-rules:validationrulestest', - }, - childPathStrategy: 'nested', - rootRouteId: 'business-rules:index', - }); - }); - - And('routed rules JSON keeps canonical bundle key ordering', () => { - const parsed = parseJsonStdout(); - - expect(Object.keys(parsed['root'] as Record)).toEqual([ - 'groupedBy', - 'groupingEntries', - 'kind', - 'rules', - 'scope', - ]); - expect(Object.keys(parsed['routing'] as Record)).toEqual([ - 'anchorStrategy', - 'childRouteIds', - 'childPathStrategy', - 'rootRouteId', - ]); - }); - - And('raw routed rules JSON keeps canonical serializer order on the wire', () => { - const stdout = getResult(state).stdout; - - expect(stdout).toContain('"rootRouteId": "business-rules:index"'); - expectOrderedSubstrings(stdout, ['"children"', '"root"', '"routing"']); - expectOrderedSubstrings(stdout, [ - '"anchorStrategy"', - '"childRouteIds"', - '"childPathStrategy"', - '"rootRouteId"', - ]); - }); - - And('the bundle root validates against FragmentSchema', () => { - const parsed = parseJsonStdout(); - const result = FragmentSchema.safeParse(parsed['root']); - - expect(result.success, result.success ? '' : z.prettifyError(result.error)).toBe(true); - }); - } - ); - - RuleScenario('Rules filters by product area', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - }); - - RuleScenario('Rules with names-only returns flat array', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a JSON string array', () => { - const result = getResult(state); - const parsed = JSON.parse(result.stdout) as unknown; - expect(Array.isArray(parsed)).toBe(true); - }); - }); - - RuleScenario('Rules with count returns a JSON number', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a JSON number', () => { - const result = getResult(state); - const parsed = JSON.parse(result.stdout) as unknown; - expect(typeof parsed).toBe('number'); - }); - - And('the rules count equals {int}', (_ctx: unknown, count: number) => { - const result = getResult(state); - const parsed = JSON.parse(result.stdout) as unknown; - expect(parsed).toBe(count); - }); - }); - - RuleScenario('Rules filters by pattern name', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - }); - - RuleScenario( - 'Rules with only-invariants excludes rules without invariants', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - } - ); - - RuleScenario( - 'Rules product area filter excludes non-matching areas', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - } - ); - - RuleScenario( - 'Rules combines product area and only-invariants filters', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - } - ); - - RuleScenario('Rules filters by canonical package name', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - - And('stdout does not contain {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).not.toContain(text); - }); - }); - - RuleScenario('Rules package filter works with count', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a JSON number', () => { - const parsed = JSON.parse(getResult(state).stdout) as unknown; - expect(typeof parsed).toBe('number'); - }); - - And('the rules count equals {int}', (_ctx: unknown, count: number) => { - const parsed = JSON.parse(getResult(state).stdout) as unknown; - expect(parsed).toBe(count); - }); - }); - - RuleScenario('Rules feature path filter works with count', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a JSON number', () => { - const parsed = JSON.parse(getResult(state).stdout) as unknown; - expect(typeof parsed).toBe('number'); - }); - - And('the rules count equals {int}', (_ctx: unknown, count: number) => { - const parsed = JSON.parse(getResult(state).stdout) as unknown; - expect(parsed).toBe(count); - }); - }); - - RuleScenario( - 'Rules feature glob filter works with names-only', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a JSON string array', () => { - const parsed = JSON.parse(getResult(state).stdout) as unknown; - expect(Array.isArray(parsed)).toBe(true); - }); - - And('the rules names-only result has {int} entries', (_ctx: unknown, count: number) => { - const parsed = JSON.parse(getResult(state).stdout) as unknown; - expect(Array.isArray(parsed)).toBe(true); - expect(parsed).toHaveLength(count); - }); - } - ); - - RuleScenario( - 'Rules feature path filter accepts package-host repo-relative path', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a JSON number', () => { - const parsed = JSON.parse(getResult(state).stdout) as unknown; - expect(typeof parsed).toBe('number'); - }); - - And('the rules count equals {int}', (_ctx: unknown, count: number) => { - const parsed = JSON.parse(getResult(state).stdout) as unknown; - expect(parsed).toBe(count); - }); - } - ); - - RuleScenario( - 'Rules feature glob filter accepts package-host repo-relative glob', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a JSON string array', () => { - const parsed = JSON.parse(getResult(state).stdout) as unknown; - expect(Array.isArray(parsed)).toBe(true); - }); - - And('the rules names-only result has {int} entries', (_ctx: unknown, count: number) => { - const parsed = JSON.parse(getResult(state).stdout) as unknown; - expect(Array.isArray(parsed)).toBe(true); - expect(parsed).toHaveLength(count); - }); - } - ); - - RuleScenario('Rules rejects retired phase filter', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - - RuleScenario( - 'Rules rejects conflicting pattern and product-area filters', - ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('Gherkin feature files with business rules', async () => { - await writeFeatureFilesWithRules(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - } - ); - }); -}); diff --git a/tests/steps/cli/pattern-graph-cli-subcommands.steps.ts b/tests/steps/cli/pattern-graph-cli-subcommands.steps.ts deleted file mode 100644 index 9565fd7..0000000 --- a/tests/steps/cli/pattern-graph-cli-subcommands.steps.ts +++ /dev/null @@ -1,532 +0,0 @@ -/** - * pattern-graph CLI Subcommands Step Definitions - * - * BDD step definitions for testing the pattern-graph CLI - * discovery subcommands: list, search, context assembly, - * tags/sources, extended arch, unannotated. - * - * @architect - * @architect-implements PatternGraphAPICLI - */ - -import { loadFeature, describeFeature } from '@amiceli/vitest-cucumber'; -import { expect } from 'vitest'; -import { - type CLITestState, - initState, - getResult, - runCLICommand, - writePatternFiles, - writeCandidateAndDeliveryPatternFiles, - writeDiagnosticFeatureFiles, - writeArchPatternFiles, - writeArchPatternFilesWithDeps, - writeTwoContextFiles, - writeMixedAnnotationFiles, - createTempDir, -} from '../../support/helpers/pattern-graph-api-state.js'; - -// ============================================================================= -// Module-level state (reset per scenario) -// ============================================================================= - -let state: CLITestState | null = null; - -// ============================================================================= -// Feature Definition -// ============================================================================= - -const feature = await loadFeature('tests/features/cli/pattern-graph-cli-subcommands.feature'); - -describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { - // --------------------------------------------------------------------------- - // Cleanup - // --------------------------------------------------------------------------- - - AfterEachScenario(async () => { - if (state?.tempContext) { - await state.tempContext.cleanup(); - } - state = null; - }); - - // --------------------------------------------------------------------------- - // Background - // --------------------------------------------------------------------------- - - Background(({ Given }) => { - Given('a temporary working directory', async () => { - state = initState(); - state.tempContext = await createTempDir({ prefix: 'cli-pattern-graph-test-' }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI list subcommand filters patterns - // --------------------------------------------------------------------------- - - Rule('CLI list subcommand filters patterns', ({ RuleScenario }) => { - RuleScenario('List all patterns returns JSON array', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - const result = getResult(state); - expect(() => JSON.parse(result.stdout) as unknown).not.toThrow(); - }); - }); - - RuleScenario('List filters candidate status', ({ Given, When, Then, And }) => { - Given('TypeScript files with candidate and delivery pattern annotations', async () => { - await writeCandidateAndDeliveryPatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - const result = getResult(state); - expect(() => JSON.parse(result.stdout) as unknown).not.toThrow(); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - - And('stdout does not contain {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).not.toContain(text); - }); - }); - - RuleScenario('List with removed phase flag shows error', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - - RuleScenario('List with removed maturity flag shows error', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI search subcommand finds patterns by fuzzy match - // --------------------------------------------------------------------------- - - Rule('CLI search subcommand finds patterns by fuzzy match', ({ RuleScenario }) => { - RuleScenario('Search returns matching patterns', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - const result = getResult(state); - expect(() => JSON.parse(result.stdout) as unknown).not.toThrow(); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - }); - - RuleScenario('Search without query shows error', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI context assembly subcommands return text output - // --------------------------------------------------------------------------- - - Rule('CLI context assembly subcommands return text output', ({ RuleScenario }) => { - RuleScenario('Context returns curated text bundle', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is non-empty', () => { - expect(getResult(state).stdout.trim().length).toBeGreaterThan(0); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - }); - - RuleScenario('Context without pattern name shows error', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('output contains {string}', (_ctx: unknown, text: string) => { - const combined = getResult(state).stdout + getResult(state).stderr; - expect(combined).toContain(text); - }); - }); - - RuleScenario('Overview returns executive summary text', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is non-empty', () => { - expect(getResult(state).stdout.trim().length).toBeGreaterThan(0); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - }); - - RuleScenario('Dep-tree returns dependency tree text', ({ Given, When, Then, And }) => { - Given('TypeScript files with architecture annotations and dependencies', async () => { - await writeArchPatternFilesWithDeps(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is non-empty', () => { - expect(getResult(state).stdout.trim().length).toBeGreaterThan(0); - }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI diagnostics subcommand returns extraction diagnostics - // --------------------------------------------------------------------------- - - Rule('CLI diagnostics subcommand returns extraction diagnostics', ({ RuleScenario }) => { - RuleScenario( - 'Diagnostics returns extraction failures from feature files', - ({ Given, And, When, Then }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - And('feature files with extraction diagnostics', async () => { - await writeDiagnosticFeatureFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - const result = getResult(state); - expect(() => JSON.parse(result.stdout) as unknown).not.toThrow(); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - } - ); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI tags, taxonomy, and sources subcommands return JSON - // --------------------------------------------------------------------------- - - Rule('CLI tags, taxonomy, and sources subcommands return JSON', ({ RuleScenario }) => { - RuleScenario('Tags returns tag usage counts', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - }); - - RuleScenario('Taxonomy returns taxonomy digest', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - - And('stdout is valid JSON', () => { - const result = getResult(state); - expect(() => JSON.parse(result.stdout) as unknown).not.toThrow(); - }); - }); - - RuleScenario('Taxonomy count returns compact text', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a single taxonomy count line', () => { - const stdout = getResult(state).stdout.trim(); - expect(stdout.split('\n')).toHaveLength(1); - expect(stdout).toMatch( - /^\d+ roles \| \d+ metadata tags \| \d+ aggregation tags \| \d+ total$/u - ); - }); - }); - - RuleScenario('Taxonomy JSON count returns four numeric keys', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is a taxonomy count JSON object', () => { - const parsed = JSON.parse(getResult(state).stdout) as Record; - expect(Object.keys(parsed)).toEqual(['roles', 'metadata', 'aggregation', 'total']); - expect(Object.values(parsed).every((value) => typeof value === 'number')).toBe(true); - }); - }); - - RuleScenario('Sources returns file inventory', ({ Given, When, Then, And }) => { - Given('TypeScript files with pattern annotations', async () => { - await writePatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - const result = getResult(state); - expect(() => JSON.parse(result.stdout) as unknown).not.toThrow(); - }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI extended arch subcommands query architecture relationships - // --------------------------------------------------------------------------- - - Rule('CLI extended arch subcommands query architecture relationships', ({ RuleScenario }) => { - RuleScenario( - 'Arch neighborhood returns pattern relationships', - ({ Given, When, Then, And }) => { - Given('TypeScript files with architecture annotations and dependencies', async () => { - await writeArchPatternFilesWithDeps(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - const result = getResult(state); - expect(() => JSON.parse(result.stdout) as unknown).not.toThrow(); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - } - ); - - RuleScenario( - 'Arch compare returns bounded-context comparison', - ({ Given, When, Then, And }) => { - Given('TypeScript files with two bounded contexts', async () => { - await writeTwoContextFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - const result = getResult(state); - expect(() => JSON.parse(result.stdout) as unknown).not.toThrow(); - }); - } - ); - - RuleScenario('Arch coverage returns annotation coverage', ({ Given, When, Then, And }) => { - Given('TypeScript files with architecture annotations', async () => { - await writeArchPatternFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout is valid JSON', () => { - const result = getResult(state); - expect(() => JSON.parse(result.stdout) as unknown).not.toThrow(); - }); - }); - }); - - // --------------------------------------------------------------------------- - // Rule: CLI unannotated subcommand finds files without annotations - // --------------------------------------------------------------------------- - - Rule('CLI unannotated subcommand finds files without annotations', ({ RuleScenario }) => { - RuleScenario( - 'Unannotated finds files missing architect marker', - ({ Given, When, Then, And }) => { - Given('TypeScript files with mixed annotations', async () => { - await writeMixedAnnotationFiles(state); - }); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(state, cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult(state).exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult(state).stdout).toContain(text); - }); - } - ); - }); -}); diff --git a/tests/steps/cli/public-contract.steps.ts b/tests/steps/cli/public-contract.steps.ts index 660992d..ad91b1b 100644 --- a/tests/steps/cli/public-contract.steps.ts +++ b/tests/steps/cli/public-contract.steps.ts @@ -5,42 +5,86 @@ */ import { describeFeature, loadFeature } from '@amiceli/vitest-cucumber'; -import { - buildPatternGraph, - createPatternGraphAPI, - createSuccess, - RenderFormatSchema, - ScopeTypeSchema, - SessionTypeSchema, - WORKSPACE_TAG_REGISTRY, - type QuerySuccess, -} from '@libar-dev/architect-core'; import * as architectProjection from '@libar-dev/architect-projection'; -import { expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; + +const [architectCore, architectGraph] = await Promise.all([ + import('@libar-dev/architect-core'), + import('@libar-dev/architect-core/graph'), +]); const feature = await loadFeature('tests/features/cli/public-contract.feature'); +const RETAINED_CORE_EXPORTS = [ + 'buildPatternGraph', + 'getDependencyContext', + 'getRulesForPattern', + 'isValidTransition', + 'validateTransition', + 'getValidTransitionsFrom', + 'getProtectionSummary', + 'resolveDecisionPattern', + 'listDecisionPatterns', + 'canonicalDecisionKey', + 'createPackageResolver', + 'WORKSPACE_TAG_REGISTRY', + 'RenderFormatSchema', + 'SessionTypeSchema', + 'ScopeTypeSchema', +] as const; + +const RETAINED_GRAPH_EXPORTS = ['Graph', 'createGraph', 'PatternGraphSchema'] as const; + +const LEGACY_CORE_EXPORTS = [ + 'createPatternGraphAPI', + 'PatternGraphAPI', + 'QueryResult', + 'QuerySuccess', + 'QueryError', + 'QueryApiError', + 'createSuccess', + 'createError', +] as const; + +function findLegacyCoreExports(moduleExports: object): readonly string[] { + return LEGACY_CORE_EXPORTS.filter((exportName) => exportName in moduleExports); +} + +describe('legacy facade export absence guard', () => { + for (const exportName of ['createPatternGraphAPI', 'PatternGraphAPI'] as const) { + it(`detects a restored ${exportName} runtime export`, () => { + // Given a package namespace with one real facade key restored + const restoredExports = { [exportName]: Symbol(exportName) }; + + // When the public-contract absence guard inspects it + const detected = findLegacyCoreExports(restoredExports); + + // Then the restored key is reported and the package-level empty assertion would fail + expect(detected).toEqual([exportName]); + }); + } +}); + describeFeature(feature, ({ Rule }) => { Rule( 'architect-core and architect-projection keep canonical exports importable', ({ RuleScenario }) => { - RuleScenario('architect-core query contract exports remain available', ({ Then }) => { - Then('architect-core query contract exports remain available', () => { - const envelope: QuerySuccess<{ ok: boolean }> = createSuccess({ ok: true }, 3); - - expect(typeof buildPatternGraph).toBe('function'); - expect(typeof createPatternGraphAPI).toBe('function'); - expect(WORKSPACE_TAG_REGISTRY).toBeDefined(); - expect(RenderFormatSchema.safeParse('json').success).toBe(true); - expect(SessionTypeSchema.safeParse('implement').success).toBe(true); - expect(ScopeTypeSchema.safeParse('design').success).toBe(true); - - expect(envelope.success).toBe(true); - expect(envelope.data).toEqual({ ok: true }); - expect(envelope.metadata.patternCount).toBe(3); - expect(Number.isNaN(Date.parse(envelope.metadata.timestamp))).toBe(false); - }); - }); + RuleScenario( + 'architect-core graph and pure-kernel exports replace the legacy facade', + ({ Then, And }) => { + Then('architect-core graph and pure-kernel exports are available', () => { + for (const exportName of RETAINED_CORE_EXPORTS) { + expect(exportName in architectCore).toBe(true); + } + for (const exportName of RETAINED_GRAPH_EXPORTS) { + expect(exportName in architectGraph).toBe(true); + } + }); + And('architect-core legacy facade exports are absent', () => { + expect(findLegacyCoreExports(architectCore)).toEqual([]); + }); + }, + ); RuleScenario( 'architect-projection canonical projection entrypoints remain public', @@ -50,7 +94,7 @@ describeFeature(feature, ({ Rule }) => { 'parseAndProjectArchitectureDiagram', 'parseAndProjectBusinessRuleSet', 'parseAndProjectConfig', - 'parseAndProjectDependencyTree', + 'parseAndProjectDependencyContext', 'parseAndProjectDocumentationBundle', 'parseAndProjectFileReadingList', 'parseAndProjectHandoffRecord', @@ -62,12 +106,12 @@ describeFeature(feature, ({ Rule }) => { ] as const; expect(typeof architectProjection.projectOverviewDigest).toBe('function'); - expect(typeof architectProjection.projectReleaseNotesDigest).toBe('function'); + expect(typeof architectProjection.projectChangelog).toBe('function'); for (const exportName of parseAndProjectExports) { expect(typeof architectProjection[exportName]).toBe('function'); } }); - } + }, ); RuleScenario( @@ -77,13 +121,16 @@ describeFeature(feature, ({ Rule }) => { 'architect-projection hides the raw architecture diagram export from the top-level barrel', () => { expect(typeof architectProjection.parseAndProjectArchitectureDiagram).toBe( - 'function' + 'function', ); expect('projectArchitectureDiagram' in architectProjection).toBe(false); - } + expect('projectConfig' in architectProjection).toBe(false); + expect('projectDocumentationBundle' in architectProjection).toBe(false); + expect('projectPrChangeReview' in architectProjection).toBe(false); + }, ); - } + }, ); - } + }, ); }); diff --git a/tests/steps/cli/validate-patterns.steps.ts b/tests/steps/cli/validate-patterns.steps.ts index 3f06b07..8332c50 100644 --- a/tests/steps/cli/validate-patterns.steps.ts +++ b/tests/steps/cli/validate-patterns.steps.ts @@ -88,42 +88,6 @@ ${backgroundSection} `; } -function createDoDGherkinPatternFile( - patternName: string, - _phase: number, - status: string, - options: { - deliverableStatus?: string; - includeAcceptanceCriteria?: boolean; - } = {} -): string { - const { deliverableStatus = 'complete', includeAcceptanceCriteria = true } = options; - - const backgroundSection = - status === 'completed' - ? ` - Background: Deliverables - Given the following deliverables: - | Deliverable | Status | Tests | Location | - | Test deliverable | ${deliverableStatus} | 1 | src/test.ts | - -` - : ''; - - const scenarioTagLine = includeAcceptanceCriteria ? ' @acceptance-criteria\n' : ''; - - return `@architect -@architect-pattern:${patternName} -@architect-status:${status} -Feature: ${patternName} - Test feature for validate-patterns CLI testing. -${backgroundSection}${scenarioTagLine} Scenario: Basic scenario - Given a test condition - When an action occurs - Then a result is expected -`; -} - // ============================================================================= // Feature Definition // ============================================================================= @@ -284,14 +248,14 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { filePath: string, patternName: string, phase: number, - status: string + status: string, ) => { await writeTempFile( getTempDir(), filePath, - createTypeScriptPatternFile(patternName, phase, status) + createTypeScriptPatternFile(patternName, phase, status), ); - } + }, ); And( @@ -301,14 +265,14 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { filePath: string, patternName: string, phase: number, - status: string + status: string, ) => { await writeTempFile( getTempDir(), filePath, - createGherkinPatternFile(patternName, phase, status) + createGherkinPatternFile(patternName, phase, status), ); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -324,9 +288,9 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { }); }); - // Wave 1 retired phase-mismatch cross-source detection; the matching - // scenario was removed from the feature file. Status mismatch (below) - // remains the canonical mismatch signal exercised here. + // Cross-source phase-mismatch detection was retired with the numeric + // @architect-phase tag (ADR-013); the matching scenario was removed from the + // feature file. Status mismatch (below) is the canonical mismatch signal. RuleScenario('Detect status mismatch between sources', ({ Given, When, Then, And }) => { Given( @@ -336,14 +300,14 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { filePath: string, patternName: string, phase: number, - status: string + status: string, ) => { await writeTempFile( getTempDir(), filePath, - createTypeScriptPatternFile(patternName, phase, status) + createTypeScriptPatternFile(patternName, phase, status), ); - } + }, ); And( @@ -353,14 +317,58 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { filePath: string, patternName: string, phase: number, - status: string + status: string, ) => { await writeTempFile( getTempDir(), filePath, - createGherkinPatternFile(patternName, phase, status) + createGherkinPatternFile(patternName, phase, status), ); - } + }, + ); + + When('running {string}', async (_ctx: unknown, cmd: string) => { + await runCLICommand(cmd); + }); + + Then('exit code is {int}', (_ctx: unknown, code: number) => { + expect(getResult().exitCode).toBe(code); + }); + + And('stdout contains {string}', (_ctx: unknown, text: string) => { + expect(getResult().stdout).toContain(text); + }); + }); + }); + + // --------------------------------------------------------------------------- + // Rule: Extraction diagnostics affect validation result + // --------------------------------------------------------------------------- + + Rule('Extraction diagnostics affect validation result', ({ RuleScenario }) => { + RuleScenario('Extraction diagnostic errors fail validation', ({ Given, And, When, Then }) => { + Given( + 'a TypeScript file {string} with content:', + async (_ctx: unknown, filePath: string, content: string) => { + await writeTempFile(getTempDir(), filePath, content); + }, + ); + + And( + 'a Gherkin file {string} with pattern {string} at phase {int} status {string}', + async ( + _ctx: unknown, + filePath: string, + patternName: string, + phase: number, + status: string, + ) => { + await writeTempFile( + getTempDir(), + filePath, + createGherkinPatternFile(patternName, phase, status), + ); + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -374,6 +382,10 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { And('stdout contains {string}', (_ctx: unknown, text: string) => { expect(getResult().stdout).toContain(text); }); + + And('stdout does not contain {string}', (_ctx: unknown, text: string) => { + expect(getResult().stdout).not.toContain(text); + }); }); }); @@ -390,14 +402,14 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { filePath: string, patternName: string, phase: number, - status: string + status: string, ) => { await writeTempFile( getTempDir(), filePath, - createTypeScriptPatternFile(patternName, phase, status) + createTypeScriptPatternFile(patternName, phase, status), ); - } + }, ); And( @@ -407,14 +419,14 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { filePath: string, patternName: string, phase: number, - status: string + status: string, ) => { await writeTempFile( getTempDir(), filePath, - createGherkinPatternFile(patternName, phase, status) + createGherkinPatternFile(patternName, phase, status), ); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -447,14 +459,14 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { filePath: string, patternName: string, phase: number, - status: string + status: string, ) => { await writeTempFile( getTempDir(), filePath, - createTypeScriptPatternFile(patternName, phase, status) + createTypeScriptPatternFile(patternName, phase, status), ); - } + }, ); And( @@ -464,14 +476,14 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { filePath: string, patternName: string, phase: number, - status: string + status: string, ) => { await writeTempFile( getTempDir(), filePath, - createGherkinPatternFile(patternName, phase, status) + createGherkinPatternFile(patternName, phase, status), ); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -506,14 +518,14 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { filePath: string, patternName: string, phase: number, - status: string + status: string, ) => { await writeTempFile( getTempDir(), filePath, - createTypeScriptPatternFile(patternName, phase, status) + createTypeScriptPatternFile(patternName, phase, status), ); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -539,14 +551,14 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { filePath: string, patternName: string, phase: number, - status: string + status: string, ) => { await writeTempFile( getTempDir(), filePath, - createTypeScriptPatternFile(patternName, phase, status) + createTypeScriptPatternFile(patternName, phase, status), ); - } + }, ); And( @@ -556,14 +568,14 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { filePath: string, patternName: string, phase: number, - status: string + status: string, ) => { await writeTempFile( getTempDir(), filePath, - createGherkinPatternFile(patternName, phase, status) + createGherkinPatternFile(patternName, phase, status), ); - } + }, ); When('running {string}', async (_ctx: unknown, cmd: string) => { @@ -581,58 +593,6 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { }); }); - // --------------------------------------------------------------------------- - // Rule: CLI validates Definition of Done from PatternGraph - // --------------------------------------------------------------------------- - - Rule('CLI validates Definition of Done from PatternGraph', ({ RuleScenario }) => { - RuleScenario( - 'DoD passes for completed pattern with deliverables and acceptance criteria', - ({ Given, When, Then, And }) => { - Given( - 'a TypeScript file {string} with pattern {string} at phase {int} status {string}', - async ( - _ctx: unknown, - filePath: string, - patternName: string, - phase: number, - status: string - ) => { - await writeTempFile( - getTempDir(), - filePath, - createTypeScriptPatternFile(patternName, phase, status) - ); - } - ); - - And( - 'a completed DoD-ready Gherkin file {string} with pattern {string} at phase {int}', - async (_ctx: unknown, filePath: string, patternName: string, phase: number) => { - await writeTempFile( - getTempDir(), - filePath, - createDoDGherkinPatternFile(patternName, phase, 'completed') - ); - } - ); - - When('running {string}', async (_ctx: unknown, cmd: string) => { - await runCLICommand(cmd); - }); - - Then('exit code is {int}', (_ctx: unknown, code: number) => { - expect(getResult().exitCode).toBe(code); - }); - - And('stdout contains {string}', (_ctx: unknown, text: string) => { - expect(getResult().stdout).toContain(text); - }); - } - ); - - // Wave 1 retired phase-grouping for DoD validation; the matching - // scenario was removed from the feature file. The DoD-pass scenario - // above still exercises the live PatternGraph-backed DoD path. - }); + // Rule 7 (CLI Definition of Done validation) was retired per ADR-013 — the + // phase-keyed DoD validator was removed, so its scenarios are gone. }); diff --git a/tests/steps/generation/architecture-doc-render-budget.steps.ts b/tests/steps/generation/architecture-doc-render-budget.steps.ts new file mode 100644 index 0000000..a723737 --- /dev/null +++ b/tests/steps/generation/architecture-doc-render-budget.steps.ts @@ -0,0 +1,74 @@ +/** + * Architecture document render-budget step definitions. + * + * Reads the generated `docs-live/ARCHITECTURE.md` and asserts every fenced + * mermaid block stays under Mermaid's default `maxTextSize`, and that the + * document is split into more than one block. This guards against a regression + * back to the single all-pattern `graph TD` that exceeded the limit and failed + * to render. + */ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { describeFeature, loadFeature } from '@amiceli/vitest-cucumber'; +import { expect } from 'vitest'; + +const MERMAID_MAX_TEXT_SIZE = 50_000; + +interface RenderBudgetState { + documentPath: string; + mermaidBlocks: string[]; +} + +let state: RenderBudgetState | null = null; + +function requireState(): RenderBudgetState { + if (!state) throw new Error('State not initialized'); + return state; +} + +function extractMermaidBlocks(markdown: string): string[] { + return [...markdown.matchAll(/```mermaid\n([\s\S]*?)\n```/gu)].map((match) => match[1] ?? ''); +} + +const feature = await loadFeature( + 'tests/features/generation/architecture-doc-render-budget.feature', +); + +describeFeature(feature, ({ Rule }) => { + Rule( + 'No single Mermaid block in the generated architecture document exceeds the renderer limit', + ({ RuleScenario }) => { + RuleScenario( + 'every mermaid block in the generated architecture document is renderable', + ({ Given, When, Then, And }) => { + Given( + 'the generated architecture document at {string}', + (_ctx: unknown, relativePath: string) => { + state = { + documentPath: path.resolve(process.cwd(), relativePath), + mermaidBlocks: [], + }; + }, + ); + + When('I extract its fenced mermaid blocks', async () => { + const current = requireState(); + const markdown = await readFile(current.documentPath, 'utf8'); + current.mermaidBlocks = extractMermaidBlocks(markdown); + }); + + Then('it should contain more than one mermaid block', () => { + expect(requireState().mermaidBlocks.length).toBeGreaterThan(1); + }); + + And('every mermaid block should be smaller than 50000 characters', () => { + for (const block of requireState().mermaidBlocks) { + expect(block.length).toBeLessThan(MERMAID_MAX_TEXT_SIZE); + } + }); + }, + ); + }, + ); +}); diff --git a/tests/steps/generation/load-preamble.steps.ts b/tests/steps/generation/load-preamble.steps.ts index 0e0c94e..0b3b684 100644 --- a/tests/steps/generation/load-preamble.steps.ts +++ b/tests/steps/generation/load-preamble.steps.ts @@ -21,7 +21,7 @@ import { loadFeature, describeFeature } from '@amiceli/vitest-cucumber'; import { expect } from 'vitest'; -import { parseMarkdownToBlocks, type SectionBlock } from '@libar-dev/architect-core'; +import { BlockSchema, parseMarkdownToBlocks, type Block } from '@libar-dev/architect-core'; // ============================================================================= // State Types @@ -29,7 +29,7 @@ import { parseMarkdownToBlocks, type SectionBlock } from '@libar-dev/architect-c interface LoadPreambleState { markdownContent: string; - blocks: readonly SectionBlock[]; + blocks: readonly Block[]; } // ============================================================================= @@ -54,7 +54,7 @@ function requireState(): LoadPreambleState { return state; } -function getBlock(index: number): SectionBlock { +function getBlock(index: number): Block { const s = requireState(); const block = s.blocks[index]; if (!block) throw new Error(`No block at index ${String(index)}`); @@ -355,7 +355,7 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { expect(block.content).toContain('const x = 1;'); expect(block.content).toContain('const y = 2;'); } - } + }, ); }); @@ -499,4 +499,97 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { }); }); }); + + // --------------------------------------------------------------------------- + // Rule: Code-fence language is a single identifier-shaped token + // --------------------------------------------------------------------------- + + Rule('Code-fence language is a single identifier-shaped token', ({ RuleScenario }) => { + RuleScenario( + 'Info string with a trailing attribute keeps only the language token', + ({ Given, When, Then }) => { + Given('markdown with a code fence info string carrying extra tokens', () => { + requireState().markdownContent = '```ts {highlight: 1}\nconst x = 1;\n```'; + }); + + When('parsing the markdown to blocks', () => { + const s = requireState(); + s.blocks = parseMarkdownToBlocks(s.markdownContent); + }); + + Then( + 'block 1 is a code block with language {string}', + (_ctx: unknown, language: string) => { + const block = getBlock(0); + expect(block.type).toBe('code'); + if (block.type === 'code') { + expect(block.language).toBe(language); + } + }, + ); + }, + ); + + RuleScenario('Non-identifier info string yields no language', ({ Given, When, Then }) => { + Given('markdown with a non-identifier code fence info string', () => { + requireState().markdownContent = '```text/markdown\nhello\n```'; + }); + + When('parsing the markdown to blocks', () => { + const s = requireState(); + s.blocks = parseMarkdownToBlocks(s.markdownContent); + }); + + Then('block 1 is a code block with no language', () => { + const block = getBlock(0); + expect(block.type).toBe('code'); + if (block.type === 'code') { + expect(block.language).toBeUndefined(); + } + }); + }); + }); + + // --------------------------------------------------------------------------- + // Rule: Parser output validates against the canonical block schema + // --------------------------------------------------------------------------- + + Rule('Parser output validates against the canonical block schema', ({ RuleScenario }) => { + RuleScenario( + "A mixed markdown document's blocks all validate against the canonical schema", + ({ Given, When, Then }) => { + Given('markdown with heading, paragraph, table, code, and list', () => { + requireState().markdownContent = [ + '## Overview', + '', + 'This is a paragraph.', + '', + '| Col A | Col B |', + '|-------|-------|', + '| val1 | val2 |', + '', + '```typescript', + 'const x = 1;', + '```', + '', + '- item one', + '- item two', + ].join('\n'); + }); + + When('parsing the markdown to blocks', () => { + const s = requireState(); + s.blocks = parseMarkdownToBlocks(s.markdownContent); + }); + + Then('every produced block validates against the canonical block schema', () => { + const s = requireState(); + expect(s.blocks.length).toBeGreaterThan(0); + for (const block of s.blocks) { + expect(BlockSchema.safeParse(block).success).toBe(true); + } + }); + }, + ); + }); }); diff --git a/tests/support/helpers/cli-runner.ts b/tests/support/helpers/cli-runner.ts index ba149ba..7c272d9 100644 --- a/tests/support/helpers/cli-runner.ts +++ b/tests/support/helpers/cli-runner.ts @@ -59,8 +59,8 @@ const PROJECT_ROOT = path.resolve(__dirname, '../../..'); /** * Path to the split CLI package source tree. */ -const CLI_PACKAGE_ROOT = path.resolve(__dirname, '../../../../architect-cli'); -const GUARD_PACKAGE_ROOT = path.resolve(__dirname, '../../../../architect-guard'); +const CLI_PACKAGE_ROOT = path.resolve(__dirname, '../../../packages/architect-cli'); +const GUARD_PACKAGE_ROOT = path.resolve(__dirname, '../../../packages/architect-guard'); /** * Resolve the tsx binary from node_modules/.bin/ rather than relying on npx. @@ -71,14 +71,13 @@ const GUARD_PACKAGE_ROOT = path.resolve(__dirname, '../../../../architect-guard' const TSX_BIN = path.join(PROJECT_ROOT, 'node_modules', '.bin', 'tsx'); const LEGACY_CLI_BIN_ALIASES: Record = { - 'pattern-graph-cli': 'architect', 'generate-docs': 'architect-generate', 'lint-patterns': 'architect-lint-patterns', 'validate-patterns': 'architect-validate', 'lint-process': 'architect-guard', }; -const SOURCE_EXECUTED_CLIS = new Set(['pattern-graph-cli', 'lint-patterns']); +const SOURCE_EXECUTED_CLIS = new Set(['graph-cli', 'lint-patterns']); function createChildEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { const childEnv: NodeJS.ProcessEnv = { ...env, FORCE_COLOR: '0' }; @@ -126,7 +125,7 @@ export function getCLIPath(cliName: string): string { export async function runCLI( cliName: string, args: string[], - options: CLIOptions = {} + options: CLIOptions = {}, ): Promise { const { cwd = process.cwd(), @@ -264,7 +263,7 @@ export function parseCommand(commandString: string): { command: string; args: st */ export async function runCommand( commandString: string, - options: CLIOptions = {} + options: CLIOptions = {}, ): Promise { const { command, args } = parseCommand(commandString); return runCLI(command, args, options); diff --git a/tests/support/helpers/file-system.ts b/tests/support/helpers/file-system.ts index d7363ef..cac6848 100644 --- a/tests/support/helpers/file-system.ts +++ b/tests/support/helpers/file-system.ts @@ -79,7 +79,7 @@ export async function createTempDir(options: TempDirOptions = {}): Promise { const fullPath = path.join(dir, relativePath); await fs.mkdir(path.dirname(fullPath), { recursive: true }); @@ -167,7 +167,6 @@ export function createTsFileWithDirective(options: { description?: string; status?: string; dependsOn?: string[]; - useCases?: string[]; uses?: string[]; usedBy?: string[]; archRole?: string; @@ -181,7 +180,6 @@ export function createTsFileWithDirective(options: { description = 'A test pattern.', status, dependsOn = [], - useCases = [], usedBy = [], uses = [], archRole, @@ -217,10 +215,6 @@ export function createTsFileWithDirective(options: { lines.push(` * @architect-uses ${dependsOn.join(', ')}`); } - for (const useCase of useCases) { - lines.push(` * @architect-usecase "${useCase}"`); - } - for (const uses_ of uses) { lines.push(` * @architect-uses ${uses_}`); } @@ -271,28 +265,20 @@ export interface RegularType { * @example * ```typescript * const content = createFeatureFile({ - * phase: 1, * status: "completed", - * quarter: "Q4-2025", * name: "Foundation Types", * }); * ``` */ export function createFeatureFile(options: { - phase?: number; status?: string; - quarter?: string; - effort?: string; team?: string; name?: string; description?: string; deliverables?: Array<{ name: string; status: string; tests: number; location?: string }>; }): string { const { - phase = 1, status = 'completed', - quarter = 'Q4-2025', - effort = '1w', team = 'platform', name = 'Test Feature', description = 'A test feature for validation.', @@ -302,10 +288,7 @@ export function createFeatureFile(options: { const lines: string[] = []; // Process tags (using @architect-* prefix per PDR-004) - lines.push(`@architect-phase:${phase}`); lines.push(`@architect-status:${status}`); - lines.push(`@architect-quarter:${quarter}`); - lines.push(`@architect-effort:${effort}`); lines.push(`@architect-team:${team}`); lines.push(`Feature: ${name}`); lines.push(` ${description}`); diff --git a/tests/support/helpers/graph-handle-contract.ts b/tests/support/helpers/graph-handle-contract.ts new file mode 100644 index 0000000..729bdc5 --- /dev/null +++ b/tests/support/helpers/graph-handle-contract.ts @@ -0,0 +1,66 @@ +import { resolve } from 'node:path'; + +import { z } from 'zod'; + +export const GRAPH_HANDLE_BATTERY_SCRIPT = ` +const dangling = g.driftFlags(() => true).dangling.length; +const specs = g.specsReverifying(g.patterns.map((p) => p.name)); +const incoherent = + specs.filter((s) => s.provenance === 'executable' && s.maturity !== 'executable').length + + specs.filter((s) => s.provenance === 'authored' && s.maturity === 'executable').length; +const adapters = + g.bySymbol('ProjectionBundle').definedIn.length > 0 && g.findByConcept('taxonomy').length > 0; +const specBridge = g.patterns + .filter((p) => p.implementedBy.length > 0) + .slice(0, 50) + .some((p) => g.invariantsOf(p.name).length > 0); +return JSON.stringify({ dangling, incoherent, adapters, specBridge }); +`; + +const BatterySchema = z.strictObject({ + dangling: z.number(), + incoherent: z.number(), + adapters: z.boolean(), + specBridge: z.boolean(), +}); + +const MigratedHandleSchema = z.strictObject({ + hasApi: z.literal(false), + hasFsm: z.literal('function'), + frozen: z.literal(true), + deferred: z.literal(true), +}); + +const StrictDanglingSchema = z.strictObject({ + baselinePath: z.string(), + written: z.literal(false), + strict: z.literal(true), + drift: z.literal(false), + baselineCount: z.number(), + currentCount: z.number(), + addedCount: z.number(), + removedCount: z.number(), + added: z.array(z.unknown()), + removed: z.array(z.unknown()), + current: z.array(z.unknown()), +}); + +export const parseBattery = (output: string) => BatterySchema.parse(JSON.parse(output)); +export const parseMigratedHandle = (output: string) => + MigratedHandleSchema.parse(JSON.parse(output)); +export const parseStrictDangling = (output: string) => + StrictDanglingSchema.parse(JSON.parse(output)); + +export const EXPECTED_STRICT_DANGLING = { + baselinePath: resolve('packages/architect-guard/src/lint/dangling-baseline.json'), + written: false, + strict: true, + drift: false, + baselineCount: 0, + currentCount: 0, + addedCount: 0, + removedCount: 0, + added: [], + removed: [], + current: [], +} as const; diff --git a/tests/support/helpers/output-pipeline.ts b/tests/support/helpers/output-pipeline.ts index 23b211f..bf62786 100644 --- a/tests/support/helpers/output-pipeline.ts +++ b/tests/support/helpers/output-pipeline.ts @@ -104,7 +104,6 @@ function summarizePattern(pattern: ExtractedPattern): Record { name: pattern.name, status: pattern.status, role: pattern.role ?? 'uncategorized', - phase: pattern.phase, filePath: pattern.source.file, }; } diff --git a/tests/support/helpers/pattern-graph-api-state.ts b/tests/support/helpers/pattern-graph-api-state.ts deleted file mode 100644 index a6db4b0..0000000 --- a/tests/support/helpers/pattern-graph-api-state.ts +++ /dev/null @@ -1,583 +0,0 @@ -/** - * Pattern Graph CLI Shared Test State and Fixture Builders - * - * Extracted from pattern-graph-cli.steps.ts to be shared across - * the split test files (core, subcommands, modifiers-rules). - * - * @architect - */ - -import { writeTempFile, createTsFileWithDirective, type TempDirContext } from './file-system.js'; -import { runCommand, type CLIResult } from './cli-runner.js'; - -// ============================================================================= -// Type Definitions -// ============================================================================= - -export interface CLITestState { - tempContext: TempDirContext | null; - result: CLIResult | null; -} - -// ============================================================================= -// State Management -// ============================================================================= - -export function initState(): CLITestState { - return { - tempContext: null, - result: null, - }; -} - -// ============================================================================= -// State Accessors -// ============================================================================= - -export function getState(state: CLITestState | null): CLITestState { - if (!state) throw new Error('State not initialized'); - return state; -} - -export function getTempDir(state: CLITestState | null): string { - const s = getState(state); - if (!s.tempContext) throw new Error('Temp context not initialized'); - return s.tempContext.tempDir; -} - -export function getResult(state: CLITestState | null): CLIResult { - const s = getState(state); - if (!s.result) throw new Error('CLI result not available - did you run a command?'); - return s.result; -} - -export async function runCLICommand( - state: CLITestState | null, - commandString: string, - options: { timeout?: number } = {} -): Promise { - const s = getState(state); - s.result = await runCommand(commandString, { - cwd: getTempDir(state), - ...(options.timeout !== undefined ? { timeout: options.timeout } : {}), - }); -} - -// ============================================================================= -// Fixture Content Builders -// ============================================================================= - -export function createPatternFiles(): Array<{ path: string; content: string }> { - return [ - { - path: 'src/completed.ts', - content: createTsFileWithDirective({ - patternName: 'CompletedPattern', - status: 'completed', - }), - }, - { - path: 'src/active.ts', - content: createTsFileWithDirective({ - patternName: 'ActivePattern', - status: 'active', - }), - }, - { - path: 'src/roadmap.ts', - content: createTsFileWithDirective({ - patternName: 'RoadmapPattern', - status: 'roadmap', - }), - }, - ]; -} - -export function createBlockedPatternFiles(): Array<{ path: string; content: string }> { - return [ - { - path: 'src/completed.ts', - content: createTsFileWithDirective({ - patternName: 'CompletedPattern', - status: 'completed', - }), - }, - { - path: 'src/active.ts', - content: createTsFileWithDirective({ - patternName: 'ActivePattern', - status: 'active', - dependsOn: ['RoadmapPattern'], - }), - }, - { - path: 'src/roadmap.ts', - content: createTsFileWithDirective({ - patternName: 'RoadmapPattern', - status: 'roadmap', - }), - }, - ]; -} - -export function createCandidateAndDeliveryPatternFiles(): Array<{ path: string; content: string }> { - return [ - { - path: 'src/candidate.ts', - content: createTsFileWithDirective({ - patternName: 'CandidatePattern', - status: 'candidate', - }), - }, - { - path: 'src/roadmap.ts', - content: createTsFileWithDirective({ - patternName: 'RoadmapPattern', - status: 'roadmap', - }), - }, - ]; -} - -export function createDiagnosticFeatureFiles(): Array<{ path: string; content: string }> { - return [ - { - path: 'architect/specs/missing-status.feature', - content: [ - '@architect', - '@architect-pattern:MissingStatusPattern', - 'Feature: Missing Status Pattern', - '', - ' Rule: Diagnostic coverage', - '', - ' **Invariant:** Gated files without status produce diagnostics.', - '', - ' **Rationale:** Missing status must not fail silently.', - '', - ' @acceptance-criteria', - ' Scenario: Missing status', - ' Given a gated file', - ' Then a diagnostic is emitted', - ].join('\n'), - }, - ]; -} - -export function createFeatureFilesWithRules(): Array<{ path: string; content: string }> { - return [ - { - path: 'packages/architect-core/specs/validation-rules.feature', - content: [ - '@architect', - '@architect-pattern:ValidationRulesTest', - '@architect-status:completed', - '@architect-unlock-reason:Split-from-original', - '@architect-product-area:Validation', - '@architect-phase:10', - 'Feature: Validation Rules Test', - '', - ' Rule: Completed files require unlock', - '', - ' **Invariant:** Completed files need unlock-reason.', - '', - ' **Rationale:** Prevents accidental regression.', - '', - ' **Verified by:** Unlock test', - '', - ' @acceptance-criteria', - ' Scenario: Unlock test', - ' Given a completed file', - ' Then it needs unlock', - '', - ' Rule: Status transitions follow FSM', - '', - ' **Invariant:** Only valid FSM transitions allowed.', - '', - ' @acceptance-criteria', - ' Scenario: Valid transition', - ' Given a roadmap pattern', - ' Then it can transition to active', - ].join('\n'), - }, - { - path: 'packages/architect-cli/specs/core-utils.feature', - content: [ - '@architect', - '@architect-pattern:CoreUtilsTest', - '@architect-status:completed', - '@architect-product-area:CoreTypes', - '@architect-phase:5', - 'Feature: Core Utils Test', - '', - ' Rule: Slugify produces URL-safe slugs', - '', - ' **Invariant:** Output must be lowercase alphanumeric with hyphens.', - '', - ' @acceptance-criteria', - ' Scenario: Slug generation', - ' Given text input', - ' Then slug is URL-safe', - '', - ' Rule: Edge cases handled', - '', - ' No invariant here, just a plain rule.', - '', - ' @acceptance-criteria', - ' Scenario: Edge case', - ' Given empty input', - ' Then empty slug returned', - ].join('\n'), - }, - { - path: 'tests/features/cli/package-host-rules.feature', - content: [ - '@architect', - '@architect-pattern:PackageHostRulesTest', - '@architect-status:completed', - '@architect-product-area:DataAPI', - 'Feature: Package Host Rules Test', - '', - ' Rule: Package host feature paths are repo-relative', - '', - ' **Invariant:** Feature filters accept repo-relative package-host paths.', - '', - ' @acceptance-criteria', - ' Scenario: Repo-relative feature filter', - ' Given a package-host feature path', - ' Then the matching rules are returned', - ].join('\n'), - }, - ]; -} - -export function createParentHierarchyFeatureFiles(): Array<{ path: string; content: string }> { - return [ - { - path: 'tests/features/parent-epic.feature', - content: [ - '@architect', - '@architect-pattern:ParentEpic', - '@architect-status:completed', - '@architect-level:epic', - '@architect-unlock-reason:SeedParentFilterCoverage', - 'Feature: Parent Epic', - ' **Problem:** Parent bundles need one query.', - '', - ' **Solution:** Keep immediate child slices grouped under the epic.', - '', - ' Scenario: Parent shell', - ' Given a parent epic', - ' Then children can attach to it', - ].join('\n'), - }, - { - path: 'tests/features/empty-epic.feature', - content: [ - '@architect', - '@architect-pattern:EmptyEpic', - '@architect-status:completed', - '@architect-level:epic', - '@architect-unlock-reason:SeedEmptyParentCoverage', - 'Feature: Empty Epic', - '', - ' Scenario: Empty parent shell', - ' Given an empty parent epic', - ' Then no children attach to it', - ].join('\n'), - }, - { - path: 'tests/features/child-alpha.feature', - content: [ - '@architect', - '@architect-pattern:ChildAlpha', - '@architect-status:active', - '@architect-level:slice', - '@architect-parent:ParentEpic', - '@architect-uses:ChildBeta', - 'Feature: Child Alpha', - ' **Problem:** Alpha needs a delivery owner.', - '', - ' **Open Questions:**', - ' - Who owns the alpha follow-up?', - ' - Which signal closes the alpha gap?', - '', - ' Rule: Alpha bundle data stays grouped', - '', - ' **Invariant:** Alpha bundle data must keep its open questions and dependencies together.', - '', - ' **Verified by:** Alpha child', - '', - ' Scenario: Alpha child', - ' Given a parent-scoped child', - ' Then it appears under its parent', - ].join('\n'), - }, - { - path: 'tests/features/child-beta.feature', - content: [ - '@architect', - '@architect-pattern:ChildBeta', - '@architect-status:completed', - '@architect-level:slice', - '@architect-parent:ParentEpic', - '@architect-unlock-reason:SeedCompletedChildCoverage', - 'Feature: Child Beta', - ' **Problem:** Beta still needs a rollout signal.', - '', - ' **Open Questions:**', - ' - What beta rollout signal is durable?', - '', - ' Rule: Beta scenarios remain visible', - '', - ' **Invariant:** Bundle scenario extraction must preserve beta scenario names.', - '', - ' **Verified by:** Beta child', - '', - ' Scenario: Beta child', - ' Given another parent-scoped child', - ' Then it appears under its parent', - ].join('\n'), - }, - { - path: 'tests/features/unrelated.feature', - content: [ - '@architect', - '@architect-pattern:UnrelatedPattern', - '@architect-status:completed', - '@architect-level:slice', - '@architect-unlock-reason:SeedUnrelatedCoverage', - 'Feature: Unrelated Pattern', - '', - ' Scenario: Unrelated pattern', - ' Given an unrelated pattern', - ' Then it stays outside the parent filter', - ].join('\n'), - }, - ]; -} - -export function createArchPatternFiles(): Array<{ path: string; content: string }> { - return [ - { - path: 'src/scanner.ts', - content: createTsFileWithDirective({ - patternName: 'TestScanner', - status: 'completed', - archRole: 'infrastructure', - archContext: 'testctx', - archLayer: 'infrastructure', - }), - }, - { - path: 'src/codec.ts', - content: createTsFileWithDirective({ - patternName: 'TestCodec', - status: 'completed', - archRole: 'projection', - archContext: 'testctx', - archLayer: 'application', - }), - }, - ]; -} - -export function createDanglingRefFiles(): Array<{ path: string; content: string }> { - return [ - { - path: 'src/consumer.ts', - content: createTsFileWithDirective({ - patternName: 'ConsumerPattern', - status: 'active', - uses: ['NonExistentDep'], - }), - }, - ]; -} - -export function createArchPatternFilesWithDeps(): Array<{ path: string; content: string }> { - return [ - { - path: 'src/scanner-service.ts', - content: createTsFileWithDirective({ - patternName: 'ContextFormatterImpl', - status: 'completed', - archRole: 'service', - archContext: 'api', - archLayer: 'application', - uses: ['ContextAssemblerImpl'], - }), - }, - { - path: 'src/file-cache.ts', - content: createTsFileWithDirective({ - patternName: 'ContextAssemblerImpl', - status: 'completed', - archRole: 'service', - archContext: 'api', - archLayer: 'application', - usedBy: ['ContextFormatterImpl'], - }), - }, - ]; -} - -// ============================================================================= -// File Writers -// ============================================================================= - -export async function writePatternFiles(state: CLITestState | null): Promise { - const dir = getTempDir(state); - for (const file of createPatternFiles()) { - await writeTempFile(dir, file.path, file.content); - } -} - -export async function writeBlockedPatternFiles(state: CLITestState | null): Promise { - const dir = getTempDir(state); - for (const file of createBlockedPatternFiles()) { - await writeTempFile(dir, file.path, file.content); - } -} - -export async function writeCandidateAndDeliveryPatternFiles( - state: CLITestState | null -): Promise { - const dir = getTempDir(state); - for (const file of createCandidateAndDeliveryPatternFiles()) { - await writeTempFile(dir, file.path, file.content); - } -} - -export async function writeDiagnosticFeatureFiles(state: CLITestState | null): Promise { - const dir = getTempDir(state); - for (const file of createDiagnosticFeatureFiles()) { - await writeTempFile(dir, file.path, file.content); - } -} - -export async function writeFeatureFilesWithRules(state: CLITestState | null): Promise { - const dir = getTempDir(state); - await writeTempFile( - dir, - 'architect.config.js', - [ - 'export default {', - ' packages: [', - " { id: 'architect-cli', displayName: 'Architect CLI', match: 'packages/architect-cli/' },", - " { id: 'architect-core', displayName: 'Architect Core', match: 'packages/architect-core/' },", - " { id: 'architect-dev', displayName: 'Architect Host', match: 'tests/features/' },", - ' ],', - '};', - '', - ].join('\n') - ); - for (const file of createFeatureFilesWithRules()) { - await writeTempFile(dir, file.path, file.content); - } -} - -export async function writeParentHierarchyFeatureFiles(state: CLITestState | null): Promise { - const dir = getTempDir(state); - await writeTempFile( - dir, - 'architect.config.js', - [ - 'export default {', - ' packages: [', - " { id: 'architect-dev', displayName: 'Architect Host', match: 'tests/features/' },", - ' ],', - '};', - '', - ].join('\n') - ); - for (const file of createPatternFiles()) { - await writeTempFile(dir, file.path, file.content); - } - for (const file of createParentHierarchyFeatureFiles()) { - await writeTempFile(dir, file.path, file.content); - } -} - -export async function writeArchPatternFiles(state: CLITestState | null): Promise { - const dir = getTempDir(state); - for (const file of createArchPatternFiles()) { - await writeTempFile(dir, file.path, file.content); - } -} - -export async function writeDanglingRefFiles(state: CLITestState | null): Promise { - const dir = getTempDir(state); - for (const file of createDanglingRefFiles()) { - await writeTempFile(dir, file.path, file.content); - } -} - -export async function writeArchPatternFilesWithDeps(state: CLITestState | null): Promise { - const dir = getTempDir(state); - for (const file of createArchPatternFilesWithDeps()) { - await writeTempFile(dir, file.path, file.content); - } -} - -export async function writeTwoContextFiles(state: CLITestState | null): Promise { - const dir = getTempDir(state); - const files = [ - { - path: 'src/scanner-svc.ts', - content: createTsFileWithDirective({ - patternName: 'ScannerSvc', - status: 'completed', - archRole: 'service', - archContext: 'scanner', - archLayer: 'application', - uses: ['SharedUtil'], - }), - }, - { - path: 'src/codec-svc.ts', - content: createTsFileWithDirective({ - patternName: 'CodecSvc', - status: 'completed', - archRole: 'projection', - archContext: 'codec', - archLayer: 'application', - uses: ['SharedUtil'], - }), - }, - { - path: 'src/shared-util.ts', - content: createTsFileWithDirective({ - patternName: 'SharedUtil', - status: 'completed', - archRole: 'infrastructure', - archContext: 'shared', - archLayer: 'infrastructure', - usedBy: ['ScannerSvc', 'CodecSvc'], - }), - }, - ]; - for (const file of files) { - await writeTempFile(dir, file.path, file.content); - } -} - -export async function writeMixedAnnotationFiles(state: CLITestState | null): Promise { - const dir = getTempDir(state); - const files = [ - ...createPatternFiles(), - { - path: 'src/unannotated.ts', - content: '/** No @architect marker */\nexport const x = 1;\n', - }, - ]; - for (const file of files) { - await writeTempFile(dir, file.path, file.content); - } -} - -// ============================================================================= -// Re-exports -// ============================================================================= - -export { createTempDir } from './file-system.js'; -export type { CLIResult } from './cli-runner.js'; diff --git a/tests/support/step-lint-setup.ts b/tests/support/step-lint-setup.ts index 12703c0..d5eb3d5 100644 --- a/tests/support/step-lint-setup.ts +++ b/tests/support/step-lint-setup.ts @@ -14,7 +14,7 @@ export function setup(): void { if (hasFailures(summary, false)) { const output = formatPretty(summary, { quiet: true }); throw new Error( - `Step lint: ${summary.errorCount} error(s) found in feature/step files.\n\n${output}\n\nFix the issues above before running tests.` + `Step lint: ${summary.errorCount} error(s) found in feature/step files.\n\n${output}\n\nFix the issues above before running tests.`, ); } } diff --git a/tests/support/world.ts b/tests/support/world.ts index 8a70806..7f4253f 100644 --- a/tests/support/world.ts +++ b/tests/support/world.ts @@ -61,7 +61,7 @@ export function getRequiredTableRow(rows: readonly DataTableRow[], index = 0): D */ export function getRequiredExampleValue( examples: Readonly>, - field: string + field: string, ): string { const value = examples[field]; if (typeof value !== 'string') { @@ -84,14 +84,14 @@ export function compactDefined(values: readonly (T | null | undefined)[]): T[ * | name | Test | */ export function tableRowsToObject( - rows: Array<{ field: string; value: string }> + rows: Array<{ field: string; value: string }>, ): Record { return rows.reduce( (acc, row) => { acc[row.field] = row.value; return acc; }, - {} as Record + {} as Record, ); } diff --git a/vitest.config.ts b/vitest.config.ts index cbae284..92a7644 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ include: [ 'tests/steps/api/**/*.steps.ts', 'tests/steps/cli/**/*.steps.ts', - 'tests/steps/generation/load-preamble.steps.ts', + 'tests/steps/generation/**/*.steps.ts', ], exclude: ['tests/support/**/*.ts', 'tests/fixtures/**/*.ts'], globals: true,