From 78ed25836760961acfb9fc2436e7e7d99334f602 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Fri, 9 Oct 2026 22:54:48 -0500 Subject: [PATCH] chore: replace em and en dashes with ASCII throughout An em dash cannot be typed on a standard keyboard without a numeric-alt or IME sequence, so every occurrence was machine-written rather than authored. They also propagate by copy, since the preset artifacts are copied between packages. Replacement is context-aware rather than a blind substitution: | Pattern | Becomes | |---|---| | Markdown heading or bold label followed by a dash | a colon | | Anything else mid-sentence | a spaced ASCII hyphen | | Numeric ranges | a bare hyphen | Documentation and comment text, plus two console status strings that are noted separately. Signed-off-by: Joey Smith Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .devcontainer/devcontainer.json | 2 +- .github/copilot-instructions.md | 28 ++++---- .github/instructions/memory.instructions.md | 6 +- .github/skills/speckit-analyze/SKILL.md | 4 +- .github/skills/speckit-checklist/SKILL.md | 4 +- .github/skills/speckit-clarify/SKILL.md | 12 ++-- .github/skills/speckit-constitution/SKILL.md | 2 +- .github/skills/speckit-converge/SKILL.md | 18 +++--- .github/skills/speckit-implement/SKILL.md | 2 +- .github/skills/speckit-plan/SKILL.md | 2 +- .github/skills/speckit-specify/SKILL.md | 10 +-- .github/skills/speckit-tasks/SKILL.md | 2 +- .github/skills/speckit-taskstoissues/SKILL.md | 2 +- Dockerfile | 2 +- README.md | 8 +-- compose.yml | 14 ++-- specs/001-theme-resolution/data-model.md | 24 +++---- specs/001-theme-resolution/decisions.md | 6 +- specs/001-theme-resolution/plan.md | 12 ++-- specs/001-theme-resolution/quickstart.md | 10 +-- specs/001-theme-resolution/research.md | 20 +++--- specs/001-theme-resolution/spec.md | 26 ++++---- specs/001-theme-resolution/tasks.md | 64 +++++++++---------- src/Resolver/ThemeResolver.php | 4 +- 24 files changed, 142 insertions(+), 142 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 90bff64..94cd7e1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ // Webware alignment Dev Container. // -// This is a thin wrapper around compose.yml — it reuses the `tooling` service +// This is a thin wrapper around compose.yml - it reuses the `tooling` service // rather than defining a parallel environment, so VS Code and plain // `docker compose` users share the exact same container. Non-VS Code // developers are not orphaned: they use `docker compose up -d` + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7213f86..bb1f5a2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,12 +1,12 @@ -# webware/webware-theme — Copilot Agent Instructions +# webware/webware-theme: Copilot Agent Instructions -## Read this before acting — fleet working agreements +## Read this before acting: fleet working agreements `vendor/webware/webware-tools/agent-working-agreements.md` is authoritative for how work is done in this fleet, and it overrides anything else in this file or in any local note. Read it before acting, and again before reporting. The non-negotiables: -- Do exactly the scope asked — only the files, branches and repositories named. No adjacent edits, +- Do exactly the scope asked - only the files, branches and repositories named. No adjacent edits, no cleanup, no onboarding, no surveys. - Anything else you notice gets one line at most, with no proposed fix. - Never close a reply with a condensed restatement of it, and never present an inference as a @@ -26,11 +26,11 @@ always a dependency, so this file does not need to be duplicated per package. Mago distinguishes what a `*::class` string may reference: -- `class-string` — a concrete **class** -- `interface-string` — an **interface** -- `enum-string` — an **enum** -- `trait-string` — a **trait** -- `class-like-string` — any of class / interface / enum +- `class-string` - a concrete **class** +- `interface-string` - an **interface** +- `enum-string` - an **enum** +- `trait-string` - a **trait** +- `class-like-string` - any of class / interface / enum Webware aliases against interfaces and builds to interfaces, so prefer the precise form: @@ -47,22 +47,22 @@ PHPUnit 13 enforces a strict separation between mocks and stubs. Violating these - **Use `createStub()`** when the test double only needs to return values (`method()->willReturn()`, `method()->willReturnCallback()`). No expectations are configured. - **Use `createMock()`** only when the test verifies behavior with `expects()` (e.g. `expects($this->once())`, `expects($this->never())`). -- **Never** call `createMock()` without also calling `expects()` on at least one method — PHPUnit 13 will issue a notice. +- **Never** call `createMock()` without also calling `expects()` on at least one method - PHPUnit 13 will issue a notice. - **Remove** `/** @var ClassName&MockObject */` annotations and `MockObject` intersection types on variables created with `createStub()`. - **Remove** the `use PHPUnit\Framework\MockObject\MockObject;` import from any file where no `createMock()` + `expects()` usage remains. ### Examples ```php -// CORRECT — stub returns value, no expectations +// CORRECT - stub returns value, no expectations $container = $this->createStub(ContainerInterface::class); $container->method('get')->willReturnCallback(static fn(string $id): mixed => ...); -// CORRECT — mock verifies behavior with expects() +// CORRECT - mock verifies behavior with expects() $handler = $this->createMock(ErrorHandler::class); $handler->expects($this->once())->method('attachListener'); -// WRONG — createMock() with no expects() triggers PHPUnit notice +// WRONG - createMock() with no expects() triggers PHPUnit notice $logger = $this->createMock(Logger::class); $logger->method('withName')->willReturn($logger); // should be createStub() ``` @@ -71,6 +71,6 @@ $logger->method('withName')->willReturn($logger); // should be createStub() `phpunit.xml.dist` has `requireCoverageMetadata="true"`. Every test class **must** have: -1. `#[CoversClass(ClassName::class)]` — one per source class under test. -2. `#[CoversMethod(ClassName::class, 'methodName')]` — one per public/protected method exercised. +1. `#[CoversClass(ClassName::class)]` - one per source class under test. +2. `#[CoversMethod(ClassName::class, 'methodName')]` - one per public/protected method exercised. 3. `use PHPUnit\Framework\Attributes\CoversClass;` and `use PHPUnit\Framework\Attributes\CoversMethod;` imports. diff --git a/.github/instructions/memory.instructions.md b/.github/instructions/memory.instructions.md index a64190d..c65a4b5 100644 --- a/.github/instructions/memory.instructions.md +++ b/.github/instructions/memory.instructions.md @@ -1,5 +1,5 @@ --- -description: Session handoff for webware-theme — theme resolver state, spec-kit location, and where the decision log and standing rules live, as of 2026-10-01. +description: Session handoff for webware-theme - theme resolver state, spec-kit location, and where the decision log and standing rules live, as of 2026-10-01. applyTo: '**/*' --- @@ -10,9 +10,9 @@ default theme and the template port are open. Everything needed to continue is i ## Start here -1. `specs/001-theme-resolution/decisions.md` — current state, the owner's standing rules, the decision log +1. `specs/001-theme-resolution/decisions.md` - current state, the owner's standing rules, the decision log (D-001 onwards) with statuses, next actions, gotchas. **Read it first.** -2. `specs/001-theme-resolution/tasks.md` — what is done and what is left; the next free task ID is stated at +2. `specs/001-theme-resolution/tasks.md` - what is done and what is left; the next free task ID is stated at the top. 3. `spec.md` (requirements), `plan.md` (design, with a built-versus-planned table), `data-model.md` (configuration shape), `research.md` (measurements), `quickstart.md`. diff --git a/.github/skills/speckit-analyze/SKILL.md b/.github/skills/speckit-analyze/SKILL.md index 8b2016d..da43f0e 100644 --- a/.github/skills/speckit-analyze/SKILL.md +++ b/.github/skills/speckit-analyze/SKILL.md @@ -60,7 +60,7 @@ Identify inconsistencies, duplications, ambiguities, and underspecified items ac **STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually). -**Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit-analyze`. +**Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks - not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit-analyze`. ## Execution Steps @@ -83,7 +83,7 @@ Load only the minimal necessary context from each artifact: - Overview/Context - Functional Requirements -- Success Criteria (measurable outcomes — e.g., performance, security, availability, user success, business impact) +- Success Criteria (measurable outcomes - e.g., performance, security, availability, user success, business impact) - User Stories - Edge Cases (if present) diff --git a/.github/skills/speckit-checklist/SKILL.md b/.github/skills/speckit-checklist/SKILL.md index 2285e3c..752fe7a 100644 --- a/.github/skills/speckit-checklist/SKILL.md +++ b/.github/skills/speckit-checklist/SKILL.md @@ -107,11 +107,11 @@ You **MUST** consider the user input before proceeding (if not empty). - Depth calibration (e.g., "Is this a lightweight pre-commit sanity list or a formal release gate?") - Audience framing (e.g., "Will this be used by the author only or peers during PR review?") - Boundary exclusion (e.g., "Should we explicitly exclude performance tuning items this round?") - - Scenario class gap (e.g., "No recovery flows detected—are rollback / partial failure paths in scope?") + - Scenario class gap (e.g., "No recovery flows detected - are rollback / partial failure paths in scope?") Question formatting rules: - If presenting options, generate a compact table with columns: Option | Candidate | Why It Matters - - Limit to A–E options maximum; omit table if a free-form answer is clearer + - Limit to A - E options maximum; omit table if a free-form answer is clearer - Never ask the user to restate what they already said - Avoid speculative categories (no hallucination). If uncertain, ask explicitly: "Confirm whether X belongs in scope." diff --git a/.github/skills/speckit-clarify/SKILL.md b/.github/skills/speckit-clarify/SKILL.md index b445bda..49b3590 100644 --- a/.github/skills/speckit-clarify/SKILL.md +++ b/.github/skills/speckit-clarify/SKILL.md @@ -128,7 +128,7 @@ Execution steps: 4. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints: - Maximum of 5 total questions across the whole session. - Each question must be answerable with EITHER: - - A short multiple‑choice selection (2–5 distinct, mutually exclusive options), OR + - A short multiple‑choice selection (2-5 distinct, mutually exclusive options), OR - A one-word / short‑phrase answer (explicitly constrain: "Answer in <=5 words"). - Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation. - Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved. @@ -140,7 +140,7 @@ Execution steps: - Present EXACTLY ONE question at a time. - **Question writing quality (applies to every question, MC or short-answer):** - Lead with `**Question:**` followed by a full interrogative that ends with `?`. The question text before the `?` must make sense on its own. - - NEVER use a topic label, section heading, or requirement id as the question itself. For example, `Acceptance device/runtime matrix (FR-023)` is INVALID — it is a label, not a question. + - NEVER use a topic label, section heading, or requirement id as the question itself. For example, `Acceptance device/runtime matrix (FR-023)` is INVALID - it is a label, not a question. - After the `?`, the only permitted suffix is an optional parenthesized requirement/question id. Exact format: `**Question:** ?` or `**Question:** ? (FR-023)`. Never put the id before the `?`, and never use the id (alone or with a topic label) as the whole prompt. - Immediately after the question line, add one plain-language "Why it matters" sentence (the stake for acceptance or shipping) before the recommendation/options. - Use everyday wording; introduce jargon only if defined in the same sentence. Self-check: a reader who does not know Spec Kit must be able to answer from the Question line alone. Terse is fine; cryptic labels are not. @@ -211,14 +211,14 @@ Execution steps: - If it does NOT exist, skip this step silently. - If it exists: 1. Read the checklist file. - 2. Identify all GitHub task-list checkbox lines — lines matching `- [ ]`, `- [x]`, or `- [X]` (case-insensitive, tolerant of leading whitespace for nested items) outside of code fences. Ignore all other content (headings, notes, non-checkbox bullets, metadata). + 2. Identify all GitHub task-list checkbox lines - lines matching `- [ ]`, `- [x]`, or `- [X]` (case-insensitive, tolerant of leading whitespace for nested items) outside of code fences. Ignore all other content (headings, notes, non-checkbox bullets, metadata). 3. For each checkbox line, record its current marker state (checked or unchecked) and item text into a before-snapshot list. 4. Re-evaluate each checkbox item against the **updated** spec (the version just saved in step 7). 5. For each checkbox item, update only if the checked/unchecked state actually changes: - If the item now passes and was unchecked: change `[ ]` to `[x]`. - If the item now fails and was checked: change `[x]`/`[X]` to `[ ]`. - If the state is unchanged: leave the marker as-is (preserve existing case to avoid cosmetic diffs). - 6. Save the updated checklist file. **Only toggle the `[ ]`/`[x]` marker portion of checkbox lines whose state changed.** All other file content — headings, metadata, notes, line ordering, whitespace — must remain unchanged to avoid noisy diffs. + 6. Save the updated checklist file. **Only toggle the `[ ]`/`[x]` marker portion of checkbox lines whose state changed.** All other file content - headings, metadata, notes, line ordering, whitespace - must remain unchanged to avoid noisy diffs. 7. Compare the before-snapshot with the current state to compute three lists for the Completion Report: - **Newly passing**: items that changed from unchecked to checked. - **Regressions**: items that changed from checked to unchecked. @@ -251,7 +251,7 @@ Check if `.specify/extensions.yml` exists in the project root. - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation - When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - - **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: + - **Mandatory hook** (`optional: false`) - **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: ``` ## Extension Hooks @@ -278,7 +278,7 @@ Report completion (after questioning loop ends or early termination): - Number of questions asked & answered. - Path to updated spec. - Sections touched (list names). -- Spec quality checklist status (if `FEATURE_DIR/checklists/requirements.md` was re-validated): show before/after pass counts (e.g., "Spec Quality Checklist: 12/16 → 15/16 items passing") and list any items that changed state — both newly checked (unchecked → checked) and any regressions (checked → unchecked). If any items remain unchecked, list them as areas needing attention. +- Spec quality checklist status (if `FEATURE_DIR/checklists/requirements.md` was re-validated): show before/after pass counts (e.g., "Spec Quality Checklist: 12/16 → 15/16 items passing") and list any items that changed state - both newly checked (unchecked → checked) and any regressions (checked → unchecked). If any items remain unchecked, list them as areas needing attention. - Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota or better suited for planning), Clear (already sufficient), Outstanding (still Partial/Missing but low impact). - If any Outstanding or Deferred remain, recommend whether to proceed to `/speckit-plan` or run `/speckit-clarify` again later post-plan. - Suggested next command. diff --git a/.github/skills/speckit-constitution/SKILL.md b/.github/skills/speckit-constitution/SKILL.md index 6a37619..04eef81 100644 --- a/.github/skills/speckit-constitution/SKILL.md +++ b/.github/skills/speckit-constitution/SKILL.md @@ -103,7 +103,7 @@ Follow this execution flow: - If version bump type ambiguous, propose reasoning before finalizing. 3. Draft the updated constitution content using the resolved template as the required structure: - - Replace every placeholder with concrete text (no bracketed tokens left except intentionally retained template slots that the project has chosen not to define yet—explicitly justify any left). + - Replace every placeholder with concrete text (no bracketed tokens left except intentionally retained template slots that the project has chosen not to define yet - explicitly justify any left). - Preserve heading hierarchy and comments can be removed once replaced unless they still add clarifying guidance. - Ensure each Principle section: succinct name line, paragraph (or bullet list) capturing non‑negotiable rules, explicit rationale if not obvious. - Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations. diff --git a/.github/skills/speckit-converge/SKILL.md b/.github/skills/speckit-converge/SKILL.md index b3397b5..0e3c02b 100644 --- a/.github/skills/speckit-converge/SKILL.md +++ b/.github/skills/speckit-converge/SKILL.md @@ -69,7 +69,7 @@ of remaining work as a new, traceable task** at the bottom of `tasks.md` so that `/speckit-implement` has run on the current `tasks.md`, and after `/speckit-tasks` has produced a complete `tasks.md`. This is **not** a diff tool and does **not** track changes. It assesses the present state -of the code relative to the feature's artifacts — no git, no branch comparison, no history. +of the code relative to the feature's artifacts - no git, no branch comparison, no history. ## Operating Constraints @@ -79,7 +79,7 @@ of the code relative to the feature's artifacts — no git, no branch comparison - modify `spec.md` or `plan.md` in any way; - rewrite, renumber, reorder, or delete any existing task (including tasks from a prior Convergence phase); -- modify, create, or delete any application code — completing the appended tasks is the +- modify, create, or delete any application code - completing the appended tasks is the job of `/speckit-implement`. When the codebase already satisfies everything, the command MUST leave `tasks.md` @@ -112,7 +112,7 @@ Load only the minimal necessary context from each artifact: **From spec.md:** - Functional Requirements (FR-###) -- Success Criteria (SC-###) — include only items requiring buildable work; exclude +- Success Criteria (SC-###) - include only items requiring buildable work; exclude post-launch outcome metrics and business KPIs - User Stories and their Acceptance Scenarios - Edge Cases (if present) @@ -142,7 +142,7 @@ Create an internal model (do not echo raw artifacts): impose buildable obligations. - **Code-scope map**: from the file paths named in `plan.md` and `tasks.md`, plus a keyword search for the concepts each requirement describes, derive the set of source files and - components in scope for assessment. Bound the assessment to these — do **not** infer + components in scope for assessment. Bound the assessment to these - do **not** infer scope beyond what the artifacts define. ### 4. Assess the Codebase and Classify Findings @@ -156,7 +156,7 @@ For each item in the intent inventory, inspect the current code in scope and pro - **`contradicts`**: the code does something that conflicts with stated intent or a constitution MUST principle. - **`unrequested`**: the code contains work not called for by the spec, plan, or tasks - (surfaced for awareness — converge does **not** delete code, it only appends a task to + (surfaced for awareness - converge does **not** delete code, it only appends a task to review/justify or remove it). Each `Finding` records: a stable id, the `source-ref` it traces to, the `gap-type`, a @@ -192,7 +192,7 @@ Before appending anything, output a compact, severity-graded summary (no file wr - Requirements / acceptance criteria checked - Plan decisions checked -- Constitution principles checked (or "skipped — template") +- Constitution principles checked (or "skipped - template") - Findings by gap type (missing / partial / contradicts / unrequested) - Findings by severity @@ -220,12 +220,12 @@ Append to the **end** of `tasks.md`, per the append contract: Constitution-violation tasks MUST be emitted first and described as `CRITICAL`. 4. Never reuse or renumber existing IDs. If a prior Convergence phase exists, add a new, - separately-numbered one below it — do not touch the old one. + separately-numbered one below it - do not touch the old one. **If there are no actionable findings** (`converged` outcome): -- Do **not** modify `tasks.md` at all — no empty phase header. -- Report: **"✅ Converged — the implementation satisfies the spec, plan, and tasks."** +- Do **not** modify `tasks.md` at all - no empty phase header. +- Report: **"✅ Converged - the implementation satisfies the spec, plan, and tasks."** - Include the summary counts of what was checked. ### 8. Provide Next Actions (Handoff) diff --git a/.github/skills/speckit-implement/SKILL.md b/.github/skills/speckit-implement/SKILL.md index 0d6a7a7..8f5724d 100644 --- a/.github/skills/speckit-implement/SKILL.md +++ b/.github/skills/speckit-implement/SKILL.md @@ -193,7 +193,7 @@ Check if `.specify/extensions.yml` exists in the project root. - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation - When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - - **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: + - **Mandatory hook** (`optional: false`) - **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: ``` ## Extension Hooks diff --git a/.github/skills/speckit-plan/SKILL.md b/.github/skills/speckit-plan/SKILL.md index 972fded..c525cfc 100644 --- a/.github/skills/speckit-plan/SKILL.md +++ b/.github/skills/speckit-plan/SKILL.md @@ -80,7 +80,7 @@ Check if `.specify/extensions.yml` exists in the project root. - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation - When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - - **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: + - **Mandatory hook** (`optional: false`) - **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: ``` ## Extension Hooks diff --git a/.github/skills/speckit-specify/SKILL.md b/.github/skills/speckit-specify/SKILL.md index 6f6dd9d..e330b69 100644 --- a/.github/skills/speckit-specify/SKILL.md +++ b/.github/skills/speckit-specify/SKILL.md @@ -83,7 +83,7 @@ Given that feature description, do this: **Resolution order for `SPECIFY_FEATURE_DIRECTORY`**: 1. If the user explicitly provided `SPECIFY_FEATURE_DIRECTORY` (e.g., via environment variable, argument, or configuration), use it as-is 2. Otherwise, auto-generate it under `specs/`: - - Check `.specify/init-options.json` for `feature_numbering` (preferred) or `branch_numbering` (deprecated, migration only — will be removed in a future release) + - Check `.specify/init-options.json` for `feature_numbering` (preferred) or `branch_numbering` (deprecated, migration only - will be removed in a future release) - If `"timestamp"`: prefix is `YYYYMMDD-HHMMSS` (current timestamp) - If `"sequential"` or absent: prefix is `NNN` (next available 3-digit number after scanning existing directories in `specs/`) - Construct the directory name: `-` (e.g., `003-user-auth` or `20260319-143022-user-auth`) @@ -106,7 +106,7 @@ Given that feature description, do this: **IMPORTANT**: - You must only create one feature per `/speckit-specify` invocation - - The spec directory name and the git branch name are independent — they may be the same but that is the user's choice + - The spec directory name and the git branch name are independent - they may be the same but that is the user's choice - The spec directory and file are always created by this command, never by the hook 4. Load the resolved active `spec-template` file to understand required sections. @@ -246,7 +246,7 @@ Check if `.specify/extensions.yml` exists in the project root. - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation - When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - - **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: + - **Mandatory hook** (`optional: false`) - **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: ``` ## Extension Hooks @@ -270,8 +270,8 @@ Check if `.specify/extensions.yml` exists in the project root. ## Completion Report Report completion to the user with: -- `SPECIFY_FEATURE_DIRECTORY` — the feature directory path -- `SPEC_FILE` — the spec file path +- `SPECIFY_FEATURE_DIRECTORY` - the feature directory path +- `SPEC_FILE` - the spec file path - Checklist results summary - Readiness for the next phase (`/speckit-clarify` or `/speckit-plan`) diff --git a/.github/skills/speckit-tasks/SKILL.md b/.github/skills/speckit-tasks/SKILL.md index d6888df..2be6dc2 100644 --- a/.github/skills/speckit-tasks/SKILL.md +++ b/.github/skills/speckit-tasks/SKILL.md @@ -100,7 +100,7 @@ Check if `.specify/extensions.yml` exists in the project root. - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation - When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - - **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: + - **Mandatory hook** (`optional: false`) - **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: ``` ## Extension Hooks diff --git a/.github/skills/speckit-taskstoissues/SKILL.md b/.github/skills/speckit-taskstoissues/SKILL.md index 0612de3..ca6fdca 100644 --- a/.github/skills/speckit-taskstoissues/SKILL.md +++ b/.github/skills/speckit-taskstoissues/SKILL.md @@ -66,7 +66,7 @@ git config --get remote.origin.url > [!CAUTION] > ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL -1. **Fetch existing issues for deduplication**: Before creating anything, build the set of task IDs you are about to process from `tasks.md` (each is a `T` followed by **at least** three digits, e.g. `T001` — `/speckit-converge` assigns new IDs with `T{M+1:03d}`, which is a floor rather than a cap, so once a file has more than 999 tasks the IDs are four digits or longer). Then use the GitHub MCP server's `list_issues` tool to look for issues that already cover those IDs. Do not pass a `state` value, since omitting it makes the tool return both open and closed issues. Request `perPage: 100` to keep the number of calls down, and since the tool uses cursor-based pagination, request pages with the `after` parameter (using the `endCursor` from the previous response). For each issue title, match it against the task ID pattern `\bT\d{3,}\b` (the `{3,}` accepts four-digit and longer IDs — with `\d{3}` a title containing `T1000` would not match at all, because the trailing `\b` cannot fall between two digits, so that task would be silently neither deduplicated nor created; word boundaries still stop a token like `ST001` from matching, and force the whole digit run to be consumed so `T100` can never match inside `T1000`; this also recognises titles written as `T001 ...`, `T001: ...` or `[T001] ...`) and, when it matches one of your task IDs, mark that ID as already having an issue. Stop paginating as soon as every task ID has been matched, or when there are no more pages, so you do not keep fetching the whole repository's issue history once all task IDs are accounted for. This bounds the number of calls on repos with large issue histories and still prevents duplicates when the command is re-run after `tasks.md` is regenerated or the skill is re-invoked. +1. **Fetch existing issues for deduplication**: Before creating anything, build the set of task IDs you are about to process from `tasks.md` (each is a `T` followed by **at least** three digits, e.g. `T001` - `/speckit-converge` assigns new IDs with `T{M+1:03d}`, which is a floor rather than a cap, so once a file has more than 999 tasks the IDs are four digits or longer). Then use the GitHub MCP server's `list_issues` tool to look for issues that already cover those IDs. Do not pass a `state` value, since omitting it makes the tool return both open and closed issues. Request `perPage: 100` to keep the number of calls down, and since the tool uses cursor-based pagination, request pages with the `after` parameter (using the `endCursor` from the previous response). For each issue title, match it against the task ID pattern `\bT\d{3,}\b` (the `{3,}` accepts four-digit and longer IDs - with `\d{3}` a title containing `T1000` would not match at all, because the trailing `\b` cannot fall between two digits, so that task would be silently neither deduplicated nor created; word boundaries still stop a token like `ST001` from matching, and force the whole digit run to be consumed so `T100` can never match inside `T1000`; this also recognises titles written as `T001 ...`, `T001: ...` or `[T001] ...`) and, when it matches one of your task IDs, mark that ID as already having an issue. Stop paginating as soon as every task ID has been matched, or when there are no more pages, so you do not keep fetching the whole repository's issue history once all task IDs are accounted for. This bounds the number of calls on repos with large issue histories and still prevents duplicates when the command is re-run after `tasks.md` is regenerated or the skill is re-invoked. 1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote. Task lines in `tasks.md` start with a markdown checkbox, so first strip the leading `- [ ]` (and any `[P]` / `[US#]` markers) to recover the task ID and its description. Create the issue with a single canonical title of the form `T001: `, with the ID written once followed by the task description (for example, the line `- [ ] T001 Create project structure` becomes the title `T001: Create project structure`). - **Skip** any task whose ID is already present in the set of existing issues from the previous step, and report it (for example, `T001 already has an issue, skipping`). - Only create issues for tasks that do not yet have a matching issue. diff --git a/Dockerfile b/Dockerfile index 7e9cea2..4ff0c47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ # # Builds a self-contained, interactive PHP development environment carrying # the Webware toolchain: PHP CLI, Composer, Mago, and Xdebug. Developers work -# inside this container — either through the VS Code Dev Container (backed by +# inside this container: either through the VS Code Dev Container (backed by # compose.yml) or by `docker compose up -d` + `docker compose exec tooling`. # The host machine (Windows, WSL, Linux, or macOS) never needs a native PHP # toolchain. diff --git a/README.md b/README.md index 4d56e1c..9d4ad20 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ config: | Path | Role | |---|---| | `mago.toml` | Extends the centre (`vendor/webware/webware-tools/mago.toml`) and overrides `php-version` only. Never re-add general rules locally. | -| `webware-ci.json` | The required CI workflow's parameter contract — read from the repository root by `webinertia/.github`. | +| `webware-ci.json` | The required CI workflow's parameter contract - read from the repository root by `webinertia/.github`. | | `phpunit.xml.dist` | PHPUnit 13 strict mode: `requireCoverageMetadata`, `failOnNotice`, `failOnWarning`, `failOnDeprecation`. | | `compose.yml` / `Dockerfile` / `.devcontainer/` | The containerized toolchain (Composer, PHPUnit, Mago, Infection, PHPBench, roave BC-check). | | `src/ConfigProvider.php` | The package wiring entry point, declared under `extra.laminas.config-provider`. | @@ -99,12 +99,12 @@ config: `mago.toml`, `phpunit.xml.dist`, `.gitattributes`, `codecov.yml`, `Dockerfile`, `.dockerignore`, `infection.json5.dist`, `phpbench.json.dist` and devcontainer config are byte-identical to the canonical artifacts in -`webware-tools/presets/webware-alignment/artifacts/` — copy updates from there rather than +`webware-tools/presets/webware-alignment/artifacts/` - copy updates from there rather than editing them here. ## Quality gates -Both MSI gates are set to **95** — the ecosystem standard, not a starting point. Lower them +Both MSI gates are set to **95**: the ecosystem standard, not a starting point. Lower them only with a deliberate decision, and never silently: ```json @@ -114,7 +114,7 @@ only with a deliberate decision, and never silently: Four Mago gates run in CI and must be clean: `format --check`, `lint`, `analyze`, `guard`. Run `mago fmt` first when making changes, and fix findings at source rather than adding -`@mago-expect` — a suppression needs to be a decision, not a reflex. +`@mago-expect` - a suppression needs to be a decision, not a reflex. ## Development diff --git a/compose.yml b/compose.yml index f6a61c3..2a7a5a1 100644 --- a/compose.yml +++ b/compose.yml @@ -9,7 +9,7 @@ # This compose file is the single source of truth for the environment. Two # ways to use it: # -# 1. VS Code Dev Container — "Reopen in Container" uses +# 1. VS Code Dev Container: "Reopen in Container" uses # .devcontainer/devcontainer.json, which is *backed by* this file (it # references the `tooling` service rather than defining its own). # @@ -24,7 +24,7 @@ # Packages whose tests need MySQL uncomment the `mysql` service below (and the # `phpmyadmin` web UI if wanted), plus the `depends_on` block on `tooling`. The # `mysql` service mirrors the `db_image` package parameter used by CI, and is -# reachable from `tooling` as host `mysql` on port 3306 — matching the PhpDb +# reachable from `tooling` as host `mysql` on port 3306: matching the PhpDb # `mysql.local.php` connection convention. Packages that need a different # engine add an equivalent service here (postgres, mariadb, ...) mirroring # their own `db-image` parameter. @@ -56,13 +56,13 @@ services: - "host.docker.internal:host-gateway" volumes: # Bind-mount the source so edits are visible on both the host and the - # container. vendor/ is NOT overridden — it is the host's own copy. + # container. vendor/ is NOT overridden - it is the host's own copy. # Composer/installs inside the container write straight into the # developer's working tree on disk. - .:/app # The Composer cache lives in a named volume (Linux-native I/O, fast on # Windows) so downloads survive container rebuilds. It is only a cache - # — the developer's real artifacts all live on the host. + # - the developer's real artifacts all live on the host. - composer-cache:/tmp/composer-cache # Uncomment when the package's tests need MySQL (see the `mysql` service # below). Ensures the database is ready before the shell attaches. @@ -72,7 +72,7 @@ services: working_dir: /app # ------------------------------------------------------------------------- - # MySQL (optional) — uncomment this whole block for packages whose tests + # MySQL (optional) - uncomment this whole block for packages whose tests # need a MySQL database (e.g. webware-acl via php-db/phpdb-mysql). # # Mirror the spec's `db_image` / `db_env_json` / `db_port` Package Parameters @@ -81,7 +81,7 @@ services: # credentials below. Adjust MYSQL_DATABASE / MYSQL_USER / MYSQL_PASSWORD to # the package's test-database convention. MYSQL_ALLOW_EMPTY_PASSWORD keeps # the test-only database free of a root password and in sync with the CI - # `db_env_json` — remove it if the package pins a root password instead. + # `db_env_json` - remove it if the package pins a root password instead. # ------------------------------------------------------------------------- # mysql: # image: mysql:9.7 @@ -101,7 +101,7 @@ services: # # survives `docker compose down` (use `down -v` to wipe it). # - mysql-data:/var/lib/mysql # - # # phpMyAdmin — optional web UI for the `mysql` service above. Reachable at + # # phpMyAdmin - optional web UI for the `mysql` service above. Reachable at # # http://localhost:8080 (log in as root with an empty password, or with # # the MYSQL_USER / MYSQL_PASSWORD credentials). Uncomment alongside # # `mysql`. diff --git a/specs/001-theme-resolution/data-model.md b/specs/001-theme-resolution/data-model.md index d7fa5a2..c1553f4 100644 --- a/specs/001-theme-resolution/data-model.md +++ b/specs/001-theme-resolution/data-model.md @@ -13,7 +13,7 @@ Identity is the directory name. There is no registry and no per-theme metadata f | Field | Type | Rules | |---|---|---| | `name` | `non-empty-string` | A single path segment: no directory separators, no `..`, not empty. It is used as a directory name under `templates/`, so anything else is a path-escape (T002) | -| `root` | — | **Superseded (D-006):** there is no root field. A theme is a key in `theme.themes` (template overrides) and in `theme.assets` (asset values) | +| `root` | - | **Superseded (D-006):** there is no root field. A theme is a key in `theme.themes` (template overrides) and in `theme.assets` (asset values) | `default` is the reserved name: every module that ships templates ships `default`, and it is the fallback consulted after the active theme. It cannot collide with mezzio's internal unnamespaced bucket, which is @@ -21,8 +21,8 @@ the private constant `__DEFAULT__` and unreachable as a template namespace. ## The layout on disk -Every module — a package, or a module the application ships itself (`App` is just one of them, shipped by -`webware/webware`) — keeps its templates as `/templates///.phtml`, and a +Every module - a package, or a module the application ships itself (`App` is just one of them, shipped by +`webware/webware`) - keeps its templates as `/templates///.phtml`, and a template is addressed `::`. The theme is **not** part of the address. ``` @@ -42,21 +42,21 @@ src/ims-store/templates/ # a module the application ships itself ``` A vendor template comes from the vendor module's own `templates/default//` and is overridden by -any module that places that namespace under its own theme directory — which is how the application +any module that places that namespace under its own theme directory - which is how the application overrides `admin::dashboard.phtml` without touching the package that ships it. ## Resolution -For a namespace, the resolver assembles the paths that namespace is served from — module by module, and -within each module the active theme's directory before `default` — and addresses resolve against that. +For a namespace, the resolver assembles the paths that namespace is served from - module by module, and +within each module the active theme's directory before `default` - and addresses resolve against that. Per-template fallback falls out of the ordering: a template the active theme does not ship resolves from `default`, and the framework's own resolvers take anything not theme-owned at all. Both questions below are **settled** (D-002, D-003). The list is kept so the alternatives stay on record: -- **Module order** for a namespace served by more than one module — presumably the application's own +- **Module order** for a namespace served by more than one module - presumably the application's own module first. -- **Namespace-keyed paths or theme-keyed maps** — the constructions worth prototyping, since each decides +- **Namespace-keyed paths or theme-keyed maps**: the constructions worth prototyping, since each decides whether the active theme is a build-time or a lookup-time fact: - *namespace-keyed paths*: mezzio's own `templates.paths` shape, with the active theme's directory prepended per namespace (walked per lookup). @@ -66,7 +66,7 @@ Both questions below are **settled** (D-002, D-003). The list is kept so the alt - *merge-last theme map*: a theme ships its overrides as an address-keyed map and is merged **last**, so it wins by the same later-wins rule that makes `templates.map` an override channel today. Every address the theme does not list keeps the earlier entry, so per-address fallback is the absence of a - key — no fallback logic at all — and the runtime stays a plain `TemplateMapResolver`. Build-time + key - no fallback logic at all - and the runtime stays a plain `TemplateMapResolver`. Build-time theme. The key of a map is already the address the renderer asks for, which is why the map is the override @@ -116,7 +116,7 @@ which is why this shape has none). | Output | an absolute path, or `false` so the aggregate continues | | Build | the name→path map is built once, at construction | | Memoization | resolved names are remembered per instance; nothing above memoizes (`research.md`) | -| Failure | never throws for an unknown name — throwing would break the aggregate's fallback | +| Failure | never throws for an unknown name - throwing would break the aggregate's fallback | ## Theme installer @@ -131,8 +131,8 @@ not hand-write `public/theme//…` any more than it hand-writes a lock fi Still open: how far "manage" goes (create only, update on change, remove on uninstall), and whether the installer is a console command in this package or lives in the application. If it is a command here, the -fleet's console contract applies — registered under `Webware\Console\ConsoleInterface::class` with -lazy resolution — which makes `webware/webware-console` a dependency of this package, as it is for +fleet's console contract applies - registered under `Webware\Console\ConsoleInterface::class` with +lazy resolution - which makes `webware/webware-console` a dependency of this package, as it is for `webware/webware-migration`. ## Asset name diff --git a/specs/001-theme-resolution/decisions.md b/specs/001-theme-resolution/decisions.md index a2763d1..863fe6a 100644 --- a/specs/001-theme-resolution/decisions.md +++ b/specs/001-theme-resolution/decisions.md @@ -12,7 +12,7 @@ row and add a dated line; never delete a row. | Area | State | |---|---| | `ThemeResolver`, `ThemeResolverFactory`, `AggregateResolverFactory`, `ConfigProvider` | Built on `prototype/resolver` (commit `d1c05dc`). 7 unit tests, 5 integration tests. Mago `format`, `lint`, `analyze`, `guard` clean. Not merged: there is no PR for this branch yet. | -| Theme-aware `asset()` helper | Built on `feat/asset-helper` (stacked on `prototype/resolver`, Phase 10 T042–T046, T048; T047 docs open). `ConfigProvider` now has the key constants and registers `AssetFactory` under `view_helpers.factories`. | +| Theme-aware `asset()` helper | Built on `feat/asset-helper` (stacked on `prototype/resolver`, Phase 10 T042 - T046, T048; T047 docs open). `ConfigProvider` now has the key constants and registers `AssetFactory` under `view_helpers.factories`. | | Asset publishing (installer) | Not built. Requirements live in `webinertia/project-tracking#6` (the Webware component installer RFC). | | Default theme visuals | Worked out in `webinertia/default-theme#1`, a scratch space (D-021): the shipped stylesheet lives in `webinertia/webware` at `public/theme/default/css/theme.css`. | | Port of webware-acl, webware-admin, webware-usermanager templates to a neutral `default` theme | Started: usermanager PR #69 open, acl moved on an uncommitted branch. See `tasks.md` Phase 12. | @@ -67,7 +67,7 @@ contradicted; confirm if you disagree), **Proposed** (needs the owner), **Open** | D-009 | 2026-10-01 | The laminas-view config key is **`view_helper_config.asset.resource_map`**, not `view_manager.asset.resource_map` as earlier docs said. | Settled | Measured in `AssetFactory` (`assertArray('view_helper_config', …)`, `assertArray('asset', …)`, `assertArray('resource_map', …)`). | | D-010 | 2026-10-01 | Component behaviour JavaScript ships with the **component** (webware-acl for the ACL page), never with a theme. A theme ships CSS that styles the component's neutral hooks. | Accepted | The ACL page's behaviour is about 400 lines of the IMS app's `public/assets/js/app.js` (from line 301, "ACL Wizard controller"), selecting by `ims-acl-*` ids and classes and building markup strings that contain them. It cannot live in a theme. | | D-011 | 2026-10-01 | Component assets are stored **per theme**, so each theme can publish its own version of a component's assets: `public/theme//component//…`, URL `/theme//component//…`. There is no theme-neutral `public/component/` directory. The component ships a default set (its `default` theme); another theme may supply its own, and the per-name fallback to `default` (D-008) applies. | Settled (path shape is my reading of the owner's answer; correct it if you meant otherwise) | Owner, 2026-10-01: "it would be better if those were stored per theme so each theme could publish their own." Supersedes the earlier proposal of `public/component//`. Task T064. | -| D-012 | 2026-10-01 | **Installing is the composer plugin installer's job** (publishes theme and component assets from `vendor/` into `public/theme//`; requirements in `project-tracking#6`). **A command** in the Webware CLI provides: creating a new theme based on `default`, registering it, and switching the active theme. **An admin widget** lets an administrator pick the active theme from the installed themes. So `webware-theme` is not the installer; whether the command lives in this package (needs `webware/webware-console`, `Console\` guard rule: a `final` `*Command` with `#[AsCommand]`, factory in `Console\Container\`) or elsewhere, and where the widget lives (webware-admin `WidgetInterface`), is for the plan. | Settled (split of responsibilities); placement of the command and widget open | Owner, 2026-10-01. Tasks T032–T034 are re-scoped; T065–T068 added. The widget needs a list of installed themes and a persisted active theme, neither of which exists (see Next actions). | +| D-012 | 2026-10-01 | **Installing is the composer plugin installer's job** (publishes theme and component assets from `vendor/` into `public/theme//`; requirements in `project-tracking#6`). **A command** in the Webware CLI provides: creating a new theme based on `default`, registering it, and switching the active theme. **An admin widget** lets an administrator pick the active theme from the installed themes. So `webware-theme` is not the installer; whether the command lives in this package (needs `webware/webware-console`, `Console\` guard rule: a `final` `*Command` with `#[AsCommand]`, factory in `Console\Container\`) or elsewhere, and where the widget lives (webware-admin `WidgetInterface`), is for the plan. | Settled (split of responsibilities); placement of the command and widget open | Owner, 2026-10-01. Tasks T032 - T034 are re-scoped; T065 - T068 added. The widget needs a list of installed themes and a persisted active theme, neither of which exists (see Next actions). | | D-013 | 2026-10-01 | `ims-*` classes in the three components' templates are **styling and behaviour hooks whose CSS and JS live in the IMS app**, not IMS content. In the `default` theme they become neutral hooks: `ims-acl-X` → `acl-X`, `ims-widget-X` → `widget-X`, `ims-badge-xs` → `badge-xs`, `ims-rules-panel-` → `acl-rules-panel-`. `ims-col-*` (usermanager list) must **not** become `col-*`, which collides with Bootstrap's grid; use `list-col-*`. The rename changes markup and JS together or the page breaks. | Accepted | 84 rules in IMS `public/assets/css/custom.css`; the ACL templates have no inline script that selects by these names. | | D-014 | 2026-10-01 | The verbatim IMS originals of every ported template go into the `ims` theme in `webinertia/webware` (`src/App/templates/ims//`, mapped in `theme.global.php`), and the IMS CSS/JS go with that theme. | Accepted | The owner: "all of the ims specific stuff will get ported verbatim into the ims-theme". | | D-015 | 2026-10-01 | The shipped `default` theme is **stock Bootstrap 5.3.8 markup**, dark by default, with a Light/Dark control that persists in `localStorage` under `webware-theme`. No build step. Two logo colours: `#7e5ae0` is `primary`, `#05a578` is `secondary`. Component tokens are restated in `default-theme/css/theme.css` because Bootstrap's compiled components hard-code their blue. | Settled | `default-theme#1`. WCAG AA checked: white on purple 4.74:1, black on green 6.66:1, dark-mode purple text tint 6.50:1. | @@ -82,7 +82,7 @@ contradicted; confirm if you disagree), **Proposed** (needs the owner), **Open** 1. Open a PR for `prototype/resolver` (docs and prototype) so the work is reviewable. Do not merge without the owner. -2. Build the asset helper (Phase 10, T042–T048). It unblocks everything that serves CSS or JS. +2. Build the asset helper (Phase 10, T042 - T048). It unblocks everything that serves CSS or JS. 3. Land `default-theme#1` and `webware-usermanager#69` (owner review), then continue Phase 12 with the ACL templates: finish the rename (D-013) and extract the ACL JavaScript into webware-acl (D-010). 4. The owner's four answers are recorded (D-005, D-011, D-012, D-018). D-018 is **a new component, not yet diff --git a/specs/001-theme-resolution/plan.md b/specs/001-theme-resolution/plan.md index eb6b1ad..60d0c30 100644 --- a/specs/001-theme-resolution/plan.md +++ b/specs/001-theme-resolution/plan.md @@ -26,7 +26,7 @@ configuration. No middleware, no build step, no database. One consumer change is a **prerequisite**, because a resolver can only resolve a name it is given: the package that layers the body and layout (webware-htmx) must name each with one configuration value -(Phase 6, T039–T041 of `tasks.md`, and the required subset of webinertia/webware-htmx#21; the work is +(Phase 6, T039 - T041 of `tasks.md`, and the required subset of webinertia/webware-htmx#21; the work is PR `webinertia/webware-htmx#22`). Everything else in that RFC is enabled by this feature's convention but not required by it. @@ -36,7 +36,7 @@ in that RFC is enabled by this feature's convention but not required by it. - **Runtime deps**: `laminas/laminas-view ^3.0`, `psr/container ^2.0`. `mezzio/mezzio-laminasviewrenderer` and `webware/webware-htmx` stay consumers, not dependencies. - **Console (conditional)**: if the installer turns out to be a command in this package (T034), - `webware/webware-console` becomes a dependency — as it is for webware-migration — and the guard rule for + `webware/webware-console` becomes a dependency - as it is for webware-migration - and the guard rule for `Console\` applies: a `final` class named `*Command` carrying `#[Symfony\Component\Console\Attribute\AsCommand]`, directly in `Console\`, with its factory in `Console\Container\`. @@ -56,10 +56,10 @@ All measured in `webinertia/webware` against the installed `laminas/laminas-view | Fact | Source | Consequence | |---|---|---| -| `TemplateMapResolver::resolve()` is `$this->map[$name] ?? false` — no filesystem call | `laminas-view/src/Resolver/TemplateMapResolver.php` | Map lookup is the resolution mechanism; a theme contributes entries, not paths | +| `TemplateMapResolver::resolve()` is `$this->map[$name] ?? false` - no filesystem call | `laminas-view/src/Resolver/TemplateMapResolver.php` | Map lookup is the resolution mechanism; a theme contributes entries, not paths | | `TemplatePathStack::resolve()` → `resolveToPath()` creates an `SplFileInfo` and calls `isReadable()` per path, on hits and misses | `laminas-view/src/Resolver/TemplatePathStack.php` | Path stacking is the cost the resolver exists to remove (FR-005) | | `NamespacedPathStackResolver::resolve()` lazily builds one `TemplatePathStack` per namespace | `mezzio-laminasviewrenderer/src/NamespacedPathStackResolver.php` | One `namespace::name` per theme is one stat list per theme | -| `AggregateResolver::resolve()` `continue`s on `false`, returns the first hit | `laminas-view/src/Resolver/AggregateResolver.php` | Per-template fallback is free between resolvers — the hook the design uses | +| `AggregateResolver::resolve()` `continue`s on `false`, returns the first hit | `laminas-view/src/Resolver/AggregateResolver.php` | Per-template fallback is free between resolvers - the hook the design uses | | No `ResolveCache` anywhere in `laminas-view` 3.x `src` | grep, 2026-09-29 | Nothing memoizes resolution; the resolver memoizes its own results | | `ConfigAggregator::mergeArray()`: `is_int($key)` appends; string keys replace and recurse | `laminas-config-aggregator/src/ConfigAggregator.php` | Maps are the override channel (later wins); path lists give the first-registered provider priority | | `Laminas\View\Helper\Asset::__invoke()` throws on an unknown name; the class is `final readonly` and is built once by `AssetFactory` from `view_helper_config.asset.resource_map` (not `view_manager.asset`, as earlier drafts said) | `laminas-view/src/Helper/Asset.php`, `Helper/Service/AssetFactory.php`; re-measured 2026-10-01 | Asset names are a fixed vocabulary (FR-009). Theme awareness is a factory override that builds a merged map (D-008), not a subclass | @@ -71,7 +71,7 @@ All measured in `webinertia/webware` against the installed `laminas/laminas-view ## Constitution Check -No constitution is ratified in this repository — `.specify/memory/constitution.md` is the untouched +No constitution is ratified in this repository - `.specify/memory/constitution.md` is the untouched init template (`[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`, … placeholders intact). There are therefore no project principles to gate this plan against. The gates that do apply, and that any implementation must pass: the fleet agent working agreements, the four Mago gates, MSI 95/95 from `webware-ci.json`, @@ -124,7 +124,7 @@ test/ ``` The implementation is deliberately smaller than the doc set: a contract, a resolver, an installer and -wiring. There is no renderer subclass — `webware-htmx` keeps `View\LaminasRenderer` for body/layout +wiring. There is no renderer subclass - `webware-htmx` keeps `View\LaminasRenderer` for body/layout layering, and this package only answers "which file is this template name?" and "what does a theme's installed asset set look like?". diff --git a/specs/001-theme-resolution/quickstart.md b/specs/001-theme-resolution/quickstart.md index 35d0927..38e011f 100644 --- a/specs/001-theme-resolution/quickstart.md +++ b/specs/001-theme-resolution/quickstart.md @@ -23,14 +23,14 @@ public/theme/acme/ # this theme's assets, where they can be s Addresses stay mezzio's namespaced ones (`::`); the theme selects the first directory segment and is never part of the address. Templates and assets live in different places because they are -served differently — `templates/` is never a served location, so nothing under it can be requested by a +served differently - `templates/` is never a served location, so nothing under it can be requested by a browser. 1. Put the theme's templates in a directory of your choosing (the layout above is the convention) and list each address the theme overrides in `theme.themes.`. Anything not listed continues to come from `default`, address by address. 2. Its assets go under `public/theme//{css,js,img,fonts}/`, and their **names** are pointed at - them in `theme.assets.` (planned, T042; the asset helper does not read it yet) — the names + them in `theme.assets.` (planned, T042; the asset helper does not read it yet) - the names are the ones the components already use; you are changing values, not inventing names. 3. Activate it: @@ -49,7 +49,7 @@ return [ ]; ``` -4. Clear the aggregated config cache — the active theme is read from configuration, and a cached +4. Clear the aggregated config cache - the active theme is read from configuration, and a cached configuration will keep serving the previous theme. ## For a package author @@ -65,10 +65,10 @@ your-package/ └── layout/default.phtml -> layout::default ``` -Address templates the way mezzio does — `::`, where the namespace is one you choose +Address templates the way mezzio does - `::`, where the namespace is one you choose (`your-namespace`, or a shared one such as `layout`). An application that installs the package gets a working interface with no theme configuration; if you want to ship an alternate theme, add -`templates///` beside `default` — same convention, no registration. +`templates///` beside `default` - same convention, no registration. Rules that keep this working across packages: diff --git a/specs/001-theme-resolution/research.md b/specs/001-theme-resolution/research.md index 919607f..3c9e82b 100644 --- a/specs/001-theme-resolution/research.md +++ b/specs/001-theme-resolution/research.md @@ -12,7 +12,7 @@ re-derived rather than taken on faith. Measurements were taken against the insta mezzio addresses templates as `::`, and `NamespacedPathStackResolver` already owns one `TemplatePathStack` per namespace. This feature builds on that rather than replacing it: the theme becomes the **first directory segment** under a module's `templates/`, and the work is assembling, per namespace, -the paths that namespace is served from — the active theme's directory before `default` — so that "the +the paths that namespace is served from - the active theme's directory before `default` - so that "the theme does not ship this template" resolves from `default` with no special case anywhere. Earlier iterations instead pushed extra paths onto the resolver stack globally and modified the renderer to @@ -25,7 +25,7 @@ The earlier iterations of this component modified the renderer and pushed additi resolver stack. It worked, and it is discarded for cost reasons: - `TemplatePathStack::resolve()` → `resolveToPath()` loops its paths doing `new SplFileInfo($path . $name)` - and `isReadable()` — one stat per path, in order, **on a hit and on a miss**. + and `isReadable()` - one stat per path, in order, **on a hit and on a miss**. - `NamespacedPathStackResolver::resolve()` lazily constructs a `TemplatePathStack` **per namespace** and delegates. So each theme-as-namespace is its own stat list, and a template the active theme lacks walks the whole theme stack before the default stack is even consulted. @@ -38,21 +38,21 @@ for you, so whatever a resolver touches at resolve time, it pays per lookup. ## Why a resolver, and why maps - `AggregateResolver::resolve()` iterates its priority queue, `continue`s when a resolver returns - `false`, and returns the first hit. Per-template fallback is therefore **free between resolvers** — + `false`, and returns the first hit. Per-template fallback is therefore **free between resolvers**: this is the hook the design uses. -- `TemplateMapResolver::resolve()` is `return $this->map[$name] ?? false;` — **no filesystem call at +- `TemplateMapResolver::resolve()` is `return $this->map[$name] ?? false;` - **no filesystem call at all**. Map entries are the cheap layer. - Therefore the resolver contributes and consults **maps**, and the only path stack in play is the one the framework already has. Consequence recorded as FR-005: resolution must not add a filesystem walk per lookup. -## Configuration precedence — measured, and counter-intuitive +## Configuration precedence: measured, and counter-intuitive `ConfigAggregator::mergeConfig()` → `mergeArray()` (the same logic as `Laminas\Stdlib\ArrayUtils::merge`): - `is_int($key)` → `$a[] = $value`: integer keys **append**. A list from a later provider lands *after* - the earlier one, so for `templates.paths` — walked in list order — the **first-registered path wins a + the earlier one, so for `templates.paths` - walked in list order - the **first-registered path wins a same-named file**, i.e. packages would shadow a theme. - string keys → `$a[$key] = $value`, recursing into string-keyed sub-arrays: **later wins**. This is how an application's `templates.map['body::default']` override works today, and why maps are the override @@ -84,19 +84,19 @@ for paths that are not files). ## The webware-htmx body/layout layer -`LaminasRendererFactory` still reads **five** keys for one idea — `templates.layout`, `templates.body`, -`templates.default_layout`, `templates.default_body`, `view_manager.default_layout` — where the first +`LaminasRendererFactory` still reads **five** keys for one idea - `templates.layout`, `templates.body`, +`templates.default_layout`, `templates.default_body`, `view_manager.default_layout` - where the first non-empty becomes the layout and the **last** non-empty becomes the body, i.e. two precedence rules over one list. FR-012 collapses this to theme-resolved names. Related measurement: `webware-htmx/templates/body/default.phtml` is the IMS application shell (Farmers IMS branding, store list, Bootstrap navbar, `$this->navigation('main'|'admin'|'user')`) and calls -`$this->imsMessenger()`. That helper is **defined nowhere** — not in the application repository outside +`$this->imsMessenger()`. That helper is **defined nowhere**: not in the application repository outside `vendor`, and not in `webware/vendor` at all; the application's own copy of the file has the call commented out. A package shipping a default template cannot call a helper nothing provides, so the shell becomes the `ims` theme and the package ships a helper-clean `default` (US3). -## LCDD — no build step +## LCDD: no build step "Least Common Denominator Development": the default is htmx plus plain CSS plus the latest Bootstrap CSS, with Tailwind bridges later via asset helpers. Consequences that shaped the design: diff --git a/specs/001-theme-resolution/spec.md b/specs/001-theme-resolution/spec.md index 846170e..56318a2 100644 --- a/specs/001-theme-resolution/spec.md +++ b/specs/001-theme-resolution/spec.md @@ -19,7 +19,7 @@ default that they can then modify." ## Purpose Webware applications are built to be re-skinned. A design or development shop starts from a Webware -application, builds a client's site on top of it, and later refreshes that site — often years later — +application, builds a client's site on top of it, and later refreshes that site - often years later - without rewriting the application. This spec defines the mechanism that makes that possible: themes as a directory convention, resolved by name, with per-template fallback to the `default` theme every package ships. @@ -62,7 +62,7 @@ pages, verbatim, into an `ims` theme. ### User Story 1 - A client redesign ships as a theme, not a fork (Priority: P1) A shop that built a client's site on Webware is asked to refresh it. They create a theme that -overrides only what the redesign changes — a layout, a few templates, a stylesheet — and every other +overrides only what the redesign changes - a layout, a few templates, a stylesheet - and every other template continues to come from the packages, unchanged. **Why this priority:** this is the entire commercial reason the component exists. Without it, a @@ -78,7 +78,7 @@ renders from the package's `default` theme. `templates/acme/layout/default.phtml`, **When** `acme` is the active theme, **Then** the layout renders from `acme` and every template `acme` does not ship renders from `default`. 2. **Given** a theme that ships no `body` template, **When** a page renders, **Then** the body comes - from `default` and the page is complete — no error, no empty region. + from `default` and the page is complete - no error, no empty region. 3. **Given** an application with no theme configured, **When** it boots, **Then** the `default` theme is used and nothing asks the developer to configure a theme. @@ -115,7 +115,7 @@ call only helpers the Webware packages themselves provide. **Why this priority:** it removes an existing defect (a package template calling an application helper that does not exist) and proves the convention on a real, non-trivial theme. -**Independent Test:** install the packages without the IMS theme and render a page — no missing-helper +**Independent Test:** install the packages without the IMS theme and render a page - no missing-helper error; then activate `ims` and confirm the IMS shell renders with its own helpers available. **Acceptance Scenarios:** @@ -169,9 +169,9 @@ the IMS application renders today. ### Edge Cases -- The active theme directory does not exist at all — the application still renders from `default` +- The active theme directory does not exist at all - the application still renders from `default` rather than failing. -- A theme ships a template name that no default provides — the name is simply unknown, and the failure +- A theme ships a template name that no default provides - the name is simply unknown, and the failure is the same failure an unknown template name has always produced. - A theme's configured asset name is not present in the resource map. The asset helper throws on an unknown name, so asset names are a vocabulary: themes re-value names, they do not invent them. @@ -179,9 +179,9 @@ the IMS application renders today. at the theme that defined the name. - A theme name that is not a single path segment (`a/b`, `..`, empty) is rejected before it is used to build a path or a URL. -- The active theme is changed in configuration but the aggregated config is cached — a cached +- The active theme is changed in configuration but the aggregated config is cached - a cached configuration must be cleared for the switch to take effect, and this must be documented. -- Two components ship the same template name and different themes are active — resolution must be +- Two components ship the same template name and different themes are active - resolution must be deterministic, driven by the configured root order, not by directory scan order. ## Requirements *(mandatory)* @@ -196,7 +196,7 @@ agreements, the four Mago gates, and the MSI floors in `webware-ci.json`. - **FR-001**: A theme MUST be a directory named for the theme, directly inside the `templates/` directory of a module that ships or overrides templates. A module's theme directory holds one subdirectory per template namespace. -- **FR-002**: Templates MUST keep mezzio's namespaced addressing — `::`, e.g. +- **FR-002**: Templates MUST keep mezzio's namespaced addressing - `::`, e.g. `app::home-page.phtml`, `layout::default`, `admin::dashboard.phtml`. The theme is never part of the address; it selects the first directory segment under `templates/`. - **FR-003** *(revised 2026-10-01, D-002/D-003)*: Resolution MUST consult the active theme's template @@ -207,7 +207,7 @@ agreements, the four Mago gates, and the MSI floors in `webware-ci.json`. resolves from `default` in the same namespace, and a template no module ships falls through to the framework's own resolvers. - **FR-004a**: Any module MUST be able to override another module's template by placing that namespace - under its own theme directory — this is how an application overrides a vendor template without + under its own theme directory - this is how an application overrides a vendor template without touching the package that ships it. - **FR-005**: Resolution MUST NOT add a filesystem walk per lookup. Map lookups are the expected mechanism; any scan performed to build a map MUST happen once. @@ -231,7 +231,7 @@ agreements, the four Mago gates, and the MSI floors in `webware-ci.json`. MUST be a theme-resolvable template name (`body`, `layout`) rather than a path the owning package publishes as a map entry. This is what lets a theme point at a body template at all, so the collapse of webware-htmx's five-key lookup (`templates.layout`, `templates.body`, `templates.default_layout`, - `templates.default_body`, `view_manager.default_layout`) is a **prerequisite of this feature** — it is + `templates.default_body`, `view_manager.default_layout`) is a **prerequisite of this feature**: it is the one part of webinertia/webware-htmx#21 that is required here. The package reads the name and hands it to whatever resolver is registered in the aggregate; it does not depend on this package to do so. - **FR-013**: The shipped `default` theme MUST call only helpers that the package's own dependencies @@ -247,7 +247,7 @@ agreements, the four Mago gates, and the MSI floors in `webware-ci.json`. development, copies in production, is idempotent, and removes what a package no longer ships. The installer's own requirements are tracked in `webinertia/project-tracking#6` (D-012). Installing is the composer plugin installer's job; creating, registering and switching themes is a command, and choosing - one is an admin widget (D-012, tasks T065–T068). + one is an admin widget (D-012, tasks T065 - T068). - **FR-016**: The shipped `default` theme MUST use stock Bootstrap 5.3 markup and spacing, be dark by default with a Light/Dark control that persists the choice, and need no build step (D-015). - **FR-017**: The two colours of the webinertia logo MUST be the theme's `primary` (`#7e5ae0`) and @@ -282,7 +282,7 @@ agreements, the four Mago gates, and the MSI floors in `webware-ci.json`. scan relative to resolving one it does ship. - **SC-003**: A package's templates render in a consuming application with zero theme configuration present. -- **SC-004**: Switching the active theme is a configuration change plus a config-cache clear — no code +- **SC-004**: Switching the active theme is a configuration change plus a config-cache clear - no code change, no file move. - **SC-005**: Extracting the IMS shell into the `ims` theme leaves the rendered markup identical. - **SC-006**: Installing the packages without any application code renders a page without a diff --git a/specs/001-theme-resolution/tasks.md b/specs/001-theme-resolution/tasks.md index 4a92d4e..9554c59 100644 --- a/specs/001-theme-resolution/tasks.md +++ b/specs/001-theme-resolution/tasks.md @@ -14,7 +14,7 @@ Notes that apply to every task: `mago fmt` before any commit; all four gates cle `#[CoversMethod]` with the matching imports (`requireCoverageMetadata="true`); MSI gates are 95/95 from `webware-ci.json` and are not a per-task parameter. -## Phase 1 — Theme contract +## Phase 1: Theme contract - [X] T001 **Dropped (D-005).** No `ThemeInterface`; config key constants instead (T062). Original text: define `ThemeInterface` in `src/ThemeInterface.php`: the name, the root it resolves from, and nothing else. A theme is a directory; identity is its name (`@api`, since consumers @@ -25,13 +25,13 @@ Notes that apply to every task: `mago fmt` before any commit; all four gates cle - [X] T003 **Superseded (D-006, D-003).** Define the theme root contract: how a package declares `templates/` as a root, as a docblock `@type` alias in `ConfigProvider` in the house style (`@type`, not `@phpstan-type`) -## Phase 2 — Resolver +## Phase 2: Resolver - [X] T004 **Built** (`ThemeResolver`, 7 unit tests). Implement `Resolver\ThemeResolver` in `src/Resolver/ThemeResolver.php`, implementing - `Laminas\View\Resolver\ResolverInterface`. It MUST return `false` — never throw — for a name it + `Laminas\View\Resolver\ResolverInterface`. It MUST return `false` - never throw - for a name it cannot resolve, so `AggregateResolver` continues to the next resolver -- [X] T005 **Superseded (D-002, D-003).** Build, per namespace, the paths that namespace is served from — module by module, and within - each module the active theme's directory before `default` — so fallback is per template and not per +- [X] T005 **Superseded (D-002, D-003).** Build, per namespace, the paths that namespace is served from - module by module, and within + each module the active theme's directory before `default` - so fallback is per template and not per theme (FR-004) - [X] T006 **Done by D-002:** the maps are config arrays, the lookup is an array access, nothing to memoize. Resolve from a map built once, not by walking directories per lookup (FR-005): build the map when the resolver is constructed, and memoize resolved names in the instance @@ -43,7 +43,7 @@ Notes that apply to every task: `mago fmt` before any commit; all four gates cle - [X] T009 **Built** (`ThemeResolverFactory`, `AggregateResolverFactory`: theme 100, map 50, path stack 1). Register the resolver in `src/Resolver/Container/ThemeResolverFactory.php` and wire it to the aggregate ahead of the map/path resolvers, so an active theme wins over package defaults -## Phase 2b — Prototype the construction before committing to it +## Phase 2b: Prototype the construction before committing to it Next free IDs, following the convention above. Both shapes hang off two measured facts: a map's key is the address the renderer asks for (`app::home-page`), and the resolver gets a factory, so the active theme can @@ -58,18 +58,18 @@ be injected from configuration. becomes a lookup-time fact, and measure what building the maps costs (directory scan or manifest) - [ ] T037 **Not run** (D-002). Prototype **merge-last theme map**: a theme ships its overrides as an address-keyed map, and a provider or post-processor registered last merges it over the rest, so the theme wins by the same - rule that makes `templates.map` an override channel today — and every address the theme does not + rule that makes `templates.map` an override channel today - and every address the theme does not list keeps its earlier entry, which is per-address fallback for free. Runtime stays a plain `TemplateMapResolver`: no custom resolver, no stats. Confirm what the rebuild costs at config time and whether the config cache absorbs it - [X] T038 **Recorded in `decisions.md` D-002.** Write down which won and why (build-time theme vs lookup-time theme is the real fork), then fold it into the resolver tasks above rather than leaving two constructions in the code -## Phase 3 — Configuration +## Phase 3: Configuration - [X] T010 **Built** (`@type ThemeConfig`: `active`, `themes`; the `roots` list is gone, D-006). Define the configuration shape (active theme + roots + per-component roots) as a docblock `@type` alias, with `ConfigProvider` publishing the package's own defaults -- [X] T011 **Built.** Read configuration only — no container bootstrapping at render time, no database access, +- [X] T011 **Built.** Read configuration only - no container bootstrapping at render time, no database access, at boot or at render (FR-006) - [X] T012 **Built** (`theme.active` absent or empty means `default`; test `defaultsToTheDefaultThemeWhenNoneIsConfigured`). Select the active theme from configuration alone (FR-008) and default to `default` when nothing is configured, so an application with no theme configuration renders (US1 scenario 3) @@ -79,35 +79,35 @@ be injected from configuration. taking the namespace root's sibling `templates/` directory; skip `autoload-dev` prefixes and handle a prefix that maps to more than one directory (FR-011) -## Phase 4 — Asset names +## Phase 4: Asset names -- [X] T015 **Superseded by Phase 10 (T042–T048), D-007…D-009.** Define the resource-map contribution: the package and every component publish +- [X] T015 **Superseded by Phase 10 (T042 - T048), D-007…D-009.** Define the resource-map contribution: the package and every component publish `view_helper_config.asset.resource_map` entries; a theme re-values names and never introduces them (FR-009, and the `Asset` helper throws on an unknown name). Settle the exact asset path shape and - filenames under `public/theme//` while doing so — assets are served from there, never from + filenames under `public/theme//` while doing so - assets are served from there, never from a theme's `templates/` root -- [X] T016 **Superseded by Phase 10 (T042–T048).** Contribute the default theme's entries from `ConfigProvider` so that an application with no +- [X] T016 **Superseded by Phase 10 (T042 - T048).** Contribute the default theme's entries from `ConfigProvider` so that an application with no theme configuration resolves every asset name the shipped templates use -## Phase 4b — Theme installer (FR-015) +## Phase 4b: Theme installer (FR-015) Task IDs are assigned in the order they were added, so this phase carries the next free numbers rather than displacing the ones below it. - [ ] T032 **Re-scoped (D-012): installing belongs to the composer plugin installer** (`project-tracking#6`), not this package. Original text: implement the theme installer: given a theme, materialise its assets under - `public/theme//` from the theme's own source. Nothing here is a build step — it is the one + `public/theme//` from the theme's own source. Nothing here is a build step - it is the one place that writes into a served location - [ ] T033 **Open; requirements are now in `project-tracking#6`** (symlink in dev, copy in prod, idempotent, removes what a package stopped shipping). Decide and record how far "manage" goes: create only, update when the theme's assets change, and what happens on uninstall. Whichever way it goes, the installer owns it rather than leaving stale files in `public/` -- [ ] T034 **Re-scoped (D-012).** The *installer* is the composer plugin. The console command is for creating a theme from `default`, registering it and switching the active theme (T065–T067). Whether that command lives in this package (registered under +- [ ] T034 **Re-scoped (D-012).** The *installer* is the composer plugin. The console command is for creating a theme from `default`, registering it and switching the active theme (T065 - T067). Whether that command lives in this package (registered under `Webware\Console\ConsoleInterface::class`, which makes `webware/webware-console` a dependency, as it is for webware-migration) or elsewhere is still to record in `plan.md`. Original text: decide whether the installer is a console command in this package (registered under -## Phase 5 — The convention in the packages +## Phase 5: The convention in the packages - [ ] T017 Define the layout convention and write it into the package README: - `/templates///.phtml`, addressed as `::` — a theme + `/templates///.phtml`, addressed as `::` - a theme is a directory of namespace directories, and it is never part of the address - [ ] T018 Confirm the convention against a package with existing templates (webware-navigation, webware-htmx) and record any divergence rather than moving files in a package outside this @@ -115,7 +115,7 @@ than displacing the ones below it. - [X] T019 **Resolved by design (D-002, D-006):** maps are config, so precedence is the config merge (later wins), not a module order. Settle the cross-module question the convention leaves open: when more than one module serves the same namespace, which module's path is consulted first (record it in `plan.md`) -## Phase 6 — The htmx configuration prerequisite (FR-012) +## Phase 6: The htmx configuration prerequisite (FR-012) - [X] T039 **Done: `webinertia/webware-htmx#22` merged and `1.0.0-alpha.1` tagged by the owner on 2026-10-01.** Original text (merge state was `UNKNOWN`, re-check): Make webware-htmx read **one key per value** for the body and the layout, whose values are theme-resolvable template names, replacing the five-key lookup and the package-published @@ -127,7 +127,7 @@ than displacing the ones below it. - [ ] T041 **Open.** Keep webware-htmx free of a dependency on this package: it reads the configured name and hands it to the aggregate resolver, whichever package supplies it -## Phase 7 — Consumer-side work, deliberately not in this list +## Phase 7: Consumer-side work, deliberately not in this list Everything else in webinertia/webware-htmx#21 is work the convention *enables* but does not depend on: @@ -138,9 +138,9 @@ Everything else in webinertia/webware-htmx#21 is work the convention *enables* b - Renderer packaging, the `TemplateRendererInterface` alias, and how a team on another client-scripting approach is served -## Phase 8 — Tests +## Phase 8: Tests -- [ ] T022 **Partly met:** theme hit, per-address fallback and unknown-returns-`false` are tested; name validation (separators) is not, and belongs to T043. Unit: `test/unit/Resolver/ThemeResolverTest.php` — a theme hit, a per-template fallback to +- [ ] T022 **Partly met:** theme hit, per-address fallback and unknown-returns-`false` are tested; name validation (separators) is not, and belongs to T043. Unit: `test/unit/Resolver/ThemeResolverTest.php` - a theme hit, a per-template fallback to `default`, an unknown name returning `false`, and name validation rejecting separators - [ ] T023 **Superseded in intent by D-002** (there is no per-namespace ordering to test); re-scope to "two themes each carry the same address" if a test is wanted. Unit: the per-namespace ordering, including the case where two modules serve the same namespace and ship the same template name @@ -149,7 +149,7 @@ Everything else in webinertia/webware-htmx#21 is work the convention *enables* b - [X] T025 **Built** (`ConfigProviderWiringTest`, green). Keep `test/integration/ConfigProviderWiringTest.php` green: it asserts `extra.laminas.config-provider` agrees with the namespace -## Phase 9 — Spec-kit tracking +## Phase 9: Spec-kit tracking - [X] T026 **Done** (specs and `.specify` are tracked on this branch). Keep `.specify/` and `/specs/` tracked in this repository (deliberate divergence from the preset's alignment tasks, which add both to `.gitignore`): this component's development is @@ -157,7 +157,7 @@ Everything else in webinertia/webware-htmx#21 is work the convention *enables* b - [X] T027 **Done** (`.specify/.gitignore` ignores `presets/`). Keep the vendored preset copy (`.specify/presets/`) local via `.specify/.gitignore`; it is released content shipped in `webware/webware-tools/presets/` -## Phase 10 — Theme-aware asset helper (FR-009, FR-009a; D-007, D-008, D-009) +## Phase 10: Theme-aware asset helper (FR-009, FR-009a; D-007, D-008, D-009) Why: laminas-view's `Asset` helper is `final readonly` and is built once from `view_helper_config.asset.resource_map`, with no idea of a theme. The fix is a factory, not a subclass. @@ -188,7 +188,7 @@ Why: laminas-view's `Asset` helper is `final readonly` and is built once from - [X] T048 **Done earlier** (`data-model.md`, `research.md` carry `view_helper_config.asset.resource_map`). Record the final key paths in `data-model.md` and `research.md` (they currently describe `view_manager.asset.resource_map`, which is wrong, D-009) -## Phase 11 — The default theme's assets (repository `webinertia/default-theme`; D-015, D-016) +## Phase 11: The default theme's assets (repository `webinertia/default-theme`; D-015, D-016) - [X] T049 **Superseded (D-021).** `webinertia/default-theme` is a scratch space, not a deliverable; its PR is not to be merged as a package. Original text: Land `webinertia/default-theme#1` (the Bootstrap 5.3.8 token overrides in `css/theme.css` and `mockup/index.html`). Owner review; do not merge without them @@ -205,7 +205,7 @@ Why: laminas-view's `Asset` helper is `final readonly` and is built once from - [ ] T053 CMS-style landing page layout mockup for `app::home-page` (hero plus regions). Blocked on the contract location, D-018 -## Phase 12 — Port the base templates to a neutral `default` theme (FR-018 to FR-020; D-010, D-013, D-014) +## Phase 12: Port the base templates to a neutral `default` theme (FR-018 to FR-020; D-010, D-013, D-014) Scope: webware-acl, webware-admin, webware-usermanager. Everything IMS-specific is copied **verbatim** into the `ims` theme first; the `default` copy then loses the `ims-` coupling. No IMS code is lost and @@ -233,7 +233,7 @@ none lives in a component (standing rule 6). resources, the user list and the dashboard render with the palette, and the ACL search, filters, rules offcanvas and protect-route wizard work. Needs an admin user in the app database -## Phase 13 — Cross-repository dependencies +## Phase 13: Cross-repository dependencies - [ ] T060 `project-tracking#6` (the installer RFC) carries the asset-publishing requirements (added 2026-10-01). Until an installer exists, publish assets with a documented one-off command and remove @@ -241,14 +241,14 @@ none lives in a component (standing rule 6). - [ ] T061 Decide where the home-page block contract lives (D-018), then write its spec and plan in that repository. This feature only needs the `default` theme templates that render it (D-019) -## Phase 14 — Owner decisions of 2026-10-01 (D-005, D-011, D-012) +## Phase 14: Owner decisions of 2026-10-01 (D-005, D-011, D-012) - [X] T062 **Done:** `ConfigProvider::THEME`, `ACTIVE`, `THEMES`, `ASSETS`, used by both factories. Add constants for the known config keys in `ConfigProvider` (`THEME = 'theme'`, `ACTIVE`, `THEMES`, `ASSETS`) and use them in `ThemeResolverFactory` and the tests instead of the string literals (D-005). No new classes - [X] T063 **Done** in `AssetFactory` (the one place a name becomes a path today); the command (T066) must reuse the same rule. Validate a theme name as a single path segment (no separators, no `..`) at the one place it becomes a path, the asset factory (T043); test it. Do not add a value object (D-005) -- [ ] T064 Component assets are per theme (D-011): in T042–T043 the asset values for a component live +- [ ] T064 Component assets are per theme (D-011): in T042 - T043 the asset values for a component live under `theme.assets.` and resolve to `/theme//component//…`; document the layout in `data-model.md` (it currently shows the superseded `public/component/` path) and `quickstart.md` @@ -272,10 +272,10 @@ none lives in a component (standing rule 6). ## Verification -- [ ] T028 `mago format --check && mago lint && mago analyze && mago guard` — all four clean +- [ ] T028 `mago format --check && mago lint && mago analyze && mago guard` - all four clean - [ ] T029 `composer test`, `composer test-integration`, `composer test-coverage`, - `composer mutation-test` — 100% line coverage, MSI at or above 95 + `composer mutation-test` - 100% line coverage, MSI at or above 95 - [ ] T030 Render a page in an application with the packages installed and no theme configured (SC-003, SC-006), then activate a theme that overrides one layout and confirm everything else falls back -- [ ] T031 Push the branch, open the PR, and confirm the required workflow's checks are green — +- [ ] T031 Push the branch, open the PR, and confirm the required workflow's checks are green - which requires `ci-target` to be set on the repository first diff --git a/src/Resolver/ThemeResolver.php b/src/Resolver/ThemeResolver.php index 75ec31d..3e34d89 100644 --- a/src/Resolver/ThemeResolver.php +++ b/src/Resolver/ThemeResolver.php @@ -20,8 +20,8 @@ /** * Resolves a namespaced template address against the active theme's map. * - * The name handed in is the address the renderer asks for — `app::home-page`, `layout::default`, - * `admin::dashboard` — and a map is keyed by exactly that, so a hit is an `isset` and nothing + * The name handed in is the address the renderer asks for - `app::home-page`, `layout::default`, + * `admin::dashboard` - and a map is keyed by exactly that, so a hit is an `isset` and nothing * touches the filesystem. An address the active theme does not carry falls back to the `default` * theme's entry for the same address, and a name neither theme carries returns `false` so the * aggregate resolver continues to the next resolver.