f7df920681f233ae0fe064ee659550bdf41ff708
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
444069d601 |
fix(#3613): copy component dirs into the plugin-validate fixture (#3627)
* fix(#3613): copy component dirs into the plugin-validate fixture C2 builds its synthetic plugin root by symlinking commands/, hooks/ and skills/ into a temp dir. `claude plugin validate` (>=2.1.233) reads component directories without following symlinks and warns on each one, and `--strict` promotes a warning to a non-zero exit — so the assertion failed on how the fixture was built, not on the manifest under test. Copy the directories with fs.cpSync instead. The validated tree still holds only plugin.json and the three component directories, so nothing else from the repo root is placed where the validator can read it, and the #2665 CLI-config isolation is untouched. The construction helper is self-cleaning: its callers' try/finally only begins once it returns, so a throw partway through would strand a half-built root on disk. The pre-refactor code ran these same steps inside C2's own try, and that teardown guarantee is preserved rather than narrowed. Its cleanup is best-effort so a teardown error cannot replace the real construction error. Add C3, an unconditional tripwire asserting the fixture exposes real, non-symlinked component directories at any depth. C2 never runs in CI — no job under .github/workflows/ installs the `claude` binary — so a revert to symlinks would pass every CI lane and surface only as a red suite on contributor machines. C3 is not a substitute for C2's end-to-end check and cannot be shown red against this base, since the helper it calls arrives with it; C2 is the failing-first artifact. C3 enforces a symlink-free fixture throughout, deliberately stricter than the CLI's own boundary. Measured on 2.1.234, `plugin validate --strict` exits 1 for a symlinked component dir and for a symlink one level inside skills/, and 0 for two levels in or for symlinks under commands/ or hooks/. Encoding that external, undocumented line would be more fragile than a superset costing one walk over ~176 files. The walk uses an explicit stack rather than readdirSync's `recursive: true`, which follows directory symlinks — a link pointing outward would otherwise traverse an unrelated tree, or a cycle, before the assertion ran. Correct the Section C docstring, which claimed C2 provides defence-in-depth coverage it cannot provide in CI. Whether to provision the CLI in a CI job is a maintainer call and is left open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(#3613): skip hooks/dist and .dist-staging when building the fixture The recursive copy added for #3613 races the hook builders. build-hooks.js writes atomically through a per-PID hooks/.dist-staging-<pid> and removes it when done, and nine test files invoke that script from their before() hooks, so a recursive walk can enumerate a staging directory and then lstat it after the owning process deleted it — the ENOENT that #3656 just fixed in the cold-tree fixture. Copy entry-by-entry and skip by NAME BEFORE anything stats it, reusing shouldCopyHookEntry from tests/helpers/cold-runtime-lib-fixture.cjs rather than re-deriving the rule. A filter applied after the stat would not close it. The predicate is already pinned including its over-match cases, which a loose startsWith('dist') would get wrong: dist-staging-no-dot and distant.js must both be kept. Excluding hooks/dist is independently right for this fixture — a real marketplace install contains neither dist nor a transient staging dir, the same reasoning that keeps the repo-root CLAUDE.md out of the validated tree. C2 still validates clean without it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(#3613): validate agents/ too, and scope the hooks predicate Three review Minors. agents/ ships in package.json `files` and IS auto-validated by the CLI — verified: a frontmatter-less agents/*.md exits 1. Including it was pointless while the fixture symlinked, because the CLI read nothing through a symlink; now that the tree is real it is the last shipped component tree C2 could not see. Proven to buy coverage rather than bytes: planting a frontmatter-less agent now reds C2, which it could not do before this change. shouldCopyHookEntry is documented as a hooks/ entry filter, so it now runs only for hooks/. Applying it to the other trees was harmless today but silently encoded a hooks-shaped exclusion into them — a future commands/dist would have vanished from the validated tree with no signal. assert.deepEqual -> deepStrictEqual on the nested-symlink assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(#3613): scope the fixture to the three directories the issue names Review findings, all five. Major 1 — `agents/` dropped from COMPONENT_DIRS. The observation behind adding it was right (the CLI does auto-validate it; a frontmatter-less agents/*.md exits 1), but it is a NEW gate the issue does not ask for, on the largest of the trees, and since C2 never runs in CI it would be red only on contributor machines with `claude` installed — the same worst-of-both-states #3613 exists to remove. Worth having as its own issue, where "should CI provision the CLI" gets answered for it too. Major 2 — C3 no longer passes on a fixture that validates nothing. buildValidationPluginRoot() mkdir's every component dir before the entry loop, so a copy that stops happening leaves real, EMPTY directories and all three structural assertions still hold (an empty tree contains no symlinks). Adds a non-empty assertion plus a known entry per tree, so a partial copy is caught as well. Stubbing the copy now reds C3 with "commands/ is EMPTY"; dropping just commands/gsd reds it too. Neither did before. Minor 1 — the measured table was wrong, and the mistake was measuring an inert file. Re-measured on CLI 2.1.239, reproducing the review's 2.1.237 result: a symlinked skills/<name>/SKILL.md at depth 2 exits 1, while a stray symlinked *.md at the same depth exits 0. The boundary is not depth at all, it is whether the symlink is a file the CLI reads as a component. Table replaced with that. Minor 2 — the hooks name filter is now local instead of importing cold-runtime-lib-fixture.cjs's, whose docstring scopes it to the cold-tree fixture and which had exactly one caller. Two consumers across fixtures with different requirements and nothing asserting they stay compatible is how a later cold-tree change silently alters what this fixture validates. Nit 1 — cost figures re-measured: ~1.06 MB over 176 files, not 2.1 MB over 243. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(#3613): correct the hooks row and the count the agents/ removal left behind Three comment-prose items from review, no code change. N1 — the corrected table gained a new wrong row, erring unsafe. "symlink inside commands/ or hooks/ -> 0" is false for hooks/hooks.json, which is the single most likely thing anyone would symlink there. Re-measured on CLI 2.1.239, matching the review's 2.1.237 figures on every cell: symlink inside commands/ (a dir, or a component *.md) 0 stray symlinked dir or *.js inside hooks/ 0 symlinked hooks/hooks.json 1 hooks/hooks.json is now its own row, and is named in the C3 assertion message alongside SKILL.md — that message is what the next engineer actually reads. N2 — "the four component directories" was residue from the revision that also copied agents/; it survived the very edit that removed it, three lines above a sentence saying three. Now three. N3 — the promised agents/ follow-up is filed as #3751 and the comment cites it, rather than promising an issue that did not exist. It frames the real decision (should CI provision the claude CLI) rather than just asking for the directory back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
69e7afd0c7 |
chore(#3212): bounded quantifiers over document content — prohibition with teeth — Phase 4 (#3441)
* feat(#3415): ship local/no-unbounded-quantifier, burn down ReDoS class Phase 4 of epic #3212 (ADR-3212 §5/§7, the final phase). New rule flags an unbounded */+/{n,} quantifier over a broad character class ([\s\S], dotAll ., or a 1-2-unit negated class like [^\n]/[^)\n] — the exact #2128-fixed shape) applied to a regex whose match target is data-flow-traced to readFileSync content. eslint-rules/lib/readfilesync-trace.cjs extracts the data-flow tracer shared with no-crlf-fragile-split (Phase 2) rather than a second copy — no-crlf-fragile-split refactored onto it with zero behavior change, parity-tested. Real triage, not 798 mechanical edits: the ADR's census (2026-08-08) screened every unbounded quantifier in the tree unscoped. Correctly scoped to readFileSync-derived content (matching Phase 2's own G2/G3 scoping), the rule found 162 real hits across two detection waves — the second wave (93) surfaced only after a genuine off-by-one bug in this rule's own first draft was caught while writing its RuleTester tests and fixed (the bug silently missed every directly-quantified [\s\S]* with no gap before the quantifier — exactly the class this rule exists to catch). 3 hits landed in production src/ (commands.cts, milestone.cts, roadmap.cts) and were each empirically timed against adversarial input (matching #2128's own measured-not-assumed precedent) — all confirmed linear-time/benign, left unbounded with a measured-evidence comment rather than mechanically bounded. The remaining 159 are test-file fixture parsing (test-author-controlled, fixed-size content, not adversarial input) — each suppressed with a specific, non-generic reason. Zero functional behavior changed anywhere in this diff. tests/no-pending-3212-markers.test.cjs locks the epic's own closing invariant (ADR §7: "assert zero pending #3212 markers remain") — ground truth confirmed trivially true today (no phase left any such marker behind), now regression-locked going forward. Design: .gsd/phase/chore-3415-prohibition-with-teeth/40-design.md Test matrix: .gsd/phase/chore-3415-prohibition-with-teeth/50-test-matrix.md Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3415): correct rule category mislabel, add CI test-scope entry An orthogonal Standards-axis review found eslint-rules/no-unbounded-quantifier.cjs mistakenly carried meta.docs.category: 'Portability', copied from a sibling rule without realizing what that implied: docs/contributing/cross-platform- portability-rules.md governs an ADR-1703 rule family under a hard "zero escape hatches" contract (tests/portability-rule-disable-ban.test.cjs's PROTECTED_RULES bans eslint-disable for those rules entirely). This rule is not part of that family — it's ADR-3212 (ReDoS/CWE-1333), a different epic — and its eslint-disable-next-line suppressions (159 of them, added earlier this same phase after empirical benign-verification) are an intentional, correct design, not a bypass. Corrected to category: 'Best Practices', matching the actual precedent (no-adhoc-regex-escape.cjs, Phase 1 of the same epic, which is also correctly outside PROTECTED_RULES), and the rule's own docstring now states this explicitly so a future reader doesn't have to re-derive it. Also registers a new scripts/ci-test-scope.cjs bucket so editing this rule or the shared eslint-rules/lib/readfilesync-trace.cjs helper re-runs their own test suites under targeted CI selection — was previously unregistered and invisible to that fast-path (this PR's own gsd-test checkpoint runs the full suite regardless, so this only affects future narrowly-scoped PRs). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3415): bound no-unbounded-quantifier's own scanner (CWE-1333, ironic) Security review found the rule meant to catch algorithmic-complexity bugs had one of its own: hasUnboundedBroadQuantifier's negated-class inner scan walked from each `[^` occurrence to the next `]` (or EOF) with no bound, while the outer loop only ever advanced by one character — O(n²) total work on a pattern with many unclosed `[^` runs. Runs unconditionally inside checkPattern on any `new RegExp('literal string')` argument in any linted file, before the (cheap) readFileSync data-flow gate — so a single crafted string literal, no valid regex syntax required, could make `npm run lint` / CI hang. Empirically confirmed both the bug and the fix: pre-fix, n=4000/8000/ 16000/32000 chars took 30.8/115.6/463.8/1874.3ms (~4x work per 2x n, quadratic); extrapolated, the 300000-char repro from the finding would run ~165s. Post-fix (bail the inner scan once units exceeds the rule's own 1-2-unit scope, rather than continuing to hunt for a closing `]`), the same 300000-char input runs in 8.7ms via the real rule module, independently reconfirmed at 18ms via a fresh Linter.verify() call. New regression row in tests/no-unbounded-quantifier.rule.test.cjs asserts the RuleTester run on a 50000-char adversarial pattern completes and returns a defined result — no wall-clock assertion (CLAUDE.md Clock Seams / local/no-elapsed-assertion). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3415): triage 3 new sites, re-raise ceiling after upstream batch next merged 12 more PRs during this PR's review. Two consequences: - tests/edit-phase.test.cjs (fix #3262, unrelated) added 3 new content.match(/<tag>([\s\S]*?)<\/tag>/) reads of this repo's own workflow .md content — the same Class A pattern as the ~159 sites already triaged elsewhere in this PR. Suppressed with the same established reason. - lint-allow-test-rule-refs' ratchet ceiling needed re-raising again (301 -> 303) for the same reason as the two prior bumps: organic growth from unrelated, already-reviewed PRs landing concurrently, not a defect in this branch's own diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
aee83c8e9e |
test(#3337): fold the manifest & package-identity issue-* cluster — Wave 5 (#3378)
* test(#3337): fold the manifest & package-identity issue-* cluster — Wave 5 Folds 6 legacy issue-*.test.cjs regression files (120 test() blocks) into their module's main suite, per H3 (#3315) of the test-hygiene epic (#3053). Second of 4 issue-* waves. - issue-766-plugin-manifest.test.cjs (50 tests): pure rename (git mv) into plugin-manifest.test.cjs, sole comprehensive suite for its module. - issue-844-manifest-version-sync.test.cjs (14 tests, rename basis) + issue-1855-marketplace-manifest.test.cjs (17 tests, merged in): both target scripts/sync-manifest-versions.cjs via non-overlapping describe blocks (generic engine vs. marketplace.json-specific), now manifest-version-sync.test.cjs, 31 tests, 0 dropped. - issue-498-identity-drift-lint.test.cjs (8 tests, rename basis) + issue-498-package-identity.test.cjs (17 tests, merged in): both target package-identity.cjs's surface from different angles (lint-side drift detection vs. derive/slugify), now package-identity.test.cjs, 25 tests, 0 dropped. - issue-607-cache-lineage.test.cjs (14 tests) merged into the existing gsd-statusline.test.cjs, matching its already-established fold-wrapper convention from prior waves. Learned from Wave 4: fold agents checked src/** (not just docs/ and gsd-core/references/) for stale filename references — found and fixed 5 across 3 ADR docs (766, 2121, 457), zero in src/ this time. Zero net test-coverage loss. No production code changed. * test(#3337): fix orthogonal-review findings — Wave 5 fold Standards-axis review found real issues in the just-folded manifest-version-sync.test.cjs, both fixed here: - The folded:issue-1855-marketplace-manifest block omitted its own test/describe/assert/fs/path/os requires, silently closing over the #844 basis section's outer-scope bindings instead of declaring its own dependencies — inconsistent with every other fold block in this wave. Restored the local requires the original issue-1855 file declared. - Merging two independently-lettered legacy files (each A-F) left duplicate top-level describe() labels (two "A:", two "B:", two "C:"). Renamed the folded-in #1855 labels (A2/B4/C2) to make every top-level label in the file unique. No test() count changed (31). No production code touched. --------- Co-authored-by: sim <sim@local> |