fix(26): retire validate.ts/verify.cjs cooperating-sibling for W005/W006-archived/I001 via generator (stacks on #156) (#158)
* test(26): reproduce W005/W006-archived/I001 false positives in CJS validate path Issue #26 (open-gsd/get-shit-done-redux): three validation drift items from PR #3479 were hand-ported to verify.cjs via PR #3806 but never routed through the generator pattern. This means they can drift again whenever validate.ts changes. RED tests assert that validate.generated.cjs exports four new items: - phaseDirNameRe (W005 regex — /^\d{2,}(?:\.\d+)*-[\w-]+$/) - MILESTONE_ARCHIVE_DIR_RE (W006-archived — /^v\d+.*-phases$/i) - PHASE_TOKEN_FROM_DIR_RE (W006-archived — phase dir token extractor) - canonicalPlanStem (I001 — plan/summary stem canonicalization) Three of four new export tests are RED (exports missing from generated artifact). Behavioral tests (W005 no-false-positive, W006-archived no-false-positive, I001 no-false-positive) are GREEN because #3806's hand-ported fixes are present. References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6), PR #3479 (original fix), PR #3806 (hand-port). * chore(26): extend gen-validate.mjs to export W005/W006-archived/I001 helpers Issue #26 (open-gsd/get-shit-done-redux): extend gen-validate.mjs (introduced in PR #156 / issue #6) to also extract the three drift items that PR #3806 hand-ported to verify.cjs but were never routed through the generator. New helpers added to gen-validate.mjs: phaseDirNameRe (PHASE_DIR_NAME_RE) — W005 phase directory naming regex. /^\d{2,}(?:\.\d+)*-[\w-]+$/ accepts multi-digit prefixes (999.1-foo valid). Requires adding PHASE_DIR_NAME_RE as a named constant to validate.ts so it appears as an extractable identifier in the compiled output. PHASE_TOKEN_FROM_DIR_RE — W006-archived regex; extracts phase token from directory names like "64-auth-service" → "64". Used by forEachArchivedPhaseToken() and collectDiskPhases() in verify.cjs. MILESTONE_ARCHIVE_DIR_RE — W006-archived regex; matches milestone archive directory names like "v1.0-phases". Used by listMilestoneArchiveDirs(). canonicalPlanStem() — I001 PLAN/SUMMARY stem canonicalization. '68-01-scaffolding' → '68-01'. Top-level named function in compiled output. Extraction approach: PHASE_TOKEN_FROM_DIR_RE and MILESTONE_ARCHIVE_DIR_RE are module-level const assignments, extracted via extractConstRegExp() (handles both `const` and `export const` prefixes). PHASE_DIR_NAME_RE is the new named export added to validate.ts in this commit. canonicalPlanStem is a top-level function, extracted via extractTopLevelFunction() (brace-balanced). validate.ts change: inline regex in Check 6 extracted to named constant PHASE_DIR_NAME_RE (exported) and Check 6 updated to reference it. References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6). * chore(26): regenerate validate.generated.cjs with W005/W006-archived/I001 helpers Re-run of sdk/scripts/gen-validate.mjs after extending it in the preceding commit. The artifact now exports seven items (was three): New (issue #26): phaseDirNameRe — /^\d{2,}(?:\.\d+)*-[\w-]+$/ (W005 check) PHASE_TOKEN_FROM_DIR_RE — phase token extractor regex (W006-archived) MILESTONE_ARCHIVE_DIR_RE — archive dir name matcher (W006-archived) canonicalPlanStem() — PLAN/SUMMARY stem canonicalization (I001) Existing (issue #6): phaseVariants() buildRoadmapPhaseVariants() buildNotStartedPhaseVariants() Freshness check: node sdk/scripts/check-validate-fresh.mjs → "fresh". References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6). * fix(26): migrate verify.cjs W005/W006-archived/I001 call sites to generated helpers Issue #26 (open-gsd/get-shit-done-redux): three hand-maintained items in verify.cjs now consumed from validate.generated.cjs (ADR-3524 §4 adapter pattern). Changes: - Top-of-file require(): extend to also destructure phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem from validate.generated.cjs (issue #26 exports). - Remove inline PHASE_TOKEN_FROM_DIR_RE and MILESTONE_ARCHIVE_DIR_RE constants (lines ~403-404). Now sourced from generated artifact. listMilestoneArchiveDirs and forEachArchivedPhaseToken pick them up via the require() at top of file. - Check 6 (W005): replace inline regex /^\d{2,}(?:\.\d+)*-[\w-]+$/ with phaseDirNameRe from validate.generated.cjs. No behavior change. - Remove inline canonicalPlanStem() function (~8 lines). Now sourced from validate.generated.cjs. Check 7 (I001) continues to call it as before. Public API of verify.cjs unchanged. Same migration shape as PR #156's Check 8. References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6), PR #3479 (original fix), PR #3806 (hand-port that #26 supersedes). * docs(26): extend ADR-3524 2026-05-23 amendment with #26 scope Extends the existing 2026-05-23 amendment (not a new dated section) to document the W005/W006-archived/I001 generator migration introduced by issue #26. Key points documented: - Four new exports added to validate.generated.cjs (phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem) - W006-archived coverage note: both fixes were already in verify.cjs from #3806; the gap was generator coverage of the regex constants - Extraction methods: extractConstRegExp() and extractTopLevelFunction() - Parity tests: tests/26-w005-w006-i001-cjs-drift-regression.test.cjs (7 tests) - Cross-reference: issue #26 completes the validate.ts ↔ verify.cjs migration scope started by issue #6 References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6), PR #3479 (original fix), PR #3806 (hand-port). * chore(26): add changeset fragment for W005/W006-archived/I001 generator migration Touches get-shit-done/bin/lib/validate.generated.cjs and verify.cjs which match USER_FACING_PREFIXES. Required by the fix-template checklist + the changeset-lint CI workflow. References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6).
This commit is contained in:
5
.changeset/w005-w006-i001-generator-migration.md
Normal file
5
.changeset/w005-w006-i001-generator-migration.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 26
|
||||
---
|
||||
W005/W006-archived/I001 drift items now covered by the validate.generated.cjs generator (gen-validate.mjs + validate.ts). PR #3806 hand-ported the behavioral fixes; issue #26 routes them through the generator so they cannot drift again. Per ADR-3524 generator framework introduced by PR #154, extended by PR #156.
|
||||
@@ -146,3 +146,60 @@ all RED on pre-fix `origin/main`). Covers each drift item with concrete fixtures
|
||||
**Allowlist:** `scripts/shared-module-handsync-allowlist.json` — `verify.cjs` entry updated to
|
||||
reference the generator and freshness check. Classification remains `cooperating-sibling` (verify.cjs
|
||||
is still a full implementation; only Check 8 helpers are generated).
|
||||
|
||||
#### Extension — issue #26: W005/W006-archived/I001 generator migration
|
||||
|
||||
PR #3479 fixed three false-positive classes in `sdk/src/query/validate.ts`. PR #3806 hand-ported
|
||||
the three fixes to `get-shit-done/bin/lib/verify.cjs` but did not route them through the generator
|
||||
— meaning they could drift again. Issue #26 closes this gap by extending `gen-validate.mjs`
|
||||
(introduced in this amendment above) to also extract and export the W005/W006-archived/I001 items.
|
||||
|
||||
**Four additional exports added to `validate.generated.cjs` (issue #26):**
|
||||
|
||||
1. **`phaseDirNameRe` (W005)** — The `PHASE_DIR_NAME_RE` constant `/^\d{2,}(?:\.\d+)*-[\w-]+$/`
|
||||
is now a named export from `validate.ts` and extracted by `gen-validate.mjs`. `verify.cjs`
|
||||
Check 6 consumes `phaseDirNameRe` from the generated artifact instead of an inline copy.
|
||||
Reproducer: `mkdir -p .planning/phases/999.1-foo` → zero W005 (previously fired with
|
||||
the `\d{2}` two-digits-only regex before PR #3806 / PR #3479).
|
||||
|
||||
2. **`PHASE_TOKEN_FROM_DIR_RE` (W006-archived)** — The regex constant previously inline in
|
||||
`verify.cjs`'s `forEachArchivedPhaseToken()` and `collectDiskPhases()`. Extracted from the
|
||||
module-level `const` in the compiled output. `verify.cjs` inline copy removed.
|
||||
|
||||
3. **`MILESTONE_ARCHIVE_DIR_RE` (W006-archived)** — The regex constant previously inline in
|
||||
`verify.cjs`'s `listMilestoneArchiveDirs()`. Extracted the same way. `verify.cjs` inline copy
|
||||
removed. Together `PHASE_TOKEN_FROM_DIR_RE` and `MILESTONE_ARCHIVE_DIR_RE` ensure the
|
||||
archive-walking logic uses the same patterns as `validate.ts`.
|
||||
|
||||
4. **`canonicalPlanStem` (I001)** — The top-level helper function previously inline in
|
||||
`verify.cjs` Check 7. Extracted via `extractTopLevelFunction()` (brace-balanced parser).
|
||||
`verify.cjs` inline copy removed. Fix: `68-01-scaffolding-PLAN.md` correctly matches
|
||||
`68-01-SUMMARY.md` — both reduce to `68-01` via `canonicalPlanStem()`.
|
||||
|
||||
**W006-archived coverage note:** Issue #26 describes W006-archived as "RELATED TO but DISTINCT
|
||||
FROM" PR #156's W006 fix. Investigation confirmed both fixes are ALREADY in `verify.cjs` (from
|
||||
PR #3806). The gap was generator coverage: the regex constants used by `forEachArchivedPhaseToken`
|
||||
were inline copies with no generator protection. This amendment closes that gap by extracting them.
|
||||
No new behavioral fix is required — the generator pattern extension is the deliverable.
|
||||
|
||||
**`validate.ts` change:** `PHASE_DIR_NAME_RE` promoted from inline anonymous regex to a named
|
||||
`export const` so it appears as an extractable identifier in the compiled ESM output.
|
||||
|
||||
**Extraction methods used:**
|
||||
- `extractConstRegExp()` (new in `gen-validate.mjs`) — handles `const` and `export const`
|
||||
single-line RegExp assignments. Used for `phaseDirNameRe`, `PHASE_TOKEN_FROM_DIR_RE`,
|
||||
`MILESTONE_ARCHIVE_DIR_RE`.
|
||||
- `extractTopLevelFunction()` (new in `gen-validate.mjs`) — brace-balanced parser for top-level
|
||||
named function declarations. Used for `canonicalPlanStem`.
|
||||
|
||||
**Parity tests:** `tests/26-w005-w006-i001-cjs-drift-regression.test.cjs` — 7 tests.
|
||||
- W005: no false positive for `999.1-foo`; W005 still fires for single-digit prefix.
|
||||
- W006-archived: no false W006 for phase archived under `milestones/v1.0-phases/`; unit tests
|
||||
for `MILESTONE_ARCHIVE_DIR_RE` and `PHASE_TOKEN_FROM_DIR_RE` export and behavior.
|
||||
- I001: no false I001 when long-stem PLAN matches short-stem SUMMARY via `canonicalPlanStem`;
|
||||
I001 still fires when there is genuinely no SUMMARY; unit test for `canonicalPlanStem` export.
|
||||
|
||||
**Cross-references:** issue #26 cures the same false-positive scenarios as issue #6 but for
|
||||
the W005/W006-archived/I001 check paths. The artifact `validate.generated.cjs` now covers all
|
||||
six drift surfaces originally identified across both issues. This completes the validate.ts ↔
|
||||
verify.cjs migration scope for generator-pattern coverage.
|
||||
|
||||
@@ -6,25 +6,45 @@
|
||||
* Source: sdk/src/query/validate.ts
|
||||
* Regenerate: cd sdk && npm run gen:validate
|
||||
*
|
||||
* Validate Helpers — pure computation helpers for phase variant normalization,
|
||||
* roadmap phase variant set construction, and unchecked-phase skip set construction.
|
||||
* No I/O. No async. No filesystem operations.
|
||||
* Validate Helpers — pure computation helpers and regex constants extracted from
|
||||
* sdk/src/query/validate.ts. No I/O. No async. No filesystem operations.
|
||||
*
|
||||
* These three helpers cure the three drift items from issue #6:
|
||||
* Issue #6 drift items (three helpers):
|
||||
* 1. phaseVariants() — replaces parseInt-based padded/unpadded check in verify.cjs
|
||||
* Check 8 (W006 disk-existence and W007 roadmap-membership checks).
|
||||
* 2. buildRoadmapPhaseVariants() — replaces raw roadmapPhases set in W007 loop.
|
||||
* 3. buildNotStartedPhaseVariants() — replaces raw+zero-padded notStartedPhases
|
||||
* in W006 skip logic.
|
||||
*
|
||||
* Issue #26 drift items (four constants/helpers):
|
||||
* 4. phaseDirNameRe — W005 phase directory naming regex (was inline in verify.cjs Check 6).
|
||||
* 5. PHASE_TOKEN_FROM_DIR_RE — extracts phase token from dir name (was inline in
|
||||
* verify.cjs forEachArchivedPhaseToken / collectDiskPhases).
|
||||
* 6. MILESTONE_ARCHIVE_DIR_RE — identifies milestone archive directories (was inline).
|
||||
* 7. canonicalPlanStem() — I001 PLAN/SUMMARY stem canonicalization (was inline in Check 7).
|
||||
*
|
||||
* I/O adapter pattern (ADR-3524 §4): pure transforms extracted from the SDK.
|
||||
*
|
||||
* References:
|
||||
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
|
||||
* - Issue #6 (open-gsd/get-shit-done-redux)
|
||||
* - Issue #26 (open-gsd/get-shit-done-redux)
|
||||
* - PR #154 (issue #4) — generator pattern precedent
|
||||
* - PR #156 (issue #6) — validate.ts generator that #26 extends
|
||||
*/
|
||||
|
||||
// ── Issue #26: regex constants (W005, W006-archived) ────────────────────────
|
||||
const phaseDirNameRe = /^\d{2,}(?:\.\d+)*-[\w-]+$/;
|
||||
const PHASE_TOKEN_FROM_DIR_RE = /^(?:[A-Z]{1,6}-)?(\d+[A-Z]?(?:\.\d+)*)(?:-|$)/i;
|
||||
const MILESTONE_ARCHIVE_DIR_RE = /^v\d+.*-phases$/i;
|
||||
|
||||
// ── Issue #26: I001 canonicalization ────────────────────────────────────────
|
||||
function canonicalPlanStem(stem) {
|
||||
const m = stem.match(/^(\d+[A-Z]?(?:\.\d+)*-\d+)/i);
|
||||
return m ? m[1] : stem;
|
||||
}
|
||||
|
||||
// ── Issue #6: phase variant helpers (W006/W007) ──────────────────────────────
|
||||
function phaseVariants(phase) {
|
||||
|
||||
const variants = new Set([phase]);
|
||||
@@ -65,6 +85,12 @@ function buildNotStartedPhaseVariants(roadmapContent) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
// Issue #26 exports (W005 regex, W006-archived regex constants, I001 helper)
|
||||
phaseDirNameRe,
|
||||
PHASE_TOKEN_FROM_DIR_RE,
|
||||
MILESTONE_ARCHIVE_DIR_RE,
|
||||
canonicalPlanStem,
|
||||
// Issue #6 exports (W006/W007 phase variant helpers)
|
||||
phaseVariants,
|
||||
buildRoadmapPhaseVariants,
|
||||
buildNotStartedPhaseVariants,
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
* Verify — Verification suite, consistency, and health validation
|
||||
*/
|
||||
|
||||
const { phaseVariants, buildRoadmapPhaseVariants, buildNotStartedPhaseVariants } = require('./validate.generated.cjs');
|
||||
const {
|
||||
// Issue #6 exports (W006/W007 phase variant helpers)
|
||||
phaseVariants, buildRoadmapPhaseVariants, buildNotStartedPhaseVariants,
|
||||
// Issue #26 exports (W005 regex, W006-archived regex constants, I001 helper)
|
||||
phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem,
|
||||
} = require('./validate.generated.cjs');
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@@ -400,8 +405,8 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
||||
}, raw, verified === results.length ? 'valid' : 'invalid');
|
||||
}
|
||||
|
||||
const PHASE_TOKEN_FROM_DIR_RE = /^(?:[A-Z]{1,6}-)?(\d+[A-Z]?(?:\.\d+)*)(?:-|$)/i;
|
||||
const MILESTONE_ARCHIVE_DIR_RE = /^v\d+.*-phases$/i;
|
||||
// PHASE_TOKEN_FROM_DIR_RE and MILESTONE_ARCHIVE_DIR_RE are sourced from
|
||||
// validate.generated.cjs (issue #26, ADR-3524). No inline copies.
|
||||
|
||||
function listMilestoneArchiveDirs(planBase) {
|
||||
const milestonesDir = path.join(planBase, 'milestones');
|
||||
@@ -596,15 +601,8 @@ function cmdValidateConsistency(cwd, raw) {
|
||||
output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical plan stem used for PLAN/SUMMARY matching.
|
||||
* Mirrors canonicalPlanStem in sdk/src/query/validate.ts (#3479 / #3806).
|
||||
* Example: `68-01-scaffolding` -> `68-01`.
|
||||
*/
|
||||
function canonicalPlanStem(stem) {
|
||||
const m = stem.match(/^(\d+[A-Z]?(?:\.\d+)*-\d+)/i);
|
||||
return m ? m[1] : stem;
|
||||
}
|
||||
// canonicalPlanStem is sourced from validate.generated.cjs (issue #26, ADR-3524).
|
||||
// No inline copy — see top-of-file require() for the import.
|
||||
|
||||
function cmdValidateHealth(cwd, options, raw) {
|
||||
// Guard: detect if CWD is the home directory (likely accidental)
|
||||
@@ -787,8 +785,9 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// ─── Check 6: Phase directory naming (NN-name format) ─────────────────────
|
||||
// phaseDirNameRe sourced from validate.generated.cjs (issue #26, ADR-3524).
|
||||
for (const e of phaseDirEntries) {
|
||||
if (!e.name.match(/^\d{2,}(?:\.\d+)*-[\w-]+$/)) {
|
||||
if (!e.name.match(phaseDirNameRe)) {
|
||||
addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
* Generator for the Validate CJS artifact.
|
||||
*
|
||||
* Reads the compiled ESM output from sdk/dist/query/validate.js, extracts the
|
||||
* pure `phaseVariants` helper function via source-text extraction (it is a
|
||||
* closure inside validateHealth, not a module-level export), then emits
|
||||
* pure helpers and constants, then emits
|
||||
* get-shit-done/bin/lib/validate.generated.cjs.
|
||||
*
|
||||
* The generated module exports three pure helpers that were missing from
|
||||
* verify.cjs (the three drift items from issue #6):
|
||||
* The generated module exports seven items (three from issue #6, four from #26):
|
||||
*
|
||||
* Issue #6 drift items:
|
||||
* 1. phaseVariants(phase) — generates all normalized variants of a phase
|
||||
* token (padded/unpadded/letter-suffix). Used for W006 disk-existence check
|
||||
* and W007 roadmap-membership check in verify.cjs Check 8.
|
||||
@@ -23,10 +22,25 @@
|
||||
* unchecked-phase skip. verify.cjs previously added only raw+zero-padded
|
||||
* (dropping letter suffix via parseInt).
|
||||
*
|
||||
* Extraction approach: since phaseVariants is defined as a closure inside
|
||||
* validateHealth (not a module export), it is extracted from the compiled source
|
||||
* text using a brace-balanced parser — the same approach used to extract
|
||||
* escapeRegex in gen-phase-lifecycle-policy.mjs.
|
||||
* Issue #26 drift items:
|
||||
* 4. phaseDirNameRe (PHASE_DIR_NAME_RE) — regex constant for W005 phase
|
||||
* directory naming check. /^\d{2,}(?:\.\d+)*-[\w-]+$/ accepts multi-digit
|
||||
* prefixes. verify.cjs Check 6 previously had an inline copy.
|
||||
*
|
||||
* 5. PHASE_TOKEN_FROM_DIR_RE — regex constant used by forEachArchivedPhaseToken()
|
||||
* to extract the phase token from a directory name. verify.cjs had an inline copy.
|
||||
*
|
||||
* 6. MILESTONE_ARCHIVE_DIR_RE — regex constant used to identify milestone archive
|
||||
* directories under .planning/milestones/. verify.cjs had an inline copy.
|
||||
*
|
||||
* 7. canonicalPlanStem(stem) — converts a PLAN file stem to its canonical form for
|
||||
* PLAN/SUMMARY matching (I001 check). '68-01-scaffolding' → '68-01'.
|
||||
* verify.cjs Check 7 previously had an inline copy.
|
||||
*
|
||||
* Extraction approach: phaseVariants is a closure inside validateHealth (not a module
|
||||
* export), extracted via brace-balanced source-text parsing. Named constants and top-level
|
||||
* functions (PHASE_DIR_NAME_RE, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE,
|
||||
* canonicalPlanStem) are extracted by simple line-scanning from the compiled source.
|
||||
*
|
||||
* Run: cd sdk && npm run gen:validate
|
||||
* Check: node sdk/scripts/check-validate-fresh.mjs
|
||||
@@ -34,7 +48,9 @@
|
||||
* References:
|
||||
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
|
||||
* - Issue #6 (open-gsd/get-shit-done-redux)
|
||||
* - Issue #26 (open-gsd/get-shit-done-redux) — #26 extends issue #6's generator
|
||||
* - PR #154 (issue #4) — generator pattern precedent
|
||||
* - PR #156 (issue #6) — validate.ts generator that #26 extends
|
||||
*/
|
||||
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
@@ -48,23 +64,31 @@ export const BANNER = `'use strict';
|
||||
* Source: sdk/src/query/validate.ts
|
||||
* Regenerate: cd sdk && npm run gen:validate
|
||||
*
|
||||
* Validate Helpers — pure computation helpers for phase variant normalization,
|
||||
* roadmap phase variant set construction, and unchecked-phase skip set construction.
|
||||
* No I/O. No async. No filesystem operations.
|
||||
* Validate Helpers — pure computation helpers and regex constants extracted from
|
||||
* sdk/src/query/validate.ts. No I/O. No async. No filesystem operations.
|
||||
*
|
||||
* These three helpers cure the three drift items from issue #6:
|
||||
* Issue #6 drift items (three helpers):
|
||||
* 1. phaseVariants() — replaces parseInt-based padded/unpadded check in verify.cjs
|
||||
* Check 8 (W006 disk-existence and W007 roadmap-membership checks).
|
||||
* 2. buildRoadmapPhaseVariants() — replaces raw roadmapPhases set in W007 loop.
|
||||
* 3. buildNotStartedPhaseVariants() — replaces raw+zero-padded notStartedPhases
|
||||
* in W006 skip logic.
|
||||
*
|
||||
* Issue #26 drift items (four constants/helpers):
|
||||
* 4. phaseDirNameRe — W005 phase directory naming regex (was inline in verify.cjs Check 6).
|
||||
* 5. PHASE_TOKEN_FROM_DIR_RE — extracts phase token from dir name (was inline in
|
||||
* verify.cjs forEachArchivedPhaseToken / collectDiskPhases).
|
||||
* 6. MILESTONE_ARCHIVE_DIR_RE — identifies milestone archive directories (was inline).
|
||||
* 7. canonicalPlanStem() — I001 PLAN/SUMMARY stem canonicalization (was inline in Check 7).
|
||||
*
|
||||
* I/O adapter pattern (ADR-3524 §4): pure transforms extracted from the SDK.
|
||||
*
|
||||
* References:
|
||||
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
|
||||
* - Issue #6 (open-gsd/get-shit-done-redux)
|
||||
* - Issue #26 (open-gsd/get-shit-done-redux)
|
||||
* - PR #154 (issue #4) — generator pattern precedent
|
||||
* - PR #156 (issue #6) — validate.ts generator that #26 extends
|
||||
*/
|
||||
|
||||
`;
|
||||
@@ -99,12 +123,88 @@ function extractPhaseVariantsBody(validateSource) {
|
||||
return `function phaseVariants(phase) {\n${bodyContent}\n}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a top-level const RegExp assignment from the source.
|
||||
*
|
||||
* Looks for the line `const <name> = /<pattern>/<flags>;` and returns the full
|
||||
* assignment statement as a `module.exports`-compatible const declaration
|
||||
* (renaming to the export name when it differs from the source name).
|
||||
*
|
||||
* @param {string} source - Compiled JS source text
|
||||
* @param {string} sourceName - The const name as it appears in the compiled output
|
||||
* @param {string} [exportName] - The name to export under (defaults to sourceName)
|
||||
*/
|
||||
function extractConstRegExp(source, sourceName, exportName) {
|
||||
const nameToUse = exportName ?? sourceName;
|
||||
const lines = source.split('\n');
|
||||
// Match both `const <name> = ...` and `export const <name> = ...`
|
||||
const suffix = `const ${sourceName} = `;
|
||||
const line = lines.find((l) => l === suffix.trimStart() + l.slice(suffix.trimStart().length)
|
||||
|| l.startsWith(suffix) || l.startsWith(`export ${suffix}`));
|
||||
// Simpler: find a line that contains `const <name> = ` (anywhere after optional export)
|
||||
const matchLine = lines.find((l) => {
|
||||
const trimmed = l.replace(/^export\s+/, '');
|
||||
return trimmed.startsWith(`const ${sourceName} = `);
|
||||
});
|
||||
if (!matchLine) throw new Error(`Could not find "const ${sourceName} = ..." in compiled validate.js`);
|
||||
// Extract just the value (after `const <name> = `)
|
||||
const assignIdx = matchLine.indexOf(`const ${sourceName} = `);
|
||||
const valueStart = assignIdx + `const ${sourceName} = `.length;
|
||||
const value = matchLine.slice(valueStart).replace(/;$/, '').trim();
|
||||
return `const ${nameToUse} = ${value};`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a top-level named function declaration from the source.
|
||||
*
|
||||
* Matches `function <name>(<params>) {` and extracts the complete function body
|
||||
* using a brace-balanced parser.
|
||||
*
|
||||
* @param {string} source - Compiled JS source text
|
||||
* @param {string} name - The function name as it appears in the compiled output
|
||||
*/
|
||||
function extractTopLevelFunction(source, name) {
|
||||
// Match a top-level function declaration (not prefixed by spaces/async/export)
|
||||
const marker = `function ${name}(`;
|
||||
const start = source.indexOf(marker);
|
||||
if (start === -1) throw new Error(`Could not find top-level function "${name}" in compiled validate.js`);
|
||||
|
||||
const braceOpen = source.indexOf('{', start);
|
||||
let depth = 0;
|
||||
let i = braceOpen;
|
||||
for (; i < source.length; i++) {
|
||||
if (source[i] === '{') depth++;
|
||||
else if (source[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return source.slice(start, i + 1);
|
||||
}
|
||||
|
||||
export async function buildValidateCjs() {
|
||||
const distUrl = new URL('../dist/query/validate.js', import.meta.url);
|
||||
const validateSource = await readFile(fileURLToPath(distUrl), 'utf-8');
|
||||
|
||||
const phaseVariantsBody = extractPhaseVariantsBody(validateSource);
|
||||
|
||||
// Issue #26: extract regex constants and canonicalPlanStem from compiled output.
|
||||
//
|
||||
// phaseDirNameRe — the PHASE_DIR_NAME_RE constant added to validate.ts for W005.
|
||||
// Named 'phaseDirNameRe' in the export (camelCase for JS convention).
|
||||
const phaseDirNameReLine = extractConstRegExp(validateSource, 'PHASE_DIR_NAME_RE', 'phaseDirNameRe');
|
||||
|
||||
// PHASE_TOKEN_FROM_DIR_RE — extracts phase token from a directory name like "64-auth-service".
|
||||
// Exported under its original name for direct use in verify.cjs.
|
||||
const phaseTokenFromDirReLine = extractConstRegExp(validateSource, 'PHASE_TOKEN_FROM_DIR_RE');
|
||||
|
||||
// MILESTONE_ARCHIVE_DIR_RE — matches milestone archive dir names like "v1.0-phases".
|
||||
const milestoneArchiveDirReLine = extractConstRegExp(validateSource, 'MILESTONE_ARCHIVE_DIR_RE');
|
||||
|
||||
// canonicalPlanStem(stem) — I001 PLAN/SUMMARY stem canonicalization.
|
||||
// '68-01-scaffolding' → '68-01'. Top-level named function in the compiled output.
|
||||
const canonicalPlanStemBody = extractTopLevelFunction(validateSource, 'canonicalPlanStem');
|
||||
|
||||
// buildRoadmapPhaseVariants: parse ROADMAP.md and return {roadmapPhases, roadmapPhaseVariants}
|
||||
// roadmapPhases — raw phase tokens as written in headings (used for W006 check)
|
||||
// roadmapPhaseVariants — all normalized variants of each roadmap phase (used for W007 check)
|
||||
@@ -135,6 +235,18 @@ export async function buildValidateCjs() {
|
||||
const parts = [
|
||||
BANNER.trimEnd(),
|
||||
'',
|
||||
// Issue #26: regex constants extracted from compiled validate.js
|
||||
'// ── Issue #26: regex constants (W005, W006-archived) ────────────────────────',
|
||||
phaseDirNameReLine,
|
||||
phaseTokenFromDirReLine,
|
||||
milestoneArchiveDirReLine,
|
||||
'',
|
||||
// Issue #26: canonicalPlanStem (I001)
|
||||
'// ── Issue #26: I001 canonicalization ────────────────────────────────────────',
|
||||
canonicalPlanStemBody,
|
||||
'',
|
||||
// Issue #6: phaseVariants closure (W006/W007)
|
||||
'// ── Issue #6: phase variant helpers (W006/W007) ──────────────────────────────',
|
||||
phaseVariantsBody,
|
||||
'',
|
||||
buildRoadmapPhaseVariantsBody,
|
||||
@@ -142,6 +254,12 @@ export async function buildValidateCjs() {
|
||||
buildNotStartedPhaseVariantsBody,
|
||||
'',
|
||||
`module.exports = {
|
||||
// Issue #26 exports (W005 regex, W006-archived regex constants, I001 helper)
|
||||
phaseDirNameRe,
|
||||
PHASE_TOKEN_FROM_DIR_RE,
|
||||
MILESTONE_ARCHIVE_DIR_RE,
|
||||
canonicalPlanStem,
|
||||
// Issue #6 exports (W006/W007 phase variant helpers)
|
||||
phaseVariants,
|
||||
buildRoadmapPhaseVariants,
|
||||
buildNotStartedPhaseVariants,
|
||||
|
||||
@@ -32,6 +32,16 @@ const MAX_KEY_LINK_PATTERN_LEN = 512;
|
||||
const PHASE_TOKEN_FROM_DIR_RE = /^(?:[A-Z]{1,6}-)?(\d+[A-Z]?(?:\.\d+)*)(?:-|$)/i;
|
||||
const MILESTONE_ARCHIVE_DIR_RE = /^v\d+.*-phases$/i;
|
||||
|
||||
/**
|
||||
* Phase directory naming regex for Check 6 (W005).
|
||||
* Matches valid phase directory names: two-or-more digit prefix, optional dot-separated
|
||||
* sub-phase suffixes, a hyphen, and a word-character name.
|
||||
* Examples: 01-setup, 999-longphase, 999.1-foo.
|
||||
* Extracted into validate.generated.cjs as phaseDirNameRe so verify.cjs
|
||||
* is not a hand-synced copy (issue #26, ADR-3524).
|
||||
*/
|
||||
export const PHASE_DIR_NAME_RE = /^\d{2,}(?:\.\d+)*-[\w-]+$/;
|
||||
|
||||
/**
|
||||
* List milestone-archive directories under `.planning/milestones/`, sorted by
|
||||
* version (numeric — `v1.10` after `v1.2`). Mirrors `listMilestoneArchiveDirs`
|
||||
@@ -648,7 +658,7 @@ export const validateHealth: QueryHandler = async (args, projectDir, workstream)
|
||||
try {
|
||||
const entries = await readdir(phasesDir, { withFileTypes: true });
|
||||
for (const e of entries) {
|
||||
if (e.isDirectory() && !e.name.match(/^\d{2,}(?:\.\d+)*-[\w-]+$/)) {
|
||||
if (e.isDirectory() && !e.name.match(PHASE_DIR_NAME_RE)) {
|
||||
addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
|
||||
}
|
||||
}
|
||||
|
||||
250
tests/26-w005-w006-i001-cjs-drift-regression.test.cjs
Normal file
250
tests/26-w005-w006-i001-cjs-drift-regression.test.cjs
Normal file
@@ -0,0 +1,250 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Regression tests for issue #26 (open-gsd/get-shit-done-redux).
|
||||
* Three generator-pattern drift items: W005 phaseDirNameRe,
|
||||
* W006-archived regex constants (PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE),
|
||||
* I001 canonicalPlanStem.
|
||||
*
|
||||
* After the generator migration, all three helpers are sourced from
|
||||
* validate.generated.cjs. If they diverge from validate.ts, these
|
||||
* tests go RED.
|
||||
*
|
||||
* References:
|
||||
* - Issue #26 (open-gsd/get-shit-done-redux) — three drift items + reproducer
|
||||
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
|
||||
* - PR #154 (issue #4) — generator pattern precedent
|
||||
* - PR #156 (issue #6) — validate.ts generator scaffolding (#26 extends this)
|
||||
* - Original PR #3479 — first validate.ts false-positive fix (never reached CJS)
|
||||
*/
|
||||
|
||||
const { describe, test, before, after } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const { runGsdTools } = require('./helpers.cjs');
|
||||
|
||||
function mkplanning(base) {
|
||||
const planningDir = path.join(base, '.planning');
|
||||
const phasesDir = path.join(planningDir, 'phases');
|
||||
fs.mkdirSync(phasesDir, { recursive: true });
|
||||
return { planningDir, phasesDir };
|
||||
}
|
||||
|
||||
function writeProjectMd(planningDir) {
|
||||
fs.writeFileSync(
|
||||
path.join(planningDir, 'PROJECT.md'),
|
||||
'# Project\n\n## What This Is\nTest.\n\n## Core Value\nTest.\n\n## Requirements\nTest.\n',
|
||||
);
|
||||
}
|
||||
|
||||
function writeStateMd(planningDir, phase) {
|
||||
fs.writeFileSync(
|
||||
path.join(planningDir, 'STATE.md'),
|
||||
`# State\n\n**Current Phase:** ${phase}\n**Status:** In progress\n`,
|
||||
);
|
||||
}
|
||||
|
||||
function writeConfigJson(planningDir) {
|
||||
fs.writeFileSync(
|
||||
path.join(planningDir, 'config.json'),
|
||||
JSON.stringify({ model_profile: 'balanced' }),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Drift Item W005: phaseDirNameRe ──────────────────────────────────────────
|
||||
//
|
||||
// Issue #26 reproducer (verbatim):
|
||||
// mkdir -p .planning/phases/999.1-foo
|
||||
// echo "# Roadmap" > .planning/ROADMAP.md
|
||||
// node .claude/get-shit-done/bin/gsd-tools.cjs validate health
|
||||
// # Bug: emits W005 about 999.1-foo not following NN-name format
|
||||
//
|
||||
// verify.cjs must consume phaseDirNameRe from validate.generated.cjs so
|
||||
// the regex /^\d{2,}(?:\.\d+)*-[\w-]+$/ is the single source of truth.
|
||||
|
||||
describe('Drift item W005 — phaseDirNameRe: 999.X-name dirs must not trigger W005', () => {
|
||||
let tmpDir;
|
||||
|
||||
before(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-26-d1-'));
|
||||
const { planningDir, phasesDir } = mkplanning(tmpDir);
|
||||
writeProjectMd(planningDir);
|
||||
writeStateMd(planningDir, '999.1');
|
||||
writeConfigJson(planningDir);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(planningDir, 'ROADMAP.md'),
|
||||
'# Roadmap\n\n- [x] **Phase 999.1:** Long Phase\n\n### Phase 999.1: Long Phase\n',
|
||||
);
|
||||
|
||||
// Exact reproducer from issue #26
|
||||
fs.mkdirSync(path.join(phasesDir, '999.1-foo'), { recursive: true });
|
||||
});
|
||||
|
||||
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
||||
|
||||
test('no W005 for 999.1-foo (multi-digit sub-phase prefix)', () => {
|
||||
const result = runGsdTools(['validate', 'health', '--json'], tmpDir);
|
||||
assert.strictEqual(result.success, true, `unexpected failure: ${result.error}`);
|
||||
const data = JSON.parse(result.output);
|
||||
const w005 = (data.warnings ?? []).filter((w) => w.code === 'W005');
|
||||
assert.strictEqual(w005.length, 0,
|
||||
`Expected zero W005 for 999.1-foo, got: ${JSON.stringify(w005)}`);
|
||||
});
|
||||
|
||||
test('phaseDirNameRe is exported from validate.generated.cjs', () => {
|
||||
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
|
||||
assert.ok(gen.phaseDirNameRe instanceof RegExp,
|
||||
'validate.generated.cjs must export phaseDirNameRe as a RegExp');
|
||||
const re = gen.phaseDirNameRe;
|
||||
assert.ok(re.test('01-setup'), 'should accept 01-setup');
|
||||
assert.ok(re.test('999-longphase'), 'should accept 999-longphase (3-digit prefix)');
|
||||
assert.ok(re.test('999.1-foo'), 'should accept 999.1-foo (sub-phase)');
|
||||
assert.ok(!re.test('1-shortname'), 'should reject single-digit prefix');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Drift Item W006-archived: PHASE_TOKEN_FROM_DIR_RE / MILESTONE_ARCHIVE_DIR_RE ─
|
||||
//
|
||||
// forEachArchivedPhaseToken() in verify.cjs uses two inline regex constants.
|
||||
// After migration both are sourced from validate.generated.cjs:
|
||||
// PHASE_TOKEN_FROM_DIR_RE — extracts token from dir name like "64-auth-service"
|
||||
// MILESTONE_ARCHIVE_DIR_RE — matches archive dirs like "v1.0-phases"
|
||||
//
|
||||
// Test: phase 64 was archived to milestones/v1.0-phases/64-auth-service/.
|
||||
// Without correct archive detection, W006 fires for "Phase 64 in ROADMAP.md
|
||||
// but no directory on disk".
|
||||
|
||||
describe('Drift item W006-archived — MILESTONE_ARCHIVE_DIR_RE and PHASE_TOKEN_FROM_DIR_RE', () => {
|
||||
let tmpDir;
|
||||
|
||||
before(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-26-d2-'));
|
||||
const { planningDir, phasesDir } = mkplanning(tmpDir);
|
||||
writeProjectMd(planningDir);
|
||||
writeStateMd(planningDir, '65');
|
||||
writeConfigJson(planningDir);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(planningDir, 'ROADMAP.md'),
|
||||
[
|
||||
'# Roadmap',
|
||||
'',
|
||||
'- [x] **Phase 65:** Current Work',
|
||||
'',
|
||||
'### Phase 65: Current Work',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>Milestone v1.0 — Shipped</summary>',
|
||||
'',
|
||||
'### Phase 64: Auth Service',
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
// Active phasesDir: only phase 65
|
||||
fs.mkdirSync(path.join(phasesDir, '65-current-work'), { recursive: true });
|
||||
|
||||
// Archive: milestones/v1.0-phases/64-auth-service
|
||||
// MILESTONE_ARCHIVE_DIR_RE must match "v1.0-phases"
|
||||
// PHASE_TOKEN_FROM_DIR_RE must extract "64" from "64-auth-service"
|
||||
fs.mkdirSync(
|
||||
path.join(planningDir, 'milestones', 'v1.0-phases', '64-auth-service'),
|
||||
{ recursive: true },
|
||||
);
|
||||
});
|
||||
|
||||
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
||||
|
||||
test('no W006 for Phase 64 archived under milestones/v1.0-phases/', () => {
|
||||
const result = runGsdTools(['validate', 'health', '--json'], tmpDir);
|
||||
assert.strictEqual(result.success, true, `unexpected failure: ${result.error}`);
|
||||
const data = JSON.parse(result.output);
|
||||
const w006 = (data.warnings ?? []).filter(
|
||||
(w) => w.code === 'W006' && /Phase 64/i.test(w.message),
|
||||
);
|
||||
assert.strictEqual(w006.length, 0,
|
||||
`Expected no W006 for archived Phase 64, got: ${JSON.stringify(w006)}`);
|
||||
});
|
||||
|
||||
test('MILESTONE_ARCHIVE_DIR_RE is exported and matches vN.N-phases dirs', () => {
|
||||
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
|
||||
assert.ok(gen.MILESTONE_ARCHIVE_DIR_RE instanceof RegExp,
|
||||
'validate.generated.cjs must export MILESTONE_ARCHIVE_DIR_RE');
|
||||
const re = gen.MILESTONE_ARCHIVE_DIR_RE;
|
||||
assert.ok(re.test('v1.0-phases'), 'should match v1.0-phases');
|
||||
assert.ok(re.test('v1.10-phases'), 'should match v1.10-phases');
|
||||
assert.ok(!re.test('phases'), 'should NOT match plain phases');
|
||||
assert.ok(!re.test('1.0-phases'), 'should NOT match missing v prefix');
|
||||
});
|
||||
|
||||
test('PHASE_TOKEN_FROM_DIR_RE is exported and extracts phase tokens correctly', () => {
|
||||
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
|
||||
assert.ok(gen.PHASE_TOKEN_FROM_DIR_RE instanceof RegExp,
|
||||
'validate.generated.cjs must export PHASE_TOKEN_FROM_DIR_RE');
|
||||
const re = gen.PHASE_TOKEN_FROM_DIR_RE;
|
||||
assert.strictEqual(re.exec('64-auth-service')?.[1], '64');
|
||||
assert.strictEqual(re.exec('03B-feature')?.[1], '03B');
|
||||
assert.strictEqual(re.exec('999.1-foo')?.[1], '999.1');
|
||||
assert.strictEqual(re.exec('CK-64-auth')?.[1], '64');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Drift Item I001: canonicalPlanStem ────────────────────────────────────────
|
||||
//
|
||||
// validate.ts Check 7: canonicalPlanStem('68-01-scaffolding') → '68-01'
|
||||
// verify.cjs had an inline copy. After migration, canonicalPlanStem is
|
||||
// sourced from validate.generated.cjs.
|
||||
//
|
||||
// Test: "68-01-scaffolding-PLAN.md" + "68-01-SUMMARY.md" → no I001
|
||||
// Both stems canonicalize to "68-01" → match found → I001 suppressed.
|
||||
|
||||
describe('Drift item I001 — canonicalPlanStem: long PLAN stem matches short SUMMARY stem', () => {
|
||||
let tmpDir;
|
||||
|
||||
before(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-26-d3-'));
|
||||
const { planningDir, phasesDir } = mkplanning(tmpDir);
|
||||
writeProjectMd(planningDir);
|
||||
writeStateMd(planningDir, '68');
|
||||
writeConfigJson(planningDir);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(planningDir, 'ROADMAP.md'),
|
||||
'# Roadmap\n\n- [x] **Phase 68:** Scaffolding\n\n### Phase 68: Scaffolding\n',
|
||||
);
|
||||
|
||||
const phaseDir = path.join(phasesDir, '68-scaffolding');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
// Long-stem PLAN + short-stem SUMMARY → must match via canonicalPlanStem
|
||||
fs.writeFileSync(path.join(phaseDir, '68-01-scaffolding-PLAN.md'), '---\nwave: 1\n---\n# Plan\n');
|
||||
fs.writeFileSync(path.join(phaseDir, '68-01-SUMMARY.md'), '# Summary\n');
|
||||
});
|
||||
|
||||
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
||||
|
||||
test('no I001 when 68-01-scaffolding-PLAN.md matches 68-01-SUMMARY.md via canonicalPlanStem', () => {
|
||||
const result = runGsdTools(['validate', 'health', '--json'], tmpDir);
|
||||
assert.strictEqual(result.success, true, `unexpected failure: ${result.error}`);
|
||||
const data = JSON.parse(result.output);
|
||||
const i001 = (data.info ?? []).filter((i) => i.code === 'I001');
|
||||
assert.strictEqual(i001.length, 0,
|
||||
`Expected zero I001, got: ${JSON.stringify(i001)}`);
|
||||
});
|
||||
|
||||
test('canonicalPlanStem is exported from validate.generated.cjs', () => {
|
||||
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
|
||||
assert.strictEqual(typeof gen.canonicalPlanStem, 'function',
|
||||
'validate.generated.cjs must export canonicalPlanStem as a function');
|
||||
assert.strictEqual(gen.canonicalPlanStem('68-01-scaffolding'), '68-01');
|
||||
assert.strictEqual(gen.canonicalPlanStem('68-01'), '68-01');
|
||||
assert.strictEqual(gen.canonicalPlanStem('3A-01-feature'), '3A-01');
|
||||
assert.strictEqual(gen.canonicalPlanStem('no-match'), 'no-match');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user