@spardutti/claude-skills 2.64.0 → 2.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -77,10 +77,11 @@ export function triggerHits(entry, text) {
77
77
  return entry.triggers.filter((t) => new RegExp(`\\b${escape(t)}\\b`, "i").test(text));
78
78
  }
79
79
 
80
- // A location is a folder or one file, like sim/aim.gd beside unrelated systems in sim/.
80
+ export const locationsOf = (entry) => [].concat(entry.location ?? []).map((l) => l.replace(/^\.\//, "")).filter(Boolean);
81
+
82
+ // A location is a folder or files, like sim/aim.gd beside unrelated systems in sim/.
81
83
  export function insideLocation(entry, file) {
82
- const loc = (entry.location ?? "").replace(/^\.\//, "");
83
- return loc !== "" && (file === loc || file.startsWith(loc.replace(/\/?$/, "/")));
84
+ return locationsOf(entry).some((loc) => file === loc || file.startsWith(loc.replace(/\/?$/, "/")));
84
85
  }
85
86
 
86
87
  export const isAllowed = (entry, file) => entry.allowed.some((a) => (a.endsWith("/") ? file.startsWith(a) : file === a));
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
3
  import { dirname, join, relative, resolve } from "node:path";
4
4
  import {
5
- REGISTRY_SUFFIX, addedText, escape, insideLocation, isAllowed, loadRegistry, planMarks, projectFiles, projectRoot, symbolsOf, triggerHits,
5
+ REGISTRY_SUFFIX, addedText, escape, insideLocation, isAllowed, loadRegistry, locationsOf, planMarks, projectFiles, projectRoot, symbolsOf, triggerHits,
6
6
  } from "./registry-lib.mjs";
7
7
  import { loadEditRules } from "./text-rules.mjs";
8
8
 
@@ -61,7 +61,7 @@ function evaluate() {
61
61
  const outside = files.filter((f) => !insideLocation(e, f) && !isAllowed(e, f) && !f.endsWith(REGISTRY_SUFFIX));
62
62
  const hits = outside.flatMap((f) => readFileSync(join(root, f), "utf8").split("\n").flatMap((line, i) =>
63
63
  e.does.filter((code) => calls(line, code)).map((code) => `registry SUSPECT ${e.name} ${f}:${i + 1} ${code}`)));
64
- console.log(hits.length ? hits.join("\n") : `registry eval ${e.name}: nothing outside ${e.location} does its job`);
64
+ console.log(hits.length ? hits.join("\n") : `registry eval ${e.name}: nothing outside ${locationsOf(e).join(", ")} does its job`);
65
65
  }
66
66
  }
67
67
 
@@ -82,16 +82,30 @@ function stale() {
82
82
  if (/^\s+-\s/m.test(e.text)) say("write lists on one line, like triggers: [aim, target]");
83
83
  if (seen.has(e.name)) say(`name ${e.name} is used twice`);
84
84
  seen.add(e.name);
85
+ staleLocation(e, say);
85
86
  staleCode(e, say);
86
87
  }
87
88
  if (problems.length) fail(problems);
88
89
  }
89
90
 
91
+ // A folder wide enough to hold another system hides that folder from eval.
92
+ function staleLocation(e, say) {
93
+ const bare = (l) => l.replace(/\/$/, "");
94
+ const own = locationsOf(e).map(bare);
95
+ for (const other of entries) {
96
+ if (other === e) continue;
97
+ const held = locationsOf(other).map(bare).filter((l) => !own.includes(l) && insideLocation(e, l));
98
+ if (held.length) say(`location holds ${other.name}'s ${held.join(", ")} — list this system's own files`);
99
+ }
100
+ }
101
+
90
102
  function staleCode(e, say) {
91
- const loc = join(root, e.location ?? "");
92
- if (!e.location || !existsSync(loc)) return say(`location ${e.location} not found`);
93
- const near = statSync(loc).isDirectory() ? loc : dirname(loc);
94
- if (e.example && !existsSync(join(near, e.example)) && !existsSync(join(root, e.example))) say(`example ${e.example} not found`);
103
+ const locs = locationsOf(e);
104
+ const where = locs.join(", ");
105
+ const gone = locs.filter((l) => !existsSync(join(root, l)));
106
+ if (!locs.length || gone.length) return say(`location ${gone.join(", ") || e.location} not found`);
107
+ const near = locs.map((l) => (statSync(join(root, l)).isDirectory() ? join(root, l) : dirname(join(root, l))));
108
+ if (e.example && !near.some((n) => existsSync(join(n, e.example))) && !existsSync(join(root, e.example))) say(`example ${e.example} not found`);
95
109
  const syms = symbolsOf(e.extend_by);
96
110
  if (!syms.length) say("extend_by names no symbol — write names like `AimStrategy`");
97
111
  const code = files.filter((f) => insideLocation(e, f) && !f.endsWith(REGISTRY_SUFFIX))
@@ -99,9 +113,9 @@ function staleCode(e, say) {
99
113
  for (const s of syms) {
100
114
  // A plain word like `first` matches `var first = enemies[0]`; `Aim.first` only matches real use.
101
115
  if (/^[a-z][a-z0-9]*$/.test(s)) say(`\`${s}\` is too common a word to prove use — name its owner, like \`Owner.${s}\``);
102
- else if (!s.split(".").every((part) => uses(code, part))) say(`${s} from extend_by is not found in ${e.location}`);
116
+ else if (!s.split(".").every((part) => uses(code, part))) say(`${s} from extend_by is not found in ${where}`);
103
117
  }
104
- for (const c of e.does) if (!calls(code, c)) say(`does ${c} is not found in ${e.location}`);
118
+ for (const c of e.does) if (!calls(code, c)) say(`does ${c} is not found in ${where}`);
105
119
  }
106
120
 
107
121
  function fail(lines) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.64.0",
3
+ "version": "2.65.0",
4
4
  "description": "Guardrails for Claude Code — best-practice skills, planning commands, and delivery gates.",
5
5
  "type": "module",
6
6
  "repository": {