From dea9b54ed9c8f70e6b52a17bce29ded64c615e4d Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sat, 8 Aug 2026 15:50:24 +0200 Subject: [PATCH] fix(phpcs): stop the SpecTagSniff instructing the pattern gate-46 rejects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two tools in the same pipeline gave OPPOSITE instructions about where an `@spec` tag should point, and following the one that runs FIRST manufactured findings for the one that runs SECOND. `SpecTagSniff` runs as a blocking `PHP Quality (phpcs)` job and told every developer, in its file docblock and in its own warning text: @spec openspec/changes/{change-name}/tasks.md#task-N A change directory is temporary by definition — completing a change moves it to `openspec/changes/archive/-/`, and renaming or dropping one removes the target outright. Every tag written to that instruction dangles from that moment on, and gate-46 (spec-anchor-existence) reports it. The developer who wrote the tag had followed this sniff's own advice. Measured on portaliq: 100 unresolved gate-46 targets, and 260 of its 385 live tags pointing into a change directory. The sniff ships identically in 20 ConductionNL repos, so grinding the tags without fixing the sniff regenerates them at the rate changes are archived. This changes the docblock example and BOTH warning messages to the canonical form gate-46 and the project rule agree on: @spec openspec/specs/{capability}/spec.md#requirement-{slug} The method-level message previously carried no guidance at all, so a developer reading it had only the class message to copy from; it now names the same canonical shape. Behaviour is unchanged: severity stays WARNING (verified via phpcs — an untagged class and public method still report 0 errors / 2 warnings, and a tagged file still reports nothing), and an `openspec/changes/...` target is still accepted, since this sniff only checks that a tag is PRESENT. No `@spec` tags are repointed here — this repo's existing tags are untouched. Refs ConductionNL/.github#228 --- .../Sniffs/Commenting/SpecTagSniff.php | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/phpcs-custom-sniffs/CustomSniffs/Sniffs/Commenting/SpecTagSniff.php b/phpcs-custom-sniffs/CustomSniffs/Sniffs/Commenting/SpecTagSniff.php index ae6ad7ee..60e75383 100644 --- a/phpcs-custom-sniffs/CustomSniffs/Sniffs/Commenting/SpecTagSniff.php +++ b/phpcs-custom-sniffs/CustomSniffs/Sniffs/Commenting/SpecTagSniff.php @@ -3,10 +3,25 @@ * Warn when a PHP class or public method lacks an @spec PHPDoc tag. * * ConductionNL ADR-003 (Backend rules) mandates that every class and public - * method MUST have one or more @spec PHPDoc tags linking back to the OpenSpec - * change that caused the code to exist: + * method MUST have one or more @spec PHPDoc tags linking back to the + * requirement the code exists to satisfy: * - * @spec openspec/changes/{change-name}/tasks.md#task-N + * @spec openspec/specs/{capability}/spec.md#requirement-{slug} + * + * POINT AT THE CANONICAL SPEC, NOT AT A CHANGE DIRECTORY. + * + * This guidance used to read `openspec/changes/{change-name}/tasks.md#task-N`, + * and tags written to it dangle by construction: `openspec/changes//` is + * temporary — archiving moves it to `openspec/changes/archive/-/`, + * and renaming or dropping a change removes the target outright. Gate-46 + * (spec-anchor-existence) then reports the tag, and the developer who wrote it + * had followed this sniff's own instruction. Measured on portaliq 2026-08-08: + * 100 unresolved targets, with 260 of its 385 live tags pointing into a change + * directory. The cross-repo measurement is in ConductionNL/.github#228. + * + * An `openspec/changes/...` target is still ACCEPTED — this sniff only checks + * that a tag is PRESENT, and gate-46 resolves archived paths — but it is no + * longer what this sniff tells you to write. * * This sniff emits warnings (not errors) so that CI surfaces the gap without * blocking merges while teams backfill coverage. Tests files and magic @@ -130,7 +145,7 @@ private function processClass(File $phpcsFile, int $stackPtr): void return; } - $message = 'Class %s is missing @spec PHPDoc tag — link back to openspec/changes/{name}/tasks.md#task-N'; + $message = 'Class %s is missing @spec PHPDoc tag — link back to openspec/specs/{capability}/spec.md#requirement-{slug}, not a change dir'; $phpcsFile->addWarning($message, $stackPtr, 'MissingClassSpec', [$className]); }//end processClass() @@ -179,7 +194,7 @@ private function processFunction(File $phpcsFile, int $stackPtr): void return; } - $message = 'Public method %s::%s() is missing @spec PHPDoc tag'; + $message = 'Public method %s::%s() is missing @spec PHPDoc tag — link back to openspec/specs/{capability}/spec.md#requirement-{slug}'; $phpcsFile->addWarning($message, $stackPtr, 'MissingMethodSpec', [$className, $methodName]); }//end processFunction()