From 820d678d093f7ec1d0d67a41bab38076b6f8ed99 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 5 Aug 2026 16:32:28 +0530 Subject: [PATCH 1/2] feat(scaffolds): add utility/* package scaffolds for framework Utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship utility/{cache,transients,logger,timer,feature-selector} as source: "package" — zero files written, rtcamp/wp-framework: ^1.0 reported under developer.install.composer, and one accessor snippet for /Helpers/Util.php under anchor // scaffold:utility/. context_slug is discovered from composer.json:name and snake-cased. Wiring targets the consumer's static helper rather than a module or Main::CLASSES: these utilities implement neither Registrable nor Shareable, and the framework Loader instantiates with no constructor argument, so Transients would fatal and Cache would lose its namespace. Refs rtCamp/wp-devtools#26 --- node-packages/wp-tooling/CHANGELOG.md | 1 + .../wp-tooling/docs/ai-orchestration.md | 2 +- .../wp-tooling/docs/authoring-scaffolds.md | 19 +++ .../wp-tooling/docs/wp-framework-contract.md | 34 ++++++ .../claude-skills/templates/scaffold-SKILL.md | 5 +- .../claude-skills/templates/setup-SKILL.md | 11 +- .../scaffolds/utility/cache/scaffold.json | 34 ++++++ .../utility/feature-selector/scaffold.json | 34 ++++++ .../scaffolds/utility/logger/scaffold.json | 34 ++++++ .../scaffolds/utility/timer/scaffold.json | 34 ++++++ .../utility/transients/scaffold.json | 34 ++++++ .../wp-tooling/skills/scaffold/SKILL.md | 5 +- .../wp-tooling/skills/setup/SKILL.md | 11 +- .../tests/scaffolds/bundled-manifests.test.js | 110 ++++++++++++++++++ 14 files changed, 363 insertions(+), 5 deletions(-) create mode 100644 node-packages/wp-tooling/scaffolds/utility/cache/scaffold.json create mode 100644 node-packages/wp-tooling/scaffolds/utility/feature-selector/scaffold.json create mode 100644 node-packages/wp-tooling/scaffolds/utility/logger/scaffold.json create mode 100644 node-packages/wp-tooling/scaffolds/utility/timer/scaffold.json create mode 100644 node-packages/wp-tooling/scaffolds/utility/transients/scaffold.json diff --git a/node-packages/wp-tooling/CHANGELOG.md b/node-packages/wp-tooling/CHANGELOG.md index 8a77b0d..52ea85f 100644 --- a/node-packages/wp-tooling/CHANGELOG.md +++ b/node-packages/wp-tooling/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added +- `utility/*` package scaffolds — `utility/cache`, `utility/transients`, `utility/logger`, `utility/timer` and `utility/feature-selector` wire the `rtcamp/wp-framework` `Utils\*` classes with no template files. Each is `source: "package"`: zero files written, `rtcamp/wp-framework: ^1.0` reported under `developer.install.composer`, and one `ai.wiring` accessor snippet for `/Helpers/Util.php` under anchor `// scaffold:utility/`. `context_slug` is discovered from `composer.json:name` and normalised to snake_case (`rtcamp/acme-blog` → `rtcamp_acme_blog`); pass `--context_slug` to override. Nothing is installed and nothing is wired automatically. These utilities are neither `Registrable` nor `Shareable`, so the wiring targets the consumer's static helper rather than a module or `Main::CLASSES` — the `Loader` would otherwise construct them with a missing or empty constructor argument. - Remote scaffolds — a scaffold's `scaffold.json` + templates can live in another repo. `scaffolds/sources.json` lists the source repos (pinned `{ repository, ref, path }`); each repo publishes a `scaffolds/index.json` enumerating the scaffolds it offers, which the registry fetches to discover them (one PR in the owning repo adds/changes a scaffold; wp-tooling only changes to onboard a new repo). Manifests + templates are fetched on `add`, cached under `${XDG_CACHE_HOME:-$HOME/.cache}/wp-tooling/remote/` and validated with ETag conditional requests (`304 Not Modified` serves the cache; movable tags refresh when they move). New error code `EFETCHFAIL` (network/HTTP) distinct from `EBADSCAFFOLD` (bad index/manifest). `list` is online-preferred with a cache fallback and reports unreachable sources as warnings; `validate --remote` fetches + schema-validates each index + manifest; `wp-tooling cache clear` empties the cache. Dormant by default — no `sources.json` ships. - Engine-side input discovery (`discover_from`) — an input declaration can say where to source its value from the project, so the engine fills it instead of the caller guessing. Resolves from `composer.json` / `package.json` (dotted paths; `autoload.psr-4` yields the root namespace) and `.wp-tooling.json`, with precedence `supplied → discovered → default`. Fail-safe: a missing or malformed project file falls through to the input's `default`, so a project without those files behaves exactly as before the resolver existed. Adds an optional `transform` step for derived inputs (`json-escape` doubles backslashes for the PSR-4 composer key). The inputs the engine actually rendered with are surfaced on `execute()` as `engine.inputs`. Bundled `setup/psr4` + `wp/*` scaffolds annotated with `discover_from`. - Feature toggle layer — a scaffold may declare an optional `feature` block (`config_key`, `owned_files`, `confirm_remove`, `gitignore`) marking it as a toggleable project feature. New TTY-free `enable` / `disable` / `status` verbs create or remove the owned files idempotently, manage `.gitignore` lines (Mustache-rendered against resolved inputs), prompt before deleting consumer-editable files (`confirm_remove`, overridable with `--force`), and persist on/off state in `.wp-tooling.json`. New `wp-tooling features` command — lists feature status by default, with `--enable` / `--disable` to toggle (plus `--json`, `--force`, `--no-install`, `--dry-run`); `setup/tailwind` ships as the first such feature. Additive — the `feature` block never affects the `add` / `execute` path. diff --git a/node-packages/wp-tooling/docs/ai-orchestration.md b/node-packages/wp-tooling/docs/ai-orchestration.md index ca56ba1..44bd65c 100644 --- a/node-packages/wp-tooling/docs/ai-orchestration.md +++ b/node-packages/wp-tooling/docs/ai-orchestration.md @@ -57,7 +57,7 @@ The engine commits to the following on a successful run. Skills can rely on thes - The engine is idempotent under `scaffold.dryRun: true`: identical inputs produce identical output across runs. - The engine never reads or writes any path outside `--cwd`. This is enforced: a rendered `dest`, test path, or feature-owned path that resolves outside the target directory (e.g. via `..` in a path input or a third-party remote manifest) fails with `EWRITEFAIL` (`errno: "EOUTSIDE"`) before anything touches the filesystem. - The engine never invokes `gh`, `git`, `composer`, `npm`, or any other external CLI on behalf of the caller. -- `scaffold.kind` is `"package"` for `source: "package"` scaffolds (no files written, only deps and wiring) and `"template"` otherwise. Remote (sources) scaffolds also report `kind: "template"` — they render Mustache the same way as local ones; where the scaffold lives is an implementation detail the orchestrator does not need to branch on. Callers branch on `kind` rather than checking `engine.wrote.length === 0`. +- `scaffold.kind` is `"package"` for `source: "package"` scaffolds (no files written, only deps and wiring; the bundled examples are `utility/*`) and `"template"` otherwise. Remote (sources) scaffolds also report `kind: "template"` — they render Mustache the same way as local ones; where the scaffold lives is an implementation detail the orchestrator does not need to branch on. Callers branch on `kind` rather than checking `engine.wrote.length === 0`. - `wp-tooling list --json` entries carry an `origin` of `"default"`, `"project"`, or `"remote"`. Remote scaffolds come from a repo's cached index (`sources.json` → each repo's `index.json`), so their `counts` is `null` (unknown until `add`); local scaffolds carry real `counts`. `list` is online-preferred with a cache fallback: it reads the index (cached, ETag-validated), and a `warnings` array reports any source that was unreachable and uncached. The top-level `{ scaffolds, warnings }` shape carries those notes. - The engine core has zero dependency on the TTY UI kit. AI orchestration mode never loads any terminal-UI primitive. Skills can rely on the engine being usable from any context, including non-TTY containers, CI runners, and headless test harnesses. - File-based `discover_from` (`composer.json:`, `package.json:`, `config:` from `.wp-tooling.json`) is resolved by the engine itself, before manifest defaults are applied, with precedence **`supplied → discovered → default`**. A value the skill passes explicitly always wins. A missing or malformed source file is ignored and the input falls back to its `default` — the engine never throws because a project file is absent or unparsable. `code:*` and `plugin-header:*` sources are **not** engine-resolved and remain the skill's responsibility (§6). diff --git a/node-packages/wp-tooling/docs/authoring-scaffolds.md b/node-packages/wp-tooling/docs/authoring-scaffolds.md index c37a5f5..24de1de 100644 --- a/node-packages/wp-tooling/docs/authoring-scaffolds.md +++ b/node-packages/wp-tooling/docs/authoring-scaffolds.md @@ -196,6 +196,24 @@ Use sections inside `snippet_template` to vary the snippet by flag (e.g. the sin --- +## Package scaffolds (`source: "package"`) + +A package scaffold wires up a class that already ships in `vendor/` rather than generating one. It +declares `files: []`, the `composer_dependencies` entry that provides the class, and the `wiring[]` +snippet that constructs it. `execute()` reports `scaffold.kind: "package"` and writes nothing. + +- `module_class`: the fully qualified PHP class the scaffold wires up (e.g. + `rtCamp\\WPFramework\\Utils\\Cache` — doubled backslashes, since this is JSON). Manifest metadata + documenting which vendor class the `wiring[]` snippet constructs. Optional, but set it on every + package scaffold. + +The `utility/*` scaffolds are the bundled examples. Note what a package scaffold **cannot** do: it +cannot create the wrapper class its snippet might want, because `files[]` is empty. Keep the snippet +self-sufficient against an existing file, and use `description` to spell out the alternatives the +project may prefer. + +--- + ## Tests, secrets, scripts - `tests[]`: test stubs written alongside production output. Each entry has `src`, `dest`, `framework` (`phpunit`, `jest`, `playwright`, `pa11y`, `actionlint`, `yaml-parse`), and optional `command`. @@ -366,6 +384,7 @@ Look at these existing scaffolds when authoring a new one: | Module that hosts other Registrable classes | `wp/module` | | Static config file (no inputs) | `setup/editorconfig` | | Wiring into an existing JSON file | `setup/psr4` | +| `source: package` — zero files, a Composer dep plus one wiring snippet | `utility/cache`, `utility/timer` | | Multiple variants of the same concept | `lint/phpcs/{full,core,vip}` | | Block with `block.json` + framework class | `wp/block-dynamic` | | Workflow / YAML scaffold with secrets | `ci/cd-wporg` | diff --git a/node-packages/wp-tooling/docs/wp-framework-contract.md b/node-packages/wp-tooling/docs/wp-framework-contract.md index 70be286..af1f0a3 100644 --- a/node-packages/wp-tooling/docs/wp-framework-contract.md +++ b/node-packages/wp-tooling/docs/wp-framework-contract.md @@ -183,6 +183,31 @@ interface CLICommand { --- +## Utility classes (targeted by `scaffolds/utility/*`) + +All under `rtCamp\WPFramework\Utils\`. These are **plain classes with public constructors** — not +`Registrable`, not `Shareable`, not singletons, and none needs the `Container`. That is why the +`utility/*` scaffolds wire them through an accessor on the consumer's static helper +(`/Helpers/Util.php`) rather than adding them to an `AbstractModule` or `Main::CLASSES`: +the `Loader` only calls `register_hooks()` / caches `Shareable`, so a listed `Utils\*` class would be +constructed with no argument — silently wrong for `Cache`, `Logger` and `FeatureSelector`, and fatal +for `Transients`. + +| Class | Constructor | Key methods | +|---|---|---| +| `Cache` | `__construct(string $context = '')` — prefixes every group as `context:group` | `get`, `set`, `delete`, `flush_group`, `remember`, `remember_swr` | +| `Transients` | `__construct(string $prefix)` — **required**, `readonly` | `get`, `set`, `delete` | +| `Logger` | `__construct(string $prefix = 'rtcamp')` | `log`, `debug`, `info`, `warning`, `error` — silent unless `WP_DEBUG` | +| `Timer` | none — **takes no arguments** | `start`, `stop`, `lap`, `get`, `get_all` | +| `FeatureSelector` | `__construct(string $context = '')` — derives the option key and constant name | `register`, `is_enabled`, `enable`, `disable`, `get_registered`, `shared_option_key` | + +Spelling is `FeatureSelector`, not `Feature_Selector`. `Timer` and `FeatureSelector` hold state on the +instance (laps; the flag registry), so they must be shared rather than constructed per call. +`FeatureSelectorSettingsPage` is an abstract admin UI over `FeatureSelector`, requiring a +`get_selector(): FeatureSelector` implementation. + +--- + ## Optional helpers (not currently scaffolded) - `AssetLoaderTrait`: register scripts, styles, block manifests with versioning from a build's `.asset.php` file. @@ -204,3 +229,12 @@ Marker pattern: `// scaffold::classes`. Examples: - `// scaffold:wp/registrable:classes` inside `Modules/Services.php` Each `Modules/.php` file is itself produced by the `wp/module` scaffold, which accepts a `kind` input that selects which anchor to emit. + +The `utility/*` scaffolds are the exception, because they register no class with the `Loader`. Their +marker takes no `:classes` suffix and sits in the consumer's static helper, not a module: + +- `// scaffold:utility/cache`, `utility/transients`, `utility/logger`, `utility/timer`, + `utility/feature-selector` — all inside `Helpers/Util.php` + +No scaffold emits these markers, so expect them to be absent; fall back to sampling the existing +accessors in that file. diff --git a/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/scaffold-SKILL.md b/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/scaffold-SKILL.md index e0c1784..a5a6269 100644 --- a/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/scaffold-SKILL.md +++ b/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/scaffold-SKILL.md @@ -55,7 +55,7 @@ Read, in order: - Block scaffolds: sample one `block.json` for vendor prefix and source dir. - CI scaffolds: sample one `.github/workflows/*.yml` for filename and trigger style. -Anchors (`// scaffold::classes`) are hints, not ground truth. Sampled patterns win. +Anchors (`// scaffold::classes`, and `// scaffold:utility/` in `Helpers/Util.php`) are hints, not ground truth. Sampled patterns win. Confirm findings with the developer in one short message. Proceed on confirmation. @@ -77,6 +77,8 @@ Files group by **kind**, never by feature. `` = project's autoload root (e | `wp/cron` | `includes/Cron/` | `\Cron` | `tests/Cron/` | `\Tests\Cron` | `\Modules\Cron` | | `wp/registrable` | `includes/Services/` | `\Services` | `tests/Services/` | `\Tests\Services` | `\Modules\Services` | +`utility/*` is absent from the table on purpose: those are `source: package`, so they have no source dir, no test dir and no module. Each returns one accessor snippet for `/Helpers/Util.php` under anchor `// scaffold:utility/`. Never add a framework `Utils\*` class to a module or to `Main::CLASSES` — they implement neither `Registrable` nor `Shareable`, so the Loader would construct them with the wrong (or a missing) constructor argument. + **Modules host one kind each. No `Modules//...`.** A multi-kind feature (e.g. Testimonials = CPT + taxonomy + block + REST) spans the per-kind directories and wires into each kind's module. If the project already has a per-feature module folder, flag as anti-pattern. Offer migration before adding new artifacts. Do not scaffold into it. @@ -163,6 +165,7 @@ Frameworks per kind: | `wp/block-dynamic` | Jest (edit.js) + PHPUnit (render method) | | `block/interactive` | Jest + Playwright | | `ci/*` | actionlint + yaml-parse | +| `utility/*` | none - no files written, so no stub to expand. Cover the calling code instead. | ### 8. Escalate when stuck - do not guess diff --git a/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/setup-SKILL.md b/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/setup-SKILL.md index 31701fc..1e39560 100644 --- a/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/setup-SKILL.md +++ b/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/setup-SKILL.md @@ -124,9 +124,18 @@ Map each feature the developer mentioned to one or more scaffold IDs from the ca | REST endpoint / API | `wp/rest` (if available) | | Cron job / background job | `wp/cron` (if available) | | Gutenberg block | `wp/block-dynamic` | -| Cache / transients | `utility/cache` | +| Object caching | `utility/cache` | +| Transients | `utility/transients` | +| Debug logging | `utility/logger` | +| Timing a slow code path | `utility/timer` | +| Feature flags / gradual rollout | `utility/feature-selector` | | CI pipeline | `ci/cd-wporg` (or other CI scaffold) | +The `utility/*` scaffolds are `source: package`: they write no files and return the +`rtcamp/wp-framework` dependency plus one accessor snippet for `/Helpers/Util.php`. +Check whether the project already exposes that accessor before applying the wiring — re-adding one +that already exists is a fatal redeclare. + Run `npx wp-tooling list --json` to see exactly what is available. If a feature the developer wants has no matching scaffold, note it explicitly as a manual task in the final report. For each feature scaffold, you need the same project-convention information as the `scaffold` skill requires (namespace, base path, class suffix, registration pattern). Collect this once from the project and cache it. diff --git a/node-packages/wp-tooling/scaffolds/utility/cache/scaffold.json b/node-packages/wp-tooling/scaffolds/utility/cache/scaffold.json new file mode 100644 index 0000000..9e68c21 --- /dev/null +++ b/node-packages/wp-tooling/scaffolds/utility/cache/scaffold.json @@ -0,0 +1,34 @@ +{ + "slug": "cache", + "category": "utility", + "name": "Cache utility", + "description": "Registers the framework Cache utility from rtcamp/wp-framework as a shared service. Writes no files: reports the Composer dependency and one accessor snippet. Cache wraps the WordPress object cache with a per-plugin group namespace, group flushing, and the remember() / remember_swr() compute-once helpers.", + "source": "package", + "module_class": "rtCamp\\WPFramework\\Utils\\Cache", + "files": [], + "inputs": [ + { + "key": "base_path", + "description": "Directory holding the PHP source, relative to the project root, without a trailing slash. Default 'includes' matches the rtCamp skeleton; pass 'inc' or 'src' for projects that differ.", + "default": "includes" + }, + { + "key": "context_slug", + "description": "Namespace every cache group is scoped to, so this project's groups can never collide with another consumer's sharing the same object cache. Discovered from the composer package name and normalised to snake_case, so 'rtcamp/acme-blog' becomes 'rtcamp_acme_blog'. Pass an explicit value to override.", + "discover_from": "composer.json:name", + "transform": "snake-case", + "default": "my_plugin" + } + ], + "wiring": [ + { + "target_file": "{{base_path}}/Helpers/Util.php", + "anchor": "// scaffold:utility/cache", + "snippet_template": "/**\n * The project's shared framework Cache, namespaced so its groups can never\n * collide with another consumer's.\n *\n * @return \\rtCamp\\WPFramework\\Utils\\Cache Shared cache.\n */\npublic static function cache(): \\rtCamp\\WPFramework\\Utils\\Cache {\n\tstatic $instance = null;\n\n\tif ( null === $instance ) {\n\t\t$instance = new \\rtCamp\\WPFramework\\Utils\\Cache( '{{context_slug}}' );\n\t}\n\n\treturn $instance;\n}", + "description": "Add a shared accessor for the framework Cache to the project's static helper class. The snippet is self-contained: paste it inside the existing helper class body and Util::cache()->remember( ... ) works with nothing further to wire. Do not add the framework Cache to a Modules class or to Main::CLASSES — it implements neither Registrable nor Shareable, so the framework Loader would construct it with an empty context. If the project instead shares services through the framework Shareable contract (a Core\\Cache class extending the framework Cache, listed in Main::CLASSES, the way the rtCamp skeleton does for Core\\Logger), translate the snippet to that pattern and add a one-line accessor there. If only one class needs the cache, skip this wiring and construct it inline in that class's constructor. Requires rtcamp/wp-framework to be installed first." + } + ], + "composer_dependencies": { + "rtcamp/wp-framework": "^1.0" + } +} diff --git a/node-packages/wp-tooling/scaffolds/utility/feature-selector/scaffold.json b/node-packages/wp-tooling/scaffolds/utility/feature-selector/scaffold.json new file mode 100644 index 0000000..1e7af11 --- /dev/null +++ b/node-packages/wp-tooling/scaffolds/utility/feature-selector/scaffold.json @@ -0,0 +1,34 @@ +{ + "slug": "feature-selector", + "category": "utility", + "name": "Feature selector utility", + "description": "Registers the framework FeatureSelector utility from rtcamp/wp-framework as a shared service. Writes no files: reports the Composer dependency and one accessor snippet. FeatureSelector registers named feature flags and resolves each one from a PHP constant or a shared option through register / is_enabled / enable / disable, failing closed for anything unregistered.", + "source": "package", + "module_class": "rtCamp\\WPFramework\\Utils\\FeatureSelector", + "files": [], + "inputs": [ + { + "key": "base_path", + "description": "Directory holding the PHP source, relative to the project root, without a trailing slash. Default 'includes' matches the rtCamp skeleton; pass 'inc' or 'src' for projects that differ.", + "default": "includes" + }, + { + "key": "context_slug", + "description": "Context the flag registry is scoped to. It derives both the shared option key and the per-flag constant name, so it must stay stable across releases — changing it orphans every stored flag. Discovered from the composer package name and normalised to snake_case, so 'rtcamp/acme-blog' becomes 'rtcamp_acme_blog'. Pass an explicit value to override.", + "discover_from": "composer.json:name", + "transform": "snake-case", + "default": "my_plugin" + } + ], + "wiring": [ + { + "target_file": "{{base_path}}/Helpers/Util.php", + "anchor": "// scaffold:utility/feature-selector", + "snippet_template": "/**\n * The project's shared framework FeatureSelector. One instance holds the whole\n * flag registry, so every caller sees the same registered features.\n *\n * @return \\rtCamp\\WPFramework\\Utils\\FeatureSelector Shared feature selector.\n */\npublic static function features(): \\rtCamp\\WPFramework\\Utils\\FeatureSelector {\n\tstatic $instance = null;\n\n\tif ( null === $instance ) {\n\t\t$instance = new \\rtCamp\\WPFramework\\Utils\\FeatureSelector( '{{context_slug}}' );\n\t}\n\n\treturn $instance;\n}", + "description": "Add a shared accessor for the framework FeatureSelector to the project's static helper class. The class is spelled FeatureSelector, not Feature_Selector. The snippet is self-contained: paste it inside the existing helper class body, register flags once during bootstrap with Util::features()->register( 'new-checkout' ), then read them with Util::features()->is_enabled( 'new-checkout' ). Sharing one instance is deliberate rather than incidental: the flag registry lives on the instance and unregistered flags fail closed, so a fresh instance per call would report every feature as disabled. Do not add the framework FeatureSelector to a Modules class or to Main::CLASSES — it implements neither Registrable nor Shareable, so the framework Loader would construct it with an empty context and derive the wrong option key. For an admin UI over these flags, subclass the framework FeatureSelectorSettingsPage and return this accessor from its get_selector(). Requires rtcamp/wp-framework to be installed first." + } + ], + "composer_dependencies": { + "rtcamp/wp-framework": "^1.0" + } +} diff --git a/node-packages/wp-tooling/scaffolds/utility/logger/scaffold.json b/node-packages/wp-tooling/scaffolds/utility/logger/scaffold.json new file mode 100644 index 0000000..bed89ba --- /dev/null +++ b/node-packages/wp-tooling/scaffolds/utility/logger/scaffold.json @@ -0,0 +1,34 @@ +{ + "slug": "logger", + "category": "utility", + "name": "Logger utility", + "description": "Registers the framework Logger utility from rtcamp/wp-framework as a shared service. Writes no files: reports the Composer dependency and one accessor snippet. Logger writes prefixed debug / info / warning / error lines and stays silent unless WP_DEBUG is on.", + "source": "package", + "module_class": "rtCamp\\WPFramework\\Utils\\Logger", + "files": [], + "inputs": [ + { + "key": "base_path", + "description": "Directory holding the PHP source, relative to the project root, without a trailing slash. Default 'includes' matches the rtCamp skeleton; pass 'inc' or 'src' for projects that differ.", + "default": "includes" + }, + { + "key": "context_slug", + "description": "Prefix every log line is tagged with, so this project's entries are greppable and distinguishable from another consumer's. Discovered from the composer package name and normalised to snake_case, so 'rtcamp/acme-blog' becomes 'rtcamp_acme_blog'. Pass an explicit value to override.", + "discover_from": "composer.json:name", + "transform": "snake-case", + "default": "my_plugin" + } + ], + "wiring": [ + { + "target_file": "{{base_path}}/Helpers/Util.php", + "anchor": "// scaffold:utility/logger", + "snippet_template": "/**\n * The project's shared framework Logger. Silent unless WP_DEBUG.\n *\n * @return \\rtCamp\\WPFramework\\Utils\\Logger Shared logger.\n */\npublic static function logger(): \\rtCamp\\WPFramework\\Utils\\Logger {\n\tstatic $instance = null;\n\n\tif ( null === $instance ) {\n\t\t$instance = new \\rtCamp\\WPFramework\\Utils\\Logger( '{{context_slug}}' );\n\t}\n\n\treturn $instance;\n}", + "description": "Add a shared accessor for the framework Logger to the project's static helper class. Check for an existing logger accessor before applying, and skip this wiring if one is already there: the rtCamp skeleton ships a Core\\Logger class extending the framework Logger, registered in Main::CLASSES and reached through Util::logger(), so re-applying the snippet would redeclare the method and fatal. Otherwise the snippet is self-contained: paste it inside the existing helper class body and Util::logger()->info( 'Cache warmed', array( 'items' => 42 ) ) works with nothing further to wire. Do not add the framework Logger to a Modules class or to Main::CLASSES — it implements neither Registrable nor Shareable, so the framework Loader would construct it with the framework's own default prefix rather than this project's. Requires rtcamp/wp-framework to be installed first." + } + ], + "composer_dependencies": { + "rtcamp/wp-framework": "^1.0" + } +} diff --git a/node-packages/wp-tooling/scaffolds/utility/timer/scaffold.json b/node-packages/wp-tooling/scaffolds/utility/timer/scaffold.json new file mode 100644 index 0000000..70881d8 --- /dev/null +++ b/node-packages/wp-tooling/scaffolds/utility/timer/scaffold.json @@ -0,0 +1,34 @@ +{ + "slug": "timer", + "category": "utility", + "name": "Timer utility", + "description": "Registers the framework Timer utility from rtcamp/wp-framework as a shared service. Writes no files: reports the Composer dependency and one accessor snippet. Timer measures elapsed time with start / stop / lap and reports through get / get_all, warning via _doing_it_wrong() on misuse rather than throwing.", + "source": "package", + "module_class": "rtCamp\\WPFramework\\Utils\\Timer", + "files": [], + "inputs": [ + { + "key": "base_path", + "description": "Directory holding the PHP source, relative to the project root, without a trailing slash. Default 'includes' matches the rtCamp skeleton; pass 'inc' or 'src' for projects that differ.", + "default": "includes" + }, + { + "key": "context_slug", + "description": "Accepted but unused: the framework Timer takes no constructor arguments. Declared so one input map works across every utility scaffold. Discovered from the composer package name and normalised to snake_case.", + "discover_from": "composer.json:name", + "transform": "snake-case", + "default": "my_plugin" + } + ], + "wiring": [ + { + "target_file": "{{base_path}}/Helpers/Util.php", + "anchor": "// scaffold:utility/timer", + "snippet_template": "/**\n * The project's shared framework Timer. One instance keeps every lap on a\n * single timeline, so callers in different classes can read each other's marks.\n *\n * @return \\rtCamp\\WPFramework\\Utils\\Timer Shared timer.\n */\npublic static function timer(): \\rtCamp\\WPFramework\\Utils\\Timer {\n\tstatic $instance = null;\n\n\tif ( null === $instance ) {\n\t\t$instance = new \\rtCamp\\WPFramework\\Utils\\Timer();\n\t}\n\n\treturn $instance;\n}", + "description": "Add a shared accessor for the framework Timer to the project's static helper class. The framework Timer takes no constructor arguments, so there is nothing to configure and the context slug is not used here. Sharing one instance is deliberate rather than incidental: labels and laps accumulate on the instance, so a fresh instance per call would lose every mark taken elsewhere. The snippet is self-contained: paste it inside the existing helper class body and Util::timer()->start( 'import' ) works with nothing further to wire. Do not add the framework Timer to a Modules class or to Main::CLASSES — it implements neither Registrable nor Shareable, so the framework Loader would construct an instance nothing can reach. If the project instead shares services through the framework Shareable contract (a Core class extending the framework class, listed in Main::CLASSES, the way the rtCamp skeleton does for Core\\Logger), translate the snippet to that pattern. Requires rtcamp/wp-framework to be installed first." + } + ], + "composer_dependencies": { + "rtcamp/wp-framework": "^1.0" + } +} diff --git a/node-packages/wp-tooling/scaffolds/utility/transients/scaffold.json b/node-packages/wp-tooling/scaffolds/utility/transients/scaffold.json new file mode 100644 index 0000000..85b81ac --- /dev/null +++ b/node-packages/wp-tooling/scaffolds/utility/transients/scaffold.json @@ -0,0 +1,34 @@ +{ + "slug": "transients", + "category": "utility", + "name": "Transients utility", + "description": "Registers the framework Transients utility from rtcamp/wp-framework as a shared service. Writes no files: reports the Composer dependency and one accessor snippet. Transients wraps get_transient / set_transient / delete_transient behind a mandatory per-project key prefix.", + "source": "package", + "module_class": "rtCamp\\WPFramework\\Utils\\Transients", + "files": [], + "inputs": [ + { + "key": "base_path", + "description": "Directory holding the PHP source, relative to the project root, without a trailing slash. Default 'includes' matches the rtCamp skeleton; pass 'inc' or 'src' for projects that differ.", + "default": "includes" + }, + { + "key": "context_slug", + "description": "Prefix every transient key is written under. The framework constructor requires it and holds it readonly, so it must never be empty. Discovered from the composer package name and normalised to snake_case, so 'rtcamp/acme-blog' becomes 'rtcamp_acme_blog'. Pass an explicit value to override, and keep it stable across releases — changing it orphans every stored transient.", + "discover_from": "composer.json:name", + "transform": "snake-case", + "default": "my_plugin" + } + ], + "wiring": [ + { + "target_file": "{{base_path}}/Helpers/Util.php", + "anchor": "// scaffold:utility/transients", + "snippet_template": "/**\n * The project's shared framework Transients store, prefixed so its keys can\n * never collide with another consumer's.\n *\n * @return \\rtCamp\\WPFramework\\Utils\\Transients Shared transients store.\n */\npublic static function transients(): \\rtCamp\\WPFramework\\Utils\\Transients {\n\tstatic $instance = null;\n\n\tif ( null === $instance ) {\n\t\t$instance = new \\rtCamp\\WPFramework\\Utils\\Transients( '{{context_slug}}' );\n\t}\n\n\treturn $instance;\n}", + "description": "Add a shared accessor for the framework Transients store to the project's static helper class. The snippet is self-contained: paste it inside the existing helper class body and Util::transients()->get( 'key' ) works with nothing further to wire. The constructor prefix is mandatory and readonly — never pass an empty string. Do not add the framework Transients to a Modules class or to Main::CLASSES: it implements neither Registrable nor Shareable, so the framework Loader would try to construct it with no argument and fatal on the missing prefix. If the project instead shares services through the framework Shareable contract (a Core\\Transients class extending the framework class, listed in Main::CLASSES, the way the rtCamp skeleton does for Core\\Logger), translate the snippet to that pattern. If only one class needs transients, skip this wiring and construct it inline in that class's constructor. Requires rtcamp/wp-framework to be installed first." + } + ], + "composer_dependencies": { + "rtcamp/wp-framework": "^1.0" + } +} diff --git a/node-packages/wp-tooling/skills/scaffold/SKILL.md b/node-packages/wp-tooling/skills/scaffold/SKILL.md index e0c1784..a5a6269 100644 --- a/node-packages/wp-tooling/skills/scaffold/SKILL.md +++ b/node-packages/wp-tooling/skills/scaffold/SKILL.md @@ -55,7 +55,7 @@ Read, in order: - Block scaffolds: sample one `block.json` for vendor prefix and source dir. - CI scaffolds: sample one `.github/workflows/*.yml` for filename and trigger style. -Anchors (`// scaffold::classes`) are hints, not ground truth. Sampled patterns win. +Anchors (`// scaffold::classes`, and `// scaffold:utility/` in `Helpers/Util.php`) are hints, not ground truth. Sampled patterns win. Confirm findings with the developer in one short message. Proceed on confirmation. @@ -77,6 +77,8 @@ Files group by **kind**, never by feature. `` = project's autoload root (e | `wp/cron` | `includes/Cron/` | `\Cron` | `tests/Cron/` | `\Tests\Cron` | `\Modules\Cron` | | `wp/registrable` | `includes/Services/` | `\Services` | `tests/Services/` | `\Tests\Services` | `\Modules\Services` | +`utility/*` is absent from the table on purpose: those are `source: package`, so they have no source dir, no test dir and no module. Each returns one accessor snippet for `/Helpers/Util.php` under anchor `// scaffold:utility/`. Never add a framework `Utils\*` class to a module or to `Main::CLASSES` — they implement neither `Registrable` nor `Shareable`, so the Loader would construct them with the wrong (or a missing) constructor argument. + **Modules host one kind each. No `Modules//...`.** A multi-kind feature (e.g. Testimonials = CPT + taxonomy + block + REST) spans the per-kind directories and wires into each kind's module. If the project already has a per-feature module folder, flag as anti-pattern. Offer migration before adding new artifacts. Do not scaffold into it. @@ -163,6 +165,7 @@ Frameworks per kind: | `wp/block-dynamic` | Jest (edit.js) + PHPUnit (render method) | | `block/interactive` | Jest + Playwright | | `ci/*` | actionlint + yaml-parse | +| `utility/*` | none - no files written, so no stub to expand. Cover the calling code instead. | ### 8. Escalate when stuck - do not guess diff --git a/node-packages/wp-tooling/skills/setup/SKILL.md b/node-packages/wp-tooling/skills/setup/SKILL.md index 31701fc..1e39560 100644 --- a/node-packages/wp-tooling/skills/setup/SKILL.md +++ b/node-packages/wp-tooling/skills/setup/SKILL.md @@ -124,9 +124,18 @@ Map each feature the developer mentioned to one or more scaffold IDs from the ca | REST endpoint / API | `wp/rest` (if available) | | Cron job / background job | `wp/cron` (if available) | | Gutenberg block | `wp/block-dynamic` | -| Cache / transients | `utility/cache` | +| Object caching | `utility/cache` | +| Transients | `utility/transients` | +| Debug logging | `utility/logger` | +| Timing a slow code path | `utility/timer` | +| Feature flags / gradual rollout | `utility/feature-selector` | | CI pipeline | `ci/cd-wporg` (or other CI scaffold) | +The `utility/*` scaffolds are `source: package`: they write no files and return the +`rtcamp/wp-framework` dependency plus one accessor snippet for `/Helpers/Util.php`. +Check whether the project already exposes that accessor before applying the wiring — re-adding one +that already exists is a fatal redeclare. + Run `npx wp-tooling list --json` to see exactly what is available. If a feature the developer wants has no matching scaffold, note it explicitly as a manual task in the final report. For each feature scaffold, you need the same project-convention information as the `scaffold` skill requires (namespace, base path, class suffix, registration pattern). Collect this once from the project and cache it. diff --git a/node-packages/wp-tooling/tests/scaffolds/bundled-manifests.test.js b/node-packages/wp-tooling/tests/scaffolds/bundled-manifests.test.js index 2c44225..66f6dca 100644 --- a/node-packages/wp-tooling/tests/scaffolds/bundled-manifests.test.js +++ b/node-packages/wp-tooling/tests/scaffolds/bundled-manifests.test.js @@ -6,6 +6,8 @@ * - wp/cli namespace + tests_namespace discovery grafts the project's * PSR-4 root onto the kind sub-namespace * - wiring targetFile paths are normalised (no `..` segments) + * - utility/* are source: package — zero files, a Composer dep and one + * accessor snippet, with context_slug discovered from composer.json:name */ 'use strict'; @@ -88,3 +90,111 @@ describe('wiring targetFile normalisation', () => { expect(target).not.toContain('..'); }); }); + +// [id, framework class basename] +const UTILITY = [ + ['utility/cache', 'Cache'], + ['utility/transients', 'Transients'], + ['utility/logger', 'Logger'], + ['utility/timer', 'Timer'], + ['utility/feature-selector', 'FeatureSelector'], +]; + +// Target dir carrying the demo skeleton's package name, so context_slug +// discovery has something to resolve. +function targetWithComposerName(name = 'rtcamp/project-name-features') { + const target = makeTmpDir(); + fs.writeFileSync( + path.join(target, 'composer.json'), + JSON.stringify({ name }), + 'utf8' + ); + return target; +} + +describe('utility/* package scaffolds', () => { + it.each(UTILITY)( + '%s writes nothing and reports the dep plus one accessor snippet', + async (id, className) => { + const result = await registry.execute( + id, + {}, + { dryRun: true, cwd: targetWithComposerName() } + ); + + expect(result.scaffold.kind).toBe('package'); + expect(result.engine.wrote).toEqual([]); + expect(result.engine.skipped).toEqual([]); + expect(result.ai.tests).toEqual([]); + expect(result.developer.secrets).toEqual([]); + expect(result.developer.install.composer).toEqual({ + 'rtcamp/wp-framework': '^1.0', + }); + + expect(result.ai.wiring).toHaveLength(1); + const w = result.ai.wiring[0]; + expect(w.anchor).toBe(`// scaffold:${id}`); + expect(w.targetFile).toBe('includes/Helpers/Util.php'); + expect(w.targetFile).not.toContain('..'); + expect(w.snippet).toContain( + `\\rtCamp\\WPFramework\\Utils\\${className}` + ); + // The engine passes `description` through verbatim, so it must not + // carry a placeholder that would reach the caller unresolved. + expect(w.description).not.toContain('{{'); + } + ); + + it('discovers context_slug from composer.json:name, snake-cased', async () => { + const result = await registry.execute( + 'utility/cache', + {}, + { dryRun: true, cwd: targetWithComposerName() } + ); + expect(result.engine.inputs.context_slug).toBe( + 'rtcamp_project_name_features' + ); + expect(result.ai.wiring[0].snippet).toContain( + "new \\rtCamp\\WPFramework\\Utils\\Cache( 'rtcamp_project_name_features' )" + ); + }); + + it('prefers a supplied context_slug over the discovered one', async () => { + const result = await registry.execute( + 'utility/transients', + { context_slug: 'acme_blog' }, + { dryRun: true, cwd: targetWithComposerName() } + ); + expect(result.engine.inputs.context_slug).toBe('acme_blog'); + expect(result.ai.wiring[0].snippet).toContain("( 'acme_blog' )"); + }); + + it('falls back to the default slug when there is no composer.json', async () => { + const result = await registry.execute( + 'utility/logger', + {}, + { dryRun: true, cwd: makeTmpDir() } + ); + expect(result.engine.inputs.context_slug).toBe('my_plugin'); + }); + + it('honours a base_path override in the wiring target', async () => { + const result = await registry.execute( + 'utility/cache', + { base_path: 'inc' }, + { dryRun: true, cwd: targetWithComposerName() } + ); + expect(result.ai.wiring[0].targetFile).toBe('inc/Helpers/Util.php'); + }); + + it('constructs Timer with no argument — it takes no context', async () => { + const result = await registry.execute( + 'utility/timer', + {}, + { dryRun: true, cwd: targetWithComposerName() } + ); + const snippet = result.ai.wiring[0].snippet; + expect(snippet).toContain('new \\rtCamp\\WPFramework\\Utils\\Timer()'); + expect(snippet).not.toContain('rtcamp_project_name_features'); + }); +}); From ddd6ef27f119359bc520d437b5e8972918c1d290 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 5 Aug 2026 17:37:41 +0530 Subject: [PATCH 2/2] fix(scaffolds): resolve utility scaffold review feedback --- .../scaffolds/setup/claude-skills/templates/setup-SKILL.md | 4 ++-- .../wp-tooling/scaffolds/utility/cache/scaffold.json | 2 +- .../scaffolds/utility/feature-selector/scaffold.json | 6 +++--- .../wp-tooling/scaffolds/utility/logger/scaffold.json | 2 +- .../wp-tooling/scaffolds/utility/timer/scaffold.json | 2 +- .../wp-tooling/scaffolds/utility/transients/scaffold.json | 2 +- node-packages/wp-tooling/skills/setup/SKILL.md | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/setup-SKILL.md b/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/setup-SKILL.md index 1e39560..28f7453 100644 --- a/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/setup-SKILL.md +++ b/node-packages/wp-tooling/scaffolds/setup/claude-skills/templates/setup-SKILL.md @@ -133,8 +133,8 @@ Map each feature the developer mentioned to one or more scaffold IDs from the ca The `utility/*` scaffolds are `source: package`: they write no files and return the `rtcamp/wp-framework` dependency plus one accessor snippet for `/Helpers/Util.php`. -Check whether the project already exposes that accessor before applying the wiring — re-adding one -that already exists is a fatal redeclare. +Check whether the project already exposes that accessor before applying the wiring — pasting in a +method name that already exists in `Helpers\Util` is a PHP fatal error (cannot redeclare method). Run `npx wp-tooling list --json` to see exactly what is available. If a feature the developer wants has no matching scaffold, note it explicitly as a manual task in the final report. diff --git a/node-packages/wp-tooling/scaffolds/utility/cache/scaffold.json b/node-packages/wp-tooling/scaffolds/utility/cache/scaffold.json index 9e68c21..b035be8 100644 --- a/node-packages/wp-tooling/scaffolds/utility/cache/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/utility/cache/scaffold.json @@ -2,7 +2,7 @@ "slug": "cache", "category": "utility", "name": "Cache utility", - "description": "Registers the framework Cache utility from rtcamp/wp-framework as a shared service. Writes no files: reports the Composer dependency and one accessor snippet. Cache wraps the WordPress object cache with a per-plugin group namespace, group flushing, and the remember() / remember_swr() compute-once helpers.", + "description": "Wires up the framework Cache utility from rtcamp/wp-framework via an accessor snippet. Writes no files: reports the Composer dependency and one accessor snippet for the developer or AI to paste in. Cache wraps the WordPress object cache with a per-plugin group namespace, group flushing, and the remember() / remember_swr() compute-once helpers.", "source": "package", "module_class": "rtCamp\\WPFramework\\Utils\\Cache", "files": [], diff --git a/node-packages/wp-tooling/scaffolds/utility/feature-selector/scaffold.json b/node-packages/wp-tooling/scaffolds/utility/feature-selector/scaffold.json index 1e7af11..8daf982 100644 --- a/node-packages/wp-tooling/scaffolds/utility/feature-selector/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/utility/feature-selector/scaffold.json @@ -2,7 +2,7 @@ "slug": "feature-selector", "category": "utility", "name": "Feature selector utility", - "description": "Registers the framework FeatureSelector utility from rtcamp/wp-framework as a shared service. Writes no files: reports the Composer dependency and one accessor snippet. FeatureSelector registers named feature flags and resolves each one from a PHP constant or a shared option through register / is_enabled / enable / disable, failing closed for anything unregistered.", + "description": "Wires up the framework FeatureSelector utility from rtcamp/wp-framework via an accessor snippet. Writes no files: reports the Composer dependency and one accessor snippet for the developer or AI to paste in. FeatureSelector registers named feature flags and resolves each one from a PHP constant or a shared option through register / is_enabled / enable / disable, failing closed for anything unregistered.", "source": "package", "module_class": "rtCamp\\WPFramework\\Utils\\FeatureSelector", "files": [], @@ -24,8 +24,8 @@ { "target_file": "{{base_path}}/Helpers/Util.php", "anchor": "// scaffold:utility/feature-selector", - "snippet_template": "/**\n * The project's shared framework FeatureSelector. One instance holds the whole\n * flag registry, so every caller sees the same registered features.\n *\n * @return \\rtCamp\\WPFramework\\Utils\\FeatureSelector Shared feature selector.\n */\npublic static function features(): \\rtCamp\\WPFramework\\Utils\\FeatureSelector {\n\tstatic $instance = null;\n\n\tif ( null === $instance ) {\n\t\t$instance = new \\rtCamp\\WPFramework\\Utils\\FeatureSelector( '{{context_slug}}' );\n\t}\n\n\treturn $instance;\n}", - "description": "Add a shared accessor for the framework FeatureSelector to the project's static helper class. The class is spelled FeatureSelector, not Feature_Selector. The snippet is self-contained: paste it inside the existing helper class body, register flags once during bootstrap with Util::features()->register( 'new-checkout' ), then read them with Util::features()->is_enabled( 'new-checkout' ). Sharing one instance is deliberate rather than incidental: the flag registry lives on the instance and unregistered flags fail closed, so a fresh instance per call would report every feature as disabled. Do not add the framework FeatureSelector to a Modules class or to Main::CLASSES — it implements neither Registrable nor Shareable, so the framework Loader would construct it with an empty context and derive the wrong option key. For an admin UI over these flags, subclass the framework FeatureSelectorSettingsPage and return this accessor from its get_selector(). Requires rtcamp/wp-framework to be installed first." + "snippet_template": "/**\n * The project's shared framework FeatureSelector. One instance holds the whole\n * flag registry, so every caller sees the same registered features.\n *\n * @return \\rtCamp\\WPFramework\\Utils\\FeatureSelector Shared feature selector.\n */\npublic static function feature_selector(): \\rtCamp\\WPFramework\\Utils\\FeatureSelector {\n\tstatic $instance = null;\n\n\tif ( null === $instance ) {\n\t\t$instance = new \\rtCamp\\WPFramework\\Utils\\FeatureSelector( '{{context_slug}}' );\n\t}\n\n\treturn $instance;\n}", + "description": "Add a shared accessor for the framework FeatureSelector to the project's static helper class. The method is named feature_selector(); if that name is already taken in the helper class, rename it before pasting. The class is spelled FeatureSelector, not Feature_Selector. The snippet is self-contained: paste it inside the existing helper class body, register flags once during bootstrap with Util::feature_selector()->register( 'new-checkout' ), then read them with Util::feature_selector()->is_enabled( 'new-checkout' ). Sharing one instance is deliberate rather than incidental: the flag registry lives on the instance and unregistered flags fail closed, so a fresh instance per call would report every feature as disabled. Do not add the framework FeatureSelector to a Modules class or to Main::CLASSES — it implements neither Registrable nor Shareable, so the framework Loader would construct it with an empty context and derive the wrong option key. For an admin UI over these flags, subclass the framework FeatureSelectorSettingsPage and return this accessor from its get_selector(). Requires rtcamp/wp-framework to be installed first." } ], "composer_dependencies": { diff --git a/node-packages/wp-tooling/scaffolds/utility/logger/scaffold.json b/node-packages/wp-tooling/scaffolds/utility/logger/scaffold.json index bed89ba..e3df50b 100644 --- a/node-packages/wp-tooling/scaffolds/utility/logger/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/utility/logger/scaffold.json @@ -2,7 +2,7 @@ "slug": "logger", "category": "utility", "name": "Logger utility", - "description": "Registers the framework Logger utility from rtcamp/wp-framework as a shared service. Writes no files: reports the Composer dependency and one accessor snippet. Logger writes prefixed debug / info / warning / error lines and stays silent unless WP_DEBUG is on.", + "description": "Wires up the framework Logger utility from rtcamp/wp-framework via an accessor snippet. Writes no files: reports the Composer dependency and one accessor snippet for the developer or AI to paste in. Logger writes prefixed debug / info / warning / error lines and stays silent unless WP_DEBUG is on.", "source": "package", "module_class": "rtCamp\\WPFramework\\Utils\\Logger", "files": [], diff --git a/node-packages/wp-tooling/scaffolds/utility/timer/scaffold.json b/node-packages/wp-tooling/scaffolds/utility/timer/scaffold.json index 70881d8..98a1c5a 100644 --- a/node-packages/wp-tooling/scaffolds/utility/timer/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/utility/timer/scaffold.json @@ -2,7 +2,7 @@ "slug": "timer", "category": "utility", "name": "Timer utility", - "description": "Registers the framework Timer utility from rtcamp/wp-framework as a shared service. Writes no files: reports the Composer dependency and one accessor snippet. Timer measures elapsed time with start / stop / lap and reports through get / get_all, warning via _doing_it_wrong() on misuse rather than throwing.", + "description": "Wires up the framework Timer utility from rtcamp/wp-framework via an accessor snippet. Writes no files: reports the Composer dependency and one accessor snippet for the developer or AI to paste in. Timer measures elapsed time with start / stop / lap and reports through get / get_all, warning via _doing_it_wrong() on misuse rather than throwing.", "source": "package", "module_class": "rtCamp\\WPFramework\\Utils\\Timer", "files": [], diff --git a/node-packages/wp-tooling/scaffolds/utility/transients/scaffold.json b/node-packages/wp-tooling/scaffolds/utility/transients/scaffold.json index 85b81ac..87c4eca 100644 --- a/node-packages/wp-tooling/scaffolds/utility/transients/scaffold.json +++ b/node-packages/wp-tooling/scaffolds/utility/transients/scaffold.json @@ -2,7 +2,7 @@ "slug": "transients", "category": "utility", "name": "Transients utility", - "description": "Registers the framework Transients utility from rtcamp/wp-framework as a shared service. Writes no files: reports the Composer dependency and one accessor snippet. Transients wraps get_transient / set_transient / delete_transient behind a mandatory per-project key prefix.", + "description": "Wires up the framework Transients utility from rtcamp/wp-framework via an accessor snippet. Writes no files: reports the Composer dependency and one accessor snippet for the developer or AI to paste in. Transients wraps get_transient / set_transient / delete_transient behind a mandatory per-project key prefix.", "source": "package", "module_class": "rtCamp\\WPFramework\\Utils\\Transients", "files": [], diff --git a/node-packages/wp-tooling/skills/setup/SKILL.md b/node-packages/wp-tooling/skills/setup/SKILL.md index 1e39560..28f7453 100644 --- a/node-packages/wp-tooling/skills/setup/SKILL.md +++ b/node-packages/wp-tooling/skills/setup/SKILL.md @@ -133,8 +133,8 @@ Map each feature the developer mentioned to one or more scaffold IDs from the ca The `utility/*` scaffolds are `source: package`: they write no files and return the `rtcamp/wp-framework` dependency plus one accessor snippet for `/Helpers/Util.php`. -Check whether the project already exposes that accessor before applying the wiring — re-adding one -that already exists is a fatal redeclare. +Check whether the project already exposes that accessor before applying the wiring — pasting in a +method name that already exists in `Helpers\Util` is a PHP fatal error (cannot redeclare method). Run `npx wp-tooling list --json` to see exactly what is available. If a feature the developer wants has no matching scaffold, note it explicitly as a manual task in the final report.