@csark0812/skeleton 1.1.5 → 1.5.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.
package/dist/cli.js CHANGED
@@ -15703,6 +15703,106 @@ var $stringify = publicApi.stringify;
15703
15703
  var $visit = visit.visit;
15704
15704
  var $visitAsync = visit.visitAsync;
15705
15705
 
15706
+ // src/audit/core/shared.ts
15707
+ var REGISTRY_REL_PATH = ".skeleton/registry.md";
15708
+ var REGISTRY_DIR_REL = ".skeleton";
15709
+ var EXTERNAL_LINK_RE = /^(https?:|mailto:|#)/;
15710
+ var SOURCE_OF_TRUTH_BANNER_RE = /\*\*Source of truth for\*\*/;
15711
+ var SOURCE_OF_TRUTH_BANNER_LINE_RE = /^\s*\*\*Source of truth for\*\*/m;
15712
+ var DOC_META_RE = /<!--\s*doc-meta:\s*owner=[^|]+\|\s*last-reviewed=\d{4}-\d{2}-\d{2}\s*-->/;
15713
+ var DOC_META_LAST_REVIEWED_RE = /last-reviewed=(\d{4}-\d{2}-\d{2})/;
15714
+ function docMetaLastReviewed(content) {
15715
+ const meta = DOC_META_RE.exec(content);
15716
+ if (!meta?.[0])
15717
+ return null;
15718
+ const match = DOC_META_LAST_REVIEWED_RE.exec(meta[0]);
15719
+ return match?.[1] ?? null;
15720
+ }
15721
+ function replaceDocMetaLastReviewed(content, date) {
15722
+ const meta = DOC_META_RE.exec(content);
15723
+ if (!meta?.[0] || meta.index === undefined)
15724
+ return null;
15725
+ const updatedComment = meta[0].replace(DOC_META_LAST_REVIEWED_RE, `last-reviewed=${date}`);
15726
+ if (updatedComment === meta[0])
15727
+ return null;
15728
+ return content.slice(0, meta.index) + updatedComment + content.slice(meta.index + meta[0].length);
15729
+ }
15730
+ var SKILL_LINK_IN_TARGET_RE = /(?:\.claude\/skills\/|\.agents\/skills\/|(?:\.\.\/)+)([a-z0-9-]+)\/SKILL\.md/;
15731
+ var SKILL_LINK_RE = /(?:\.claude\/skills\/|\.agents\/skills\/|\.\.\/|\.\/)?([a-z0-9-]+)\/SKILL\.md/g;
15732
+ function normalizeRelPath(p) {
15733
+ let out = p.replace(/\\/g, "/");
15734
+ if (out.startsWith("/"))
15735
+ return out;
15736
+ while (out.startsWith("./")) {
15737
+ out = out.slice(2);
15738
+ }
15739
+ return out;
15740
+ }
15741
+ function isExternalLink(target) {
15742
+ return EXTERNAL_LINK_RE.test(target);
15743
+ }
15744
+ function isPlaceholderLink(target) {
15745
+ return !target.includes("/") && !target.includes(".") && !target.startsWith("#");
15746
+ }
15747
+ function escapeRegexLiteral(s) {
15748
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15749
+ }
15750
+ function globToRegex(glob) {
15751
+ let pattern = glob.replace(/\\/g, "/");
15752
+ pattern = pattern.replace(/\{([^}]+)\}/g, (_, inner) => {
15753
+ const parts = inner.split(",").map((p) => escapeRegexLiteral(p.trim()));
15754
+ return `(${parts.join("|")})`;
15755
+ });
15756
+ pattern = pattern.replace(/\*\*/g, "§§").replace(/\*/g, "[^/]*").replace(/§§/g, ".*").replace(/\?/g, "[^/]");
15757
+ return new RegExp(`^${pattern}$`);
15758
+ }
15759
+ function matchesGlobScope(relPath, scope) {
15760
+ if (!scope)
15761
+ return true;
15762
+ return globToRegex(scope).test(normalizeRelPath(relPath));
15763
+ }
15764
+ function extractScanRootsFromInclude(include) {
15765
+ const roots = new Set;
15766
+ for (const pattern of include) {
15767
+ const normalized = normalizeRelPath(pattern);
15768
+ const globIdx = normalized.search(/[*?[{]/);
15769
+ if (globIdx === -1) {
15770
+ if (/\.[a-z0-9]+$/i.test(normalized))
15771
+ continue;
15772
+ roots.add(normalized.replace(/\/$/, ""));
15773
+ continue;
15774
+ }
15775
+ const root = normalized.slice(0, globIdx).replace(/\/$/, "");
15776
+ if (root)
15777
+ roots.add(root);
15778
+ }
15779
+ return [...roots];
15780
+ }
15781
+
15782
+ // src/audit/core/draft.ts
15783
+ var DRAFT_FILENAME_RE = /(^|\/)_draft-[^/]+\.md$/i;
15784
+ function normalizeDraftPrefix(prefix) {
15785
+ const normalized = normalizeRelPath(prefix);
15786
+ const withSlash = normalized.endsWith("/") ? normalized : `${normalized}/`;
15787
+ if (withSlash === "/" || withSlash === "./") {
15788
+ throw new Error(`Invalid draftPathPrefixes entry ${JSON.stringify(prefix)}: must be a repo-relative directory (e.g. drafts/), not ${JSON.stringify(prefix)}`);
15789
+ }
15790
+ return withSlash;
15791
+ }
15792
+ function validateDraftPathPrefixes(prefixes) {
15793
+ if (!prefixes)
15794
+ return;
15795
+ for (const prefix of prefixes) {
15796
+ normalizeDraftPrefix(prefix);
15797
+ }
15798
+ }
15799
+ function isDraftPlacementAllowed(relPath, draftPathPrefixes) {
15800
+ const normalized = normalizeRelPath(relPath);
15801
+ if (DRAFT_FILENAME_RE.test(normalized))
15802
+ return true;
15803
+ return draftPathPrefixes.some((prefix) => normalized.startsWith(normalizeDraftPrefix(prefix)));
15804
+ }
15805
+
15706
15806
  // src/audit/config/load.ts
15707
15807
  var SCHEMA_CANDIDATES = [
15708
15808
  join(dirname(fileURLToPath(import.meta.url)), "../../../schemas/config.schema.json"),
@@ -15743,7 +15843,9 @@ function validateConfig(raw) {
15743
15843
  const detail = validate.errors?.map((e) => `${e.instancePath || "/"} ${e.message}`).join("; ");
15744
15844
  throw new Error(`Invalid .skeleton/config.yaml: ${detail ?? "schema validation failed"}`);
15745
15845
  }
15746
- return raw;
15846
+ const config = raw;
15847
+ validateDraftPathPrefixes(config.draftPathPrefixes);
15848
+ return config;
15747
15849
  }
15748
15850
  function loadConfig(root) {
15749
15851
  const configPath = join(root, ".skeleton", "config.yaml");
@@ -15763,9 +15865,10 @@ function nonPublicSkills(config) {
15763
15865
  return config.scan.nonPublicSkills ?? [];
15764
15866
  }
15765
15867
 
15766
- // src/audit/core/collect.ts
15767
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
15768
- import { join as join3, relative as relative3 } from "node:path";
15868
+ // src/plugins/load.ts
15869
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
15870
+ import { relative as relative2 } from "node:path";
15871
+ import { pathToFileURL } from "node:url";
15769
15872
 
15770
15873
  // node_modules/tinyglobby/dist/index.mjs
15771
15874
  import { readdir, readdirSync, realpath, realpathSync, stat, statSync } from "fs";
@@ -16593,63 +16696,300 @@ function globSync(globInput, options) {
16593
16696
  return crawler ? formatPaths(crawler.sync(), relative2) : [];
16594
16697
  }
16595
16698
 
16596
- // src/audit/core/shared.ts
16597
- var REGISTRY_REL_PATH = ".skeleton/registry.md";
16598
- var REGISTRY_DIR_REL = ".skeleton";
16599
- var EXTERNAL_LINK_RE = /^(https?:|mailto:|#)/;
16600
- var SOURCE_OF_TRUTH_BANNER_RE = /\*\*Source of truth for\*\*/;
16601
- var SOURCE_OF_TRUTH_BANNER_LINE_RE = /^\s*\*\*Source of truth for\*\*/m;
16602
- var DOC_META_RE = /<!--\s*doc-meta:\s*owner=[^|]+\|\s*last-reviewed=\d{4}-\d{2}-\d{2}\s*-->/;
16603
- var DOC_META_LAST_REVIEWED_RE = /last-reviewed=(\d{4}-\d{2}-\d{2})/;
16604
- var SKILL_LINK_IN_TARGET_RE = /(?:\.claude\/skills\/|\.agents\/skills\/|(?:\.\.\/)+)([a-z0-9-]+)\/SKILL\.md/;
16605
- var SKILL_LINK_RE = /(?:\.claude\/skills\/|\.agents\/skills\/|\.\.\/|\.\/)?([a-z0-9-]+)\/SKILL\.md/g;
16606
- function normalizeRelPath(p) {
16607
- return p.replace(/\\/g, "/");
16608
- }
16609
- function isExternalLink(target) {
16610
- return EXTERNAL_LINK_RE.test(target);
16699
+ // src/audit/policies/load.ts
16700
+ var import_ajv2 = __toESM(require_ajv(), 1);
16701
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
16702
+ import { basename as basename2, dirname as dirname3, extname, join as join2 } from "node:path";
16703
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
16704
+ var SCHEMA_CANDIDATES2 = [
16705
+ join2(dirname3(fileURLToPath3(import.meta.url)), "../../../schemas/policy-file.schema.json"),
16706
+ join2(dirname3(fileURLToPath3(import.meta.url)), "../schemas/policy-file.schema.json"),
16707
+ join2(dirname3(fileURLToPath3(import.meta.url)), "../../schemas/policy-file.schema.json")
16708
+ ];
16709
+ function resolvePolicySchemaPath() {
16710
+ for (const candidate of SCHEMA_CANDIDATES2) {
16711
+ if (existsSync2(candidate))
16712
+ return candidate;
16713
+ }
16714
+ throw new Error("Missing schemas/policy-file.schema.json in package");
16611
16715
  }
16612
- function isPlaceholderLink(target) {
16613
- return !target.includes("/") && !target.includes(".") && !target.startsWith("#");
16716
+ function validatePolicyYaml(raw, label) {
16717
+ const schema = JSON.parse(readFileSync2(resolvePolicySchemaPath(), "utf8"));
16718
+ const ajv = new import_ajv2.default({ allErrors: true, strict: false });
16719
+ const validate = ajv.compile(schema);
16720
+ if (!validate(raw)) {
16721
+ const detail = validate.errors?.map((e) => `${e.instancePath || "/"} ${e.message}`).join("; ");
16722
+ throw new Error(`Invalid policy ${label}: ${detail ?? "schema validation failed"}`);
16723
+ }
16724
+ return raw;
16614
16725
  }
16615
- function escapeRegexLiteral(s) {
16616
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16726
+ function parsePolicyYaml(content, fileStem) {
16727
+ const parsed = $parse(content);
16728
+ if (Array.isArray(parsed)) {
16729
+ throw new Error(`Policy ${fileStem}.yaml must use Policy File shape (name + entries) — see schemas/policy-file.schema.json`);
16730
+ }
16731
+ if (!parsed || typeof parsed !== "object") {
16732
+ throw new Error(`Policy ${fileStem}.yaml missing required 'entries' array`);
16733
+ }
16734
+ return validatePolicyYaml(parsed, `${fileStem}.yaml`);
16617
16735
  }
16618
- function globToRegex(glob) {
16619
- let pattern = glob.replace(/\\/g, "/");
16620
- pattern = pattern.replace(/\{([^}]+)\}/g, (_, inner) => {
16621
- const parts = inner.split(",").map((p) => escapeRegexLiteral(p.trim()));
16622
- return `(${parts.join("|")})`;
16736
+ function compilePolicy(name, raw) {
16737
+ const caseInsensitive = name !== "skill-hub-duplication";
16738
+ const entries = raw.map((entry) => {
16739
+ const mode = entry.mode ?? "pattern";
16740
+ if (mode === "fingerprint") {
16741
+ return { ...entry, mode, regex: null };
16742
+ }
16743
+ if (!entry.pattern) {
16744
+ throw new Error(`Policy ${name} entry ${entry.id} requires pattern when mode is pattern`);
16745
+ }
16746
+ let regex;
16747
+ try {
16748
+ const flags = entry.pattern.startsWith("^") || !caseInsensitive ? "" : "i";
16749
+ regex = new RegExp(entry.pattern, flags);
16750
+ } catch (err) {
16751
+ throw new Error(`Invalid regex in policy ${name} entry ${entry.id}: ${entry.pattern} — ${err}`);
16752
+ }
16753
+ return { ...entry, mode, regex };
16623
16754
  });
16624
- pattern = pattern.replace(/\*\*/g, "§§").replace(/\*/g, "[^/]*").replace(/§§/g, ".*").replace(/\?/g, "[^/]");
16625
- return new RegExp(`^${pattern}$`);
16755
+ return { name, entries };
16626
16756
  }
16627
- function matchesGlobScope(relPath, scope) {
16628
- if (!scope)
16629
- return true;
16630
- return globToRegex(scope).test(normalizeRelPath(relPath));
16757
+ function loadPolicyFile(absPath, content) {
16758
+ const stem = basename2(absPath, extname(absPath));
16759
+ const { name, entries } = parsePolicyYaml(content, stem);
16760
+ return compilePolicy(name || stem, entries);
16631
16761
  }
16632
- function extractScanRootsFromInclude(include) {
16633
- const roots = new Set;
16634
- for (const pattern of include) {
16635
- const normalized = normalizeRelPath(pattern);
16636
- const globIdx = normalized.search(/[*?[{]/);
16637
- if (globIdx === -1) {
16638
- if (/\.[a-z0-9]+$/i.test(normalized))
16639
- continue;
16640
- roots.add(normalized.replace(/\/$/, ""));
16762
+ function policiesForFile(policies, relPath) {
16763
+ const matched = [];
16764
+ for (const policy of policies) {
16765
+ for (const entry of policy.entries) {
16766
+ if (matchesGlobScope(relPath, entry.scope)) {
16767
+ matched.push({ ...entry, policyName: policy.name });
16768
+ }
16769
+ }
16770
+ }
16771
+ return matched;
16772
+ }
16773
+
16774
+ // src/plugins/paths.ts
16775
+ import { existsSync as existsSync3, realpathSync as realpathSync2 } from "node:fs";
16776
+ import { dirname as dirname4, join as join3, resolve as resolve3, sep as sep2 } from "node:path";
16777
+ function skeletonDir(root) {
16778
+ return join3(root, ".skeleton");
16779
+ }
16780
+ function mjsPathForTs(tsPath) {
16781
+ if (tsPath.endsWith(".mjs"))
16782
+ return tsPath;
16783
+ if (tsPath.endsWith(".ts"))
16784
+ return `${tsPath.slice(0, -3)}.mjs`;
16785
+ return `${tsPath}.mjs`;
16786
+ }
16787
+ function underBase(pathAbs, baseAbs) {
16788
+ return pathAbs !== baseAbs && pathAbs.startsWith(baseAbs + sep2);
16789
+ }
16790
+ function assertUnderBase(abs, base, label) {
16791
+ const baseAbs = resolve3(base);
16792
+ const pathAbs = resolve3(abs);
16793
+ if (!underBase(pathAbs, baseAbs)) {
16794
+ throw new Error(`${label} must stay under .skeleton/: ${abs}`);
16795
+ }
16796
+ }
16797
+ function assertRealUnderBase(abs, baseReal, label) {
16798
+ const pathAbs = resolve3(abs);
16799
+ let cursor = pathAbs;
16800
+ while (true) {
16801
+ if (existsSync3(cursor)) {
16802
+ const real = realpathSync2(cursor);
16803
+ if (real !== baseReal && !underBase(real, baseReal)) {
16804
+ throw new Error(`${label} must stay under .skeleton/: ${abs}`);
16805
+ }
16806
+ return;
16807
+ }
16808
+ const parent = dirname4(cursor);
16809
+ if (parent === cursor) {
16810
+ throw new Error(`${label} must stay under .skeleton/: ${abs}`);
16811
+ }
16812
+ cursor = parent;
16813
+ }
16814
+ }
16815
+ function skeletonRealPath(root) {
16816
+ const base = resolve3(skeletonDir(root));
16817
+ return existsSync3(base) ? realpathSync2(base) : base;
16818
+ }
16819
+ function resolvePluginTsPath(root, entry) {
16820
+ if (entry.startsWith("/") || /^[A-Za-z]:[\\/]/.test(entry)) {
16821
+ throw new Error(`Plugin path must be relative to .skeleton/: ${entry}`);
16822
+ }
16823
+ const cleaned = entry.replace(/^\.skeleton\//, "").replace(/^\.\//, "");
16824
+ if (!cleaned || cleaned === ".") {
16825
+ throw new Error(`Plugin path must stay under .skeleton/: ${entry}`);
16826
+ }
16827
+ if (cleaned.split(/[/\\]/).includes("..")) {
16828
+ throw new Error(`Plugin path must stay under .skeleton/: ${entry}`);
16829
+ }
16830
+ const base = resolve3(skeletonDir(root));
16831
+ const abs = resolve3(base, cleaned);
16832
+ return assertPluginTsUnderSkeleton(root, abs, entry);
16833
+ }
16834
+ function resolveAbsolutePluginTsPath(root, absEntry) {
16835
+ if (!(absEntry.startsWith("/") || /^[A-Za-z]:[\\/]/.test(absEntry))) {
16836
+ throw new Error(`Expected absolute plugin path: ${absEntry}`);
16837
+ }
16838
+ const abs = resolve3(absEntry);
16839
+ return assertPluginTsUnderSkeleton(root, abs, absEntry);
16840
+ }
16841
+ function assertPluginTsUnderSkeleton(root, abs, label) {
16842
+ const base = resolve3(skeletonDir(root));
16843
+ assertUnderBase(abs, base, "Plugin path");
16844
+ if (!abs.endsWith(".ts")) {
16845
+ throw new Error(`Plugin path must be a .ts file under .skeleton/: ${label}`);
16846
+ }
16847
+ const mjsAbs = mjsPathForTs(abs);
16848
+ assertUnderBase(mjsAbs, base, "Plugin output");
16849
+ const baseReal = skeletonRealPath(root);
16850
+ assertRealUnderBase(abs, baseReal, "Plugin path");
16851
+ assertRealUnderBase(mjsAbs, baseReal, "Plugin output");
16852
+ return abs;
16853
+ }
16854
+ function assertUnderSkeleton(root, absPath) {
16855
+ const base = resolve3(skeletonDir(root));
16856
+ assertUnderBase(absPath, base, "Policy file");
16857
+ assertRealUnderBase(absPath, skeletonRealPath(root), "Policy file");
16858
+ }
16859
+
16860
+ // src/plugins/load.ts
16861
+ function normalizeExport(mod) {
16862
+ if (!mod || typeof mod !== "object") {
16863
+ throw new Error("Plugin module must export { rules: AuditRule[]; policies?: string[] }");
16864
+ }
16865
+ const record = mod;
16866
+ const namedRules = Array.isArray(record.rules) ? record.rules : undefined;
16867
+ const namedPoliciesRaw = "policies" in record ? record.policies : undefined;
16868
+ const def = "default" in record && record.default && typeof record.default === "object" ? record.default : null;
16869
+ const defaultRules = def && Array.isArray(def.rules) ? def.rules : undefined;
16870
+ const defaultPoliciesRaw = def && "policies" in def ? def.policies : undefined;
16871
+ let rules;
16872
+ if (defaultRules !== undefined && namedRules !== undefined) {
16873
+ if (defaultRules.length !== namedRules.length || defaultRules.some((rule, i) => rule.id !== namedRules[i]?.id || rule.run !== namedRules[i]?.run)) {
16874
+ throw new Error("Plugin exports disagree on rules: default and named `rules` must match when both are set");
16875
+ }
16876
+ rules = defaultRules;
16877
+ } else {
16878
+ rules = defaultRules ?? namedRules;
16879
+ }
16880
+ let policies;
16881
+ if (defaultPoliciesRaw !== undefined && namedPoliciesRaw !== undefined) {
16882
+ if (!Array.isArray(defaultPoliciesRaw) || !Array.isArray(namedPoliciesRaw) || defaultPoliciesRaw.length !== namedPoliciesRaw.length || defaultPoliciesRaw.some((p, i) => p !== namedPoliciesRaw[i])) {
16883
+ throw new Error("Plugin exports disagree on policies: default and named `policies` must match when both are set");
16884
+ }
16885
+ policies = defaultPoliciesRaw;
16886
+ } else {
16887
+ policies = defaultPoliciesRaw ?? namedPoliciesRaw;
16888
+ }
16889
+ if (!Array.isArray(rules)) {
16890
+ throw new Error("Plugin module must export { rules: AuditRule[]; policies?: string[] }");
16891
+ }
16892
+ for (const rule of rules) {
16893
+ if (!rule || typeof rule.id !== "string" || typeof rule.run !== "function") {
16894
+ throw new Error("Plugin rules must each have string id and run()");
16895
+ }
16896
+ }
16897
+ if (policies !== undefined) {
16898
+ if (!Array.isArray(policies) || policies.some((p) => typeof p !== "string")) {
16899
+ throw new Error("Plugin policies must be string[] (globs relative to .skeleton/) — got non-array");
16900
+ }
16901
+ }
16902
+ return {
16903
+ rules,
16904
+ policies
16905
+ };
16906
+ }
16907
+ function expandPolicyGlobs(root, globs) {
16908
+ const base = skeletonDir(root);
16909
+ const files = new Set;
16910
+ for (const pattern of globs) {
16911
+ if (pattern.split(/[/\\]/).includes("..")) {
16912
+ throw new Error(`Policy glob must stay under .skeleton/: ${pattern}`);
16913
+ }
16914
+ const matches = globSync(pattern, {
16915
+ cwd: base,
16916
+ absolute: true,
16917
+ onlyFiles: true
16918
+ });
16919
+ for (const match of matches) {
16920
+ assertUnderSkeleton(root, match);
16921
+ if (match.endsWith(".yaml") || match.endsWith(".yml")) {
16922
+ files.add(match);
16923
+ }
16924
+ }
16925
+ }
16926
+ return [...files].sort();
16927
+ }
16928
+ function loadPoliciesFromGlobs(root, globs) {
16929
+ const files = expandPolicyGlobs(root, globs);
16930
+ const policies = [];
16931
+ for (const abs of files) {
16932
+ assertUnderSkeleton(root, abs);
16933
+ policies.push(loadPolicyFile(abs, readFileSync3(abs, "utf8")));
16934
+ }
16935
+ if (globs.length > 0 && policies.length === 0) {
16936
+ throw new Error(`Plugin policies matched no YAML under .skeleton/: ${globs.join(", ")}. Fix the glob or add policy files.`);
16937
+ }
16938
+ return policies;
16939
+ }
16940
+ async function collectWiredPolicyRelPaths(root, config) {
16941
+ const entries = config.plugins ?? [];
16942
+ const wired = new Set;
16943
+ if (entries.length === 0)
16944
+ return wired;
16945
+ for (const entry of entries) {
16946
+ const tsAbs = resolvePluginTsPath(root, entry);
16947
+ const mjsAbs = mjsPathForTs(tsAbs);
16948
+ if (!existsSync4(mjsAbs)) {
16949
+ const rel = relative2(skeletonDir(root), tsAbs) || entry;
16950
+ throw new Error(`Plugin not built: ${rel} (missing ${relative2(root, mjsAbs) || mjsAbs}). Run: skeleton build-plugin`);
16951
+ }
16952
+ const mod = await import(pathToFileURL(mjsAbs).href);
16953
+ const normalized = normalizeExport(mod);
16954
+ if (!normalized.policies?.length)
16641
16955
  continue;
16956
+ for (const abs of expandPolicyGlobs(root, normalized.policies)) {
16957
+ wired.add(normalizeRelPath(relative2(root, abs)));
16642
16958
  }
16643
- const root = normalized.slice(0, globIdx).replace(/\/$/, "");
16644
- if (root)
16645
- roots.add(root);
16646
16959
  }
16647
- return [...roots];
16960
+ return wired;
16961
+ }
16962
+ async function loadPlugins(root, config) {
16963
+ const entries = config.plugins ?? [];
16964
+ if (entries.length === 0) {
16965
+ return { rules: [], policies: [] };
16966
+ }
16967
+ const rules = [];
16968
+ const policies = [];
16969
+ for (const entry of entries) {
16970
+ const tsAbs = resolvePluginTsPath(root, entry);
16971
+ const mjsAbs = mjsPathForTs(tsAbs);
16972
+ if (!existsSync4(mjsAbs)) {
16973
+ const rel = relative2(skeletonDir(root), tsAbs) || entry;
16974
+ throw new Error(`Plugin not built: ${rel} (missing ${relative2(root, mjsAbs) || mjsAbs}). Run: skeleton build-plugin`);
16975
+ }
16976
+ const mod = await import(pathToFileURL(mjsAbs).href);
16977
+ const normalized = normalizeExport(mod);
16978
+ rules.push(...normalized.rules);
16979
+ if (normalized.policies?.length) {
16980
+ policies.push(...loadPoliciesFromGlobs(root, normalized.policies));
16981
+ }
16982
+ }
16983
+ return { rules, policies };
16648
16984
  }
16649
16985
 
16986
+ // src/audit/core/collect.ts
16987
+ import { existsSync as existsSync6, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
16988
+ import { join as join5, relative as relative4 } from "node:path";
16989
+
16650
16990
  // src/audit/core/skill-roots.ts
16651
- import { existsSync as existsSync2, readdirSync as readdirSync2, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
16652
- import { join as join2, relative as relative2 } from "node:path";
16991
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readlinkSync, realpathSync as realpathSync3 } from "node:fs";
16992
+ import { join as join4, relative as relative3 } from "node:path";
16653
16993
  var NESTED_SKILL_ROOTS = [".claude/skills", ".agents/skills"];
16654
16994
  var FLAT_SKILL_DENYLIST = new Set([
16655
16995
  ".git",
@@ -16672,16 +17012,16 @@ var FLAT_SKILL_DENYLIST = new Set([
16672
17012
  var NESTED_EXCLUDED_DIRS = new Set(["references", "_shared"]);
16673
17013
  function safeRealpath(path) {
16674
17014
  try {
16675
- return realpathSync2(path);
17015
+ return realpathSync3(path);
16676
17016
  } catch {
16677
17017
  return null;
16678
17018
  }
16679
17019
  }
16680
17020
  function listNestedSlugs(root, relRoot) {
16681
- const absRoot = join2(root, relRoot);
16682
- if (!existsSync2(absRoot))
17021
+ const absRoot = join4(root, relRoot);
17022
+ if (!existsSync5(absRoot))
16683
17023
  return [];
16684
- return readdirSync2(absRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !NESTED_EXCLUDED_DIRS.has(entry.name)).filter((entry) => existsSync2(join2(absRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
17024
+ return readdirSync2(absRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !NESTED_EXCLUDED_DIRS.has(entry.name)).filter((entry) => existsSync5(join4(absRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
16685
17025
  }
16686
17026
  function listFlatSlugs(root) {
16687
17027
  const slugs = [];
@@ -16690,7 +17030,7 @@ function listFlatSlugs(root) {
16690
17030
  continue;
16691
17031
  if (FLAT_SKILL_DENYLIST.has(entry.name))
16692
17032
  continue;
16693
- if (existsSync2(join2(root, entry.name, "SKILL.md"))) {
17033
+ if (existsSync5(join4(root, entry.name, "SKILL.md"))) {
16694
17034
  slugs.push(entry.name);
16695
17035
  }
16696
17036
  }
@@ -16700,8 +17040,8 @@ function detectSkillRoots(root) {
16700
17040
  const roots = [];
16701
17041
  let claudeReal = null;
16702
17042
  for (const relRoot of NESTED_SKILL_ROOTS) {
16703
- const abs = join2(root, relRoot);
16704
- if (!existsSync2(abs))
17043
+ const abs = join4(root, relRoot);
17044
+ if (!existsSync5(abs))
16705
17045
  continue;
16706
17046
  if (relRoot === ".claude/skills") {
16707
17047
  claudeReal = safeRealpath(abs);
@@ -16715,11 +17055,11 @@ function detectSkillRoots(root) {
16715
17055
  continue;
16716
17056
  try {
16717
17057
  const link = readlinkSync(abs);
16718
- if (link && claudeReal && safeRealpath(join2(root, link)) === claudeReal)
17058
+ if (link && claudeReal && safeRealpath(join4(root, link)) === claudeReal)
16719
17059
  continue;
16720
17060
  } catch {}
16721
17061
  }
16722
- if (listNestedSlugs(root, relRoot).length > 0 || existsSync2(abs)) {
17062
+ if (listNestedSlugs(root, relRoot).length > 0 || existsSync5(abs)) {
16723
17063
  roots.push({ kind: "nested", relPath: relRoot });
16724
17064
  }
16725
17065
  }
@@ -16746,9 +17086,9 @@ function buildSkillIndex(root) {
16746
17086
  }
16747
17087
  function resolveSkillPath(index, root, slug) {
16748
17088
  for (const skillRoot of index.roots) {
16749
- const candidate = skillRoot.kind === "nested" ? join2(root, skillRoot.relPath, slug, "SKILL.md") : join2(root, slug, "SKILL.md");
16750
- if (existsSync2(candidate)) {
16751
- return normalizeRelPath(relative2(root, candidate));
17089
+ const candidate = skillRoot.kind === "nested" ? join4(root, skillRoot.relPath, slug, "SKILL.md") : join4(root, slug, "SKILL.md");
17090
+ if (existsSync5(candidate)) {
17091
+ return normalizeRelPath(relative3(root, candidate));
16752
17092
  }
16753
17093
  }
16754
17094
  return null;
@@ -16782,12 +17122,33 @@ function skillCollectAugments(index) {
16782
17122
  }
16783
17123
  return patterns;
16784
17124
  }
17125
+ function listSkillMarkdownPaths(root, index) {
17126
+ const paths = new Set;
17127
+ for (const skillRoot of index.roots) {
17128
+ const slugs = skillRoot.kind === "nested" ? listNestedSlugs(root, skillRoot.relPath) : listFlatSlugs(root);
17129
+ for (const slug of slugs) {
17130
+ const absDir = skillRoot.kind === "nested" ? join4(root, skillRoot.relPath, slug) : join4(root, slug);
17131
+ if (!existsSync5(absDir))
17132
+ continue;
17133
+ for (const abs of globSync("**/*.{md,mdc}", {
17134
+ cwd: absDir,
17135
+ absolute: true,
17136
+ onlyFiles: true,
17137
+ dot: true
17138
+ })) {
17139
+ paths.add(normalizeRelPath(relative3(root, abs)));
17140
+ }
17141
+ }
17142
+ }
17143
+ return [...paths].sort();
17144
+ }
16785
17145
  function listSkillSlugs(index) {
16786
17146
  return index.slugs;
16787
17147
  }
16788
17148
 
16789
17149
  // src/audit/core/collect.ts
16790
17150
  var MARKDOWN_GLOBS = ["**/*.md", "**/*.mdc"];
17151
+ var BUILTIN_INCLUDE_PATTERNS = [".skeleton/customize/**"];
16791
17152
  function isMarkdownFile(absPath) {
16792
17153
  return absPath.endsWith(".md") || absPath.endsWith(".mdc");
16793
17154
  }
@@ -16805,7 +17166,7 @@ function expandPatterns(root, patterns, exclude) {
16805
17166
  })) {
16806
17167
  if (!isMarkdownFile(abs))
16807
17168
  continue;
16808
- const rel = normalizeRelPath(relative3(root, abs));
17169
+ const rel = normalizeRelPath(relative4(root, abs));
16809
17170
  if (shouldExclude(rel, exclude))
16810
17171
  continue;
16811
17172
  files.add(abs);
@@ -16815,7 +17176,7 @@ function expandPatterns(root, patterns, exclude) {
16815
17176
  }
16816
17177
  function collectScanFiles(config, root, skillIndex) {
16817
17178
  const exclude = mergedExcludes(config);
16818
- const includePatterns = [...config.scan.include];
17179
+ const includePatterns = [...BUILTIN_INCLUDE_PATTERNS, ...config.scan.include];
16819
17180
  if (skillIndex) {
16820
17181
  includePatterns.push(...skillCollectAugments(skillIndex));
16821
17182
  }
@@ -16833,7 +17194,7 @@ function collectBannedFiles(config, root) {
16833
17194
  onlyFiles: true,
16834
17195
  dot: false
16835
17196
  })) {
16836
- const rel = normalizeRelPath(relative3(root, abs));
17197
+ const rel = normalizeRelPath(relative4(root, abs));
16837
17198
  if (shouldExclude(rel, exclude))
16838
17199
  continue;
16839
17200
  files.add(abs);
@@ -16850,7 +17211,7 @@ function collectCoverageCandidateFiles(root, exclude) {
16850
17211
  onlyFiles: true,
16851
17212
  dot: false
16852
17213
  })) {
16853
- const rel = normalizeRelPath(relative3(root, abs));
17214
+ const rel = normalizeRelPath(relative4(root, abs));
16854
17215
  if (shouldExclude(rel, exclude))
16855
17216
  continue;
16856
17217
  files.add(rel);
@@ -16861,25 +17222,25 @@ function collectCoverageCandidateFiles(root, exclude) {
16861
17222
  function collectDocMetaPaths(config, root, registryPaths, skillIndex) {
16862
17223
  const paths = [];
16863
17224
  for (const abs of expandPatterns(root, ["docs/*/README.md"], mergedExcludes(config))) {
16864
- paths.push(normalizeRelPath(relative3(root, abs)));
17225
+ paths.push(normalizeRelPath(relative4(root, abs)));
16865
17226
  }
16866
17227
  const extras = ["docs/README.md", ".skeleton/registry.md"];
16867
17228
  for (const file of extras) {
16868
- const abs = join3(root, file);
16869
- if (existsSync3(abs))
17229
+ const abs = join5(root, file);
17230
+ if (existsSync6(abs))
16870
17231
  paths.push(normalizeRelPath(file));
16871
17232
  }
16872
17233
  for (const rel of registryPaths) {
16873
17234
  if (!rel.endsWith(".md") && !rel.endsWith(".mdc"))
16874
17235
  continue;
16875
- const abs = join3(root, rel);
16876
- if (existsSync3(abs))
17236
+ const abs = join5(root, rel);
17237
+ if (existsSync6(abs))
16877
17238
  paths.push(normalizeRelPath(rel));
16878
17239
  }
16879
17240
  for (const abs of collectScanFiles(config, root, skillIndex)) {
16880
- const content = readFileSync2(abs, "utf8");
17241
+ const content = readFileSync4(abs, "utf8");
16881
17242
  if (/<!--\s*doc-meta:/.test(content)) {
16882
- paths.push(normalizeRelPath(relative3(root, abs)));
17243
+ paths.push(normalizeRelPath(relative4(root, abs)));
16883
17244
  }
16884
17245
  }
16885
17246
  return [...new Set(paths)];
@@ -16887,7 +17248,7 @@ function collectDocMetaPaths(config, root, registryPaths, skillIndex) {
16887
17248
  function validateScanRoots(config, root) {
16888
17249
  const missing = [];
16889
17250
  for (const tree of extractScanRootsFromInclude(config.scan.include)) {
16890
- if (!existsSync3(join3(root, tree)))
17251
+ if (!existsSync6(join5(root, tree)))
16891
17252
  missing.push(tree);
16892
17253
  }
16893
17254
  return missing;
@@ -16901,28 +17262,56 @@ function filterDocMetaPaths(docMetaPaths, paths) {
16901
17262
  function filterToPaths(files, paths, root) {
16902
17263
  const normalizedPaths = paths.map((path) => normalizeRelPath(path));
16903
17264
  return files.filter((abs) => {
16904
- const rel = normalizeRelPath(relative3(root, abs));
17265
+ const rel = normalizeRelPath(relative4(root, abs));
16905
17266
  return normalizedPaths.some((path) => rel === path || rel.startsWith(`${path}/`));
16906
17267
  });
16907
17268
  }
17269
+ function includeExplicitMarkdownPaths(files, paths, root) {
17270
+ const out = new Set(files);
17271
+ for (const raw of paths) {
17272
+ const rel = normalizeRelPath(raw);
17273
+ const abs = join5(root, rel);
17274
+ if (!existsSync6(abs))
17275
+ continue;
17276
+ if (isMarkdownFile(rel)) {
17277
+ out.add(abs);
17278
+ continue;
17279
+ }
17280
+ try {
17281
+ if (!statSync2(abs).isDirectory())
17282
+ continue;
17283
+ } catch {
17284
+ continue;
17285
+ }
17286
+ for (const md of globSync("**/*.{md,mdc}", {
17287
+ cwd: abs,
17288
+ absolute: true,
17289
+ onlyFiles: true,
17290
+ dot: true
17291
+ })) {
17292
+ out.add(md);
17293
+ }
17294
+ }
17295
+ return [...out];
17296
+ }
16908
17297
  function readFileContent(absPath) {
16909
- return readFileSync2(absPath, "utf8");
17298
+ return readFileSync4(absPath, "utf8");
16910
17299
  }
16911
17300
  function relPath(absPath, root) {
16912
- return normalizeRelPath(relative3(root, absPath));
17301
+ return normalizeRelPath(relative4(root, absPath));
16913
17302
  }
16914
17303
 
16915
17304
  // src/audit/core/registry.ts
16916
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
16917
- import { join as join4, relative as relative4, resolve as resolve3 } from "node:path";
17305
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
17306
+ import { join as join6, relative as relative5, resolve as resolve4 } from "node:path";
16918
17307
  var REGISTRY_TABLE_ROW_RE = /^\|\s*[^|]+\|\s*\[[^\]]*\]\(([^)]+)\)\s*\|/;
16919
17308
  var REGISTRY_TABLE_HEADER_RE = /\|\s*Topic\s*\|\s*Canonical file\s*\|/i;
16920
17309
  function parseRegistry(root) {
16921
- const abs = join4(root, REGISTRY_REL_PATH);
16922
- if (!existsSync4(abs)) {
17310
+ const abs = join6(root, REGISTRY_REL_PATH);
17311
+ if (!existsSync7(abs)) {
16923
17312
  return { paths: [], hasTableHeader: false };
16924
17313
  }
16925
- const content = readFileSync3(abs, "utf8");
17314
+ const content = readFileSync5(abs, "utf8");
16926
17315
  const hasTableHeader = REGISTRY_TABLE_HEADER_RE.test(content);
16927
17316
  const paths = [];
16928
17317
  for (const line of content.split(`
@@ -16933,8 +17322,8 @@ function parseRegistry(root) {
16933
17322
  const linkTarget = match[1].trim();
16934
17323
  if (linkTarget.startsWith("#"))
16935
17324
  continue;
16936
- const resolved = resolve3(join4(root, REGISTRY_DIR_REL), linkTarget);
16937
- paths.push(normalizeRelPath(relative4(root, resolved)));
17325
+ const resolved = resolve4(join6(root, REGISTRY_DIR_REL), linkTarget);
17326
+ paths.push(normalizeRelPath(relative5(root, resolved)));
16938
17327
  }
16939
17328
  return { paths: [...new Set(paths)], hasTableHeader };
16940
17329
  }
@@ -16948,7 +17337,11 @@ function createContext(options = {}) {
16948
17337
  const config = loadConfig(root);
16949
17338
  const skillIndex = buildSkillIndex(root);
16950
17339
  let files = collectScanFiles(config, root, skillIndex);
17340
+ if (options.includeExcludedSkillTrees) {
17341
+ files = includeExplicitMarkdownPaths(files, listSkillMarkdownPaths(root, skillIndex), root);
17342
+ }
16951
17343
  if (options.paths && options.paths.length > 0) {
17344
+ files = includeExplicitMarkdownPaths(files, options.paths, root);
16952
17345
  files = filterToPaths(files, options.paths, root);
16953
17346
  }
16954
17347
  const registry = parseRegistry(root);
@@ -16961,479 +17354,111 @@ function createContext(options = {}) {
16961
17354
  registryPaths: registry.paths,
16962
17355
  registryHasTableHeader: registry.hasTableHeader,
16963
17356
  retiredSkills: new Set(retiredSkills(config)),
16964
- skillIndex
17357
+ skillIndex,
17358
+ policies: options.policies ?? []
16965
17359
  };
16966
17360
  }
16967
17361
 
16968
- // src/audit/core/report.ts
16969
- function issue(rule, file, message, opts) {
16970
- return {
16971
- rule,
16972
- file,
16973
- link: opts?.link,
16974
- message,
16975
- severity: opts?.severity ?? "error"
16976
- };
17362
+ // src/audit/core/fix.ts
17363
+ import { existsSync as existsSync10, realpathSync as realpathSync4, writeFileSync } from "node:fs";
17364
+ import { dirname as dirname6, resolve as resolve6, sep as sep3 } from "node:path";
17365
+
17366
+ // src/audit/fix/anchors.ts
17367
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
17368
+ import { dirname as dirname5, resolve as resolve5 } from "node:path";
17369
+
17370
+ // node_modules/github-slugger/regex.js
17371
+ var regex = /[\0-\x1F!-,\.\/:-@\[-\^`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482\u0530\u0557\u0558\u055A-\u055F\u0589-\u0590\u05BE\u05C0\u05C3\u05C6\u05C8-\u05CF\u05EB-\u05EE\u05F3-\u060F\u061B-\u061F\u066A-\u066D\u06D4\u06DD\u06DE\u06E9\u06FD\u06FE\u0700-\u070F\u074B\u074C\u07B2-\u07BF\u07F6-\u07F9\u07FB\u07FC\u07FE\u07FF\u082E-\u083F\u085C-\u085F\u086B-\u089F\u08B5\u08C8-\u08D2\u08E2\u0964\u0965\u0970\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09F2-\u09FB\u09FD\u09FF\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF0-\u0AF8\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B54\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B70\u0B72-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BF0-\u0BFF\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5B-\u0C5F\u0C64\u0C65\u0C70-\u0C7F\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0CFF\u0D0D\u0D11\u0D45\u0D49\u0D4F-\u0D53\u0D58-\u0D5E\u0D64\u0D65\u0D70-\u0D79\u0D80\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DE5\u0DF0\u0DF1\u0DF4-\u0E00\u0E3B-\u0E3F\u0E4F\u0E5A-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F01-\u0F17\u0F1A-\u0F1F\u0F2A-\u0F34\u0F36\u0F38\u0F3A-\u0F3D\u0F48\u0F6D-\u0F70\u0F85\u0F98\u0FBD-\u0FC5\u0FC7-\u0FFF\u104A-\u104F\u109E\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u1360-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16ED\u16F9-\u16FF\u170D\u1715-\u171F\u1735-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17D4-\u17D6\u17D8-\u17DB\u17DE\u17DF\u17EA-\u180A\u180E\u180F\u181A-\u181F\u1879-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191F\u192C-\u192F\u193C-\u1945\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DA-\u19FF\u1A1C-\u1A1F\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1AA6\u1AA8-\u1AAF\u1AC1-\u1AFF\u1B4C-\u1B4F\u1B5A-\u1B6A\u1B74-\u1B7F\u1BF4-\u1BFF\u1C38-\u1C3F\u1C4A-\u1C4C\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CCF\u1CD3\u1CFB-\u1CFF\u1DFA\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u203E\u2041-\u2053\u2055-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u20CF\u20F1-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u215F\u2189-\u24B5\u24EA-\u2BFF\u2C2F\u2C5F\u2CE5-\u2CEA\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E00-\u2E2E\u2E30-\u3004\u3008-\u3020\u3030\u3036\u3037\u303D-\u3040\u3097\u3098\u309B\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31C0-\u31EF\u3200-\u33FF\u4DC0-\u4DFF\u9FFD-\u9FFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA62C-\uA63F\uA673\uA67E\uA6F2-\uA716\uA720\uA721\uA789\uA78A\uA7C0\uA7C1\uA7CB-\uA7F4\uA828-\uA82B\uA82D-\uA83F\uA874-\uA87F\uA8C6-\uA8CF\uA8DA-\uA8DF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA954-\uA95F\uA97D-\uA97F\uA9C1-\uA9CE\uA9DA-\uA9DF\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A-\uAA5F\uAA77-\uAA79\uAAC3-\uAADA\uAADE\uAADF\uAAF0\uAAF1\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB6A-\uAB6F\uABEB\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFDFF\uFE10-\uFE1F\uFE30-\uFE32\uFE35-\uFE4C\uFE50-\uFE6F\uFE75\uFEFD-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3E\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDD3F\uDD75-\uDDFC\uDDFE-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEDF\uDEE1-\uDEFF\uDF20-\uDF2C\uDF4B-\uDF4F\uDF7B-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0\uDFD6-\uDFFF]|\uD801[\uDC9E\uDC9F\uDCAA-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE04\uDE07-\uDE0B\uDE14\uDE18\uDE36\uDE37\uDE3B-\uDE3E\uDE40-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE7-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD28-\uDD2F\uDD3A-\uDE7F\uDEAA\uDEAD-\uDEAF\uDEB2-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF51-\uDFAF\uDFC5-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC47-\uDC65\uDC70-\uDC7E\uDCBB-\uDCCF\uDCE9-\uDCEF\uDCFA-\uDCFF\uDD35\uDD40-\uDD43\uDD48-\uDD4F\uDD74\uDD75\uDD77-\uDD7F\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDFF\uDE12\uDE38-\uDE3D\uDE3F-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEEB-\uDEEF\uDEFA-\uDEFF\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A\uDF45\uDF46\uDF49\uDF4A\uDF4E\uDF4F\uDF51-\uDF56\uDF58-\uDF5C\uDF64\uDF65\uDF6D-\uDF6F\uDF75-\uDFFF]|\uD805[\uDC4B-\uDC4F\uDC5A-\uDC5D\uDC62-\uDC7F\uDCC6\uDCC8-\uDCCF\uDCDA-\uDD7F\uDDB6\uDDB7\uDDC1-\uDDD7\uDDDE-\uDDFF\uDE41-\uDE43\uDE45-\uDE4F\uDE5A-\uDE7F\uDEB9-\uDEBF\uDECA-\uDEFF\uDF1B\uDF1C\uDF2C-\uDF2F\uDF3A-\uDFFF]|\uD806[\uDC3B-\uDC9F\uDCEA-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD36\uDD39\uDD3A\uDD44-\uDD4F\uDD5A-\uDD9F\uDDA8\uDDA9\uDDD8\uDDD9\uDDE2\uDDE5-\uDDFF\uDE3F-\uDE46\uDE48-\uDE4F\uDE9A-\uDE9C\uDE9E-\uDEBF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC37\uDC41-\uDC4F\uDC5A-\uDC71\uDC90\uDC91\uDCA8\uDCB7-\uDCFF\uDD07\uDD0A\uDD37-\uDD39\uDD3B\uDD3E\uDD48-\uDD4F\uDD5A-\uDD5F\uDD66\uDD69\uDD8F\uDD92\uDD99-\uDD9F\uDDAA-\uDEDF\uDEF7-\uDFAF\uDFB1-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC6F-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80B\uD80E-\uD810\uD812-\uD819\uD824-\uD82B\uD82D\uD82E\uD830-\uD833\uD837\uD839\uD83D\uD83F\uD87B-\uD87D\uD87F\uD885-\uDB3F\uDB41-\uDBFF][\uDC00-\uDFFF]|\uD80D[\uDC2F-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F\uDE6A-\uDECF\uDEEE\uDEEF\uDEF5-\uDEFF\uDF37-\uDF3F\uDF44-\uDF4F\uDF5A-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4E\uDF88-\uDF8E\uDFA0-\uDFDF\uDFE2\uDFE5-\uDFEF\uDFF2-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82C[\uDD1F-\uDD4F\uDD53-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDC9C\uDC9F-\uDFFF]|\uD834[\uDC00-\uDD64\uDD6A-\uDD6C\uDD73-\uDD7A\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDE41\uDE45-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC\uDFCD]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85-\uDE9A\uDEA0\uDEB0-\uDFFF]|\uD838[\uDC07\uDC19\uDC1A\uDC22\uDC25\uDC2B-\uDCFF\uDD2D-\uDD2F\uDD3E\uDD3F\uDD4A-\uDD4D\uDD4F-\uDEBF\uDEFA-\uDFFF]|\uD83A[\uDCC5-\uDCCF\uDCD7-\uDCFF\uDD4C-\uDD4F\uDD5A-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD83C[\uDC00-\uDD2F\uDD4A-\uDD4F\uDD6A-\uDD6F\uDD8A-\uDFFF]|\uD83E[\uDC00-\uDFEF\uDFFA-\uDFFF]|\uD869[\uDEDE-\uDEFF]|\uD86D[\uDF35-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDFFF]|\uDB40[\uDC00-\uDCFF\uDDF0-\uDFFF]/g;
17372
+
17373
+ // node_modules/github-slugger/index.js
17374
+ var own = Object.hasOwnProperty;
17375
+
17376
+ class BananaSlug {
17377
+ constructor() {
17378
+ this.occurrences;
17379
+ this.reset();
17380
+ }
17381
+ slug(value, maintainCase) {
17382
+ const self = this;
17383
+ let result = slug(value, maintainCase === true);
17384
+ const originalSlug = result;
17385
+ while (own.call(self.occurrences, result)) {
17386
+ self.occurrences[originalSlug]++;
17387
+ result = originalSlug + "-" + self.occurrences[originalSlug];
17388
+ }
17389
+ self.occurrences[result] = 0;
17390
+ return result;
17391
+ }
17392
+ reset() {
17393
+ this.occurrences = Object.create(null);
17394
+ }
16977
17395
  }
16978
- function finalizeIssues(issues, strict) {
16979
- if (!strict)
16980
- return issues;
16981
- return issues.map((i) => i.severity === "warning" ? { ...i, severity: "error" } : i);
17396
+ function slug(value, maintainCase) {
17397
+ if (typeof value !== "string")
17398
+ return "";
17399
+ if (!maintainCase)
17400
+ value = value.toLowerCase();
17401
+ return value.replace(regex, "").replace(/ /g, "-");
16982
17402
  }
16983
- function printReport(issues, options) {
16984
- const finalized = finalizeIssues(issues, options.strict ?? false);
16985
- const errors2 = finalized.filter((i) => i.severity === "error");
16986
- const warnings = finalized.filter((i) => i.severity === "warning");
16987
- const label = options.label ?? "Audit";
16988
- if (options.json) {
16989
- console.log(JSON.stringify({
16990
- label,
16991
- fileCount: options.fileCount,
16992
- errors: errors2.length,
16993
- warnings: warnings.length,
16994
- issues: finalized
16995
- }, null, 2));
16996
- return errors2.length > 0 ? 1 : 0;
16997
- }
16998
- if (warnings.length > 0) {
16999
- console.log(`${label} warnings:
17000
- `);
17001
- for (const i of warnings) {
17002
- const linkPart = i.link ? ` (${i.link})` : "";
17003
- console.log(`- ${i.file}${linkPart}: ${i.message}`);
17004
- }
17005
- console.log("");
17006
- }
17007
- if (errors2.length === 0) {
17008
- const warnNote = warnings.length > 0 ? `, ${warnings.length} warning(s)` : "";
17009
- const countNote = options.successSuffix ?? (options.fileCount !== undefined ? ` (${options.fileCount} files scanned${warnNote})` : "");
17010
- console.log(`${label} passed${countNote}.`);
17011
- return 0;
17012
- }
17013
- console.log(`${label} failed:
17014
- `);
17015
- for (const i of errors2) {
17016
- const linkPart = i.link ? ` (${i.link})` : "";
17017
- console.log(`- ${i.file}${linkPart}: ${i.message}`);
17018
- }
17019
- return 1;
17020
- }
17021
-
17022
- // src/references/check.ts
17023
- import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "node:fs";
17024
- import { join as join6, relative as relative6 } from "node:path";
17025
-
17026
- // src/references/constants.ts
17027
- var CANONICAL_REFS_DIR = ".skeleton/references";
17028
- var GENERATED_MARKER_START = "<!-- skeleton: generated-reference";
17029
- var GENERATED_MARKER_RE = /<!-- skeleton: generated-reference\s*\nsource: ([^\n]+)\s*\nredundancy: intentional\s*\n-->\s*\n?/;
17030
- var SHARED_REF_LINK_RE = /\((?:\.\.\/)+references\/([^)]+)\)/g;
17031
- function formatGeneratedHeader(sourceRelPath) {
17032
- return `${GENERATED_MARKER_START}
17033
- source: ${sourceRelPath}
17034
- redundancy: intentional
17035
- -->
17036
17403
 
17037
- `;
17038
- }
17039
- function stripGeneratedHeader(content) {
17040
- return content.replace(GENERATED_MARKER_RE, "");
17041
- }
17042
- function isGeneratedReference(content) {
17043
- return content.startsWith(GENERATED_MARKER_START);
17044
- }
17404
+ // node_modules/devlop/lib/development.js
17405
+ var codesWarned = new Set;
17045
17406
 
17046
- // src/references/discover.ts
17047
- import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
17048
- import { join as join5, relative as relative5 } from "node:path";
17049
- function walkMarkdownFiles(dir, root) {
17050
- const files = [];
17051
- if (!existsSync5(dir))
17052
- return files;
17053
- for (const entry of readdirSync3(dir, { withFileTypes: true })) {
17054
- if (entry.name.startsWith("."))
17055
- continue;
17056
- const fullPath = join5(dir, entry.name);
17057
- if (entry.isDirectory()) {
17058
- files.push(...walkMarkdownFiles(fullPath, root));
17059
- continue;
17060
- }
17061
- if (entry.name.endsWith(".md")) {
17062
- files.push(normalizeRelPath(relative5(root, fullPath)));
17063
- }
17064
- }
17065
- return files;
17066
- }
17067
- function canonicalExists(root, refPath) {
17068
- return existsSync5(join5(root, CANONICAL_REFS_DIR, refPath));
17069
- }
17070
- function findSharedRefLinks(content, sourceFile) {
17071
- const links = [];
17072
- for (const match of content.matchAll(SHARED_REF_LINK_RE)) {
17073
- const refPath = match[1];
17074
- if (!refPath)
17075
- continue;
17076
- links.push({ refPath: normalizeRelPath(refPath), sourceFile });
17077
- }
17078
- return links;
17079
- }
17080
- function findLocalCanonicalLinks(root, content, sourceFile) {
17081
- const links = [];
17082
- const localRefRe = /\((?:\.\/)?references\/([^)]+)\)/g;
17083
- for (const match of content.matchAll(localRefRe)) {
17084
- const refPath = normalizeRelPath(match[1] ?? "");
17085
- if (!refPath || !canonicalExists(root, refPath))
17086
- continue;
17087
- links.push({ refPath, sourceFile });
17088
- }
17089
- const inReferencesDir = /\/references\//.test(sourceFile);
17090
- if (inReferencesDir) {
17091
- const refsIdx = sourceFile.lastIndexOf("/references/");
17092
- const withinRefs = sourceFile.slice(refsIdx + "/references/".length);
17093
- const withinDir = withinRefs.includes("/") ? withinRefs.slice(0, withinRefs.lastIndexOf("/")) : "";
17094
- const siblingRe = /\((?!https?:|#|\.\.\/)([a-z0-9./_-]+\.md)\)/gi;
17095
- for (const match of content.matchAll(siblingRe)) {
17096
- const raw = normalizeRelPath(match[1] ?? "");
17097
- if (!raw)
17098
- continue;
17099
- const refPath = withinDir ? normalizeRelPath(join5(withinDir, raw)) : raw;
17100
- if (!canonicalExists(root, refPath))
17101
- continue;
17102
- links.push({ refPath, sourceFile });
17103
- }
17104
- }
17105
- return links;
17106
- }
17107
- function discoverSkillReferencePlans(root) {
17108
- const index = buildSkillIndex(root);
17109
- const plans = [];
17110
- for (const slug of index.slugs) {
17111
- const skillDir = join5(root, slug);
17112
- if (!existsSync5(join5(skillDir, "SKILL.md")))
17113
- continue;
17114
- const refPaths = new Set;
17115
- const links = [];
17116
- for (const relFile of walkMarkdownFiles(skillDir, root)) {
17117
- const content = readFileSync4(join5(root, relFile), "utf8");
17118
- if (isGeneratedReference(content))
17119
- continue;
17120
- for (const link of findSharedRefLinks(content, relFile)) {
17121
- refPaths.add(link.refPath);
17122
- links.push(link);
17123
- }
17124
- for (const link of findLocalCanonicalLinks(root, content, relFile)) {
17125
- refPaths.add(link.refPath);
17126
- links.push(link);
17127
- }
17128
- }
17129
- const queue = [...refPaths];
17130
- while (queue.length > 0) {
17131
- const refPath = queue.pop();
17132
- if (!refPath || !canonicalExists(root, refPath))
17133
- continue;
17134
- const canonicalContent = readFileSync4(join5(root, CANONICAL_REFS_DIR, refPath), "utf8");
17135
- const syntheticSource = generatedRefPath(slug, refPath);
17136
- for (const link of findLocalCanonicalLinks(root, canonicalContent, syntheticSource)) {
17137
- if (refPaths.has(link.refPath))
17138
- continue;
17139
- refPaths.add(link.refPath);
17140
- links.push(link);
17141
- queue.push(link.refPath);
17142
- }
17143
- }
17144
- if (refPaths.size > 0) {
17145
- plans.push({ skill: slug, refPaths, links });
17407
+ class AssertionError extends Error {
17408
+ name = "Assertion";
17409
+ code = "ERR_ASSERTION";
17410
+ constructor(message, actual, expected, operator, generated) {
17411
+ super(message);
17412
+ if (Error.captureStackTrace) {
17413
+ Error.captureStackTrace(this, this.constructor);
17146
17414
  }
17415
+ this.actual = actual;
17416
+ this.expected = expected;
17417
+ this.generated = generated;
17418
+ this.operator = operator;
17147
17419
  }
17148
- return plans.sort((a, b) => a.skill.localeCompare(b.skill));
17149
17420
  }
17150
- function generatedRefPath(skill, refPath) {
17151
- return normalizeRelPath(join5(skill, "references", refPath));
17421
+ function ok(value, message) {
17422
+ assert(Boolean(value), false, true, "ok", "Expected value to be truthy", message);
17152
17423
  }
17153
- function rewriteSharedRefTarget(sourceFile, skill, refPath) {
17154
- const sourceDir = sourceFile.slice(0, sourceFile.lastIndexOf("/"));
17155
- const target = generatedRefPath(skill, refPath);
17156
- if (!sourceDir)
17157
- return target;
17158
- const fromParts = sourceDir.split("/");
17159
- const toParts = target.split("/");
17160
- let i = 0;
17161
- while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) {
17162
- i++;
17424
+ function assert(bool, actual, expected, operator, defaultMessage, userMessage) {
17425
+ if (!bool) {
17426
+ throw userMessage instanceof Error ? userMessage : new AssertionError(userMessage || defaultMessage, actual, expected, operator, !userMessage);
17163
17427
  }
17164
- const ups = fromParts.length - i;
17165
- const down = toParts.slice(i);
17166
- const rel = [...Array(ups).fill(".."), ...down].join("/");
17167
- return rel || (toParts.at(-1) ?? refPath);
17168
- }
17169
- function rewriteSharedRefLinks(content, sourceFile, skill) {
17170
- return content.replace(SHARED_REF_LINK_RE, (_match, refPath) => {
17171
- const rewritten = rewriteSharedRefTarget(sourceFile, skill, normalizeRelPath(refPath));
17172
- return `(${rewritten})`;
17173
- });
17174
17428
  }
17175
17429
 
17176
- // src/references/check.ts
17177
- function listAllGeneratedFiles(root) {
17178
- const files = [];
17179
- const walk = (dir) => {
17180
- if (!existsSync6(dir))
17181
- return;
17182
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
17183
- if (entry.name.startsWith("."))
17184
- continue;
17185
- const fullPath = join6(dir, entry.name);
17186
- if (entry.isDirectory()) {
17187
- walk(fullPath);
17188
- continue;
17189
- }
17190
- if (!entry.name.endsWith(".md"))
17191
- continue;
17192
- const content = readFileSync5(fullPath, "utf8");
17193
- if (isGeneratedReference(content)) {
17194
- files.push(normalizeRelPath(relative6(root, fullPath)));
17195
- }
17196
- }
17197
- };
17198
- walk(root);
17199
- return files;
17430
+ // node_modules/mdast-util-to-string/lib/index.js
17431
+ var emptyOptions = {};
17432
+ function toString(value, options) {
17433
+ const settings = options || emptyOptions;
17434
+ const includeImageAlt = typeof settings.includeImageAlt === "boolean" ? settings.includeImageAlt : true;
17435
+ const includeHtml = typeof settings.includeHtml === "boolean" ? settings.includeHtml : true;
17436
+ return one(value, includeImageAlt, includeHtml);
17200
17437
  }
17201
- function runGeneratedReferencesCheck(root) {
17202
- const issues = [];
17203
- const canonicalDir = join6(root, CANONICAL_REFS_DIR);
17204
- if (!existsSync6(canonicalDir))
17205
- return issues;
17206
- const plans = discoverSkillReferencePlans(root);
17207
- const needed = new Set;
17208
- for (const plan of plans) {
17209
- for (const refPath of plan.refPaths) {
17210
- needed.add(generatedRefPath(plan.skill, refPath));
17211
- }
17212
- }
17213
- for (const targetRel of needed) {
17214
- const targetPath = join6(root, targetRel);
17215
- if (!existsSync6(targetPath)) {
17216
- issues.push(issue("generated-references", targetRel, "missing generated copy — run skeleton references sync"));
17217
- continue;
17218
- }
17219
- const generated = readFileSync5(targetPath, "utf8");
17220
- if (!isGeneratedReference(generated)) {
17221
- issues.push(issue("generated-references", targetRel, "expected generated-reference provenance header"));
17222
- continue;
17223
- }
17224
- const body = stripGeneratedHeader(generated);
17225
- const sourceRel = normalizeRelPath(generated.match(/source: ([^\n]+)/)?.[1] ?? join6(CANONICAL_REFS_DIR, targetRel.split("/references/")[1] ?? ""));
17226
- const canonicalPath = join6(root, sourceRel);
17227
- if (!existsSync6(canonicalPath)) {
17228
- issues.push(issue("generated-references", targetRel, `canonical source missing: ${sourceRel}`));
17229
- continue;
17438
+ function one(value, includeImageAlt, includeHtml) {
17439
+ if (node(value)) {
17440
+ if ("value" in value) {
17441
+ return value.type === "html" && !includeHtml ? "" : value.value;
17230
17442
  }
17231
- const canonical = readFileSync5(canonicalPath, "utf8");
17232
- if (body !== canonical) {
17233
- issues.push(issue("generated-references", targetRel, "stale generated copy — run skeleton references sync"));
17443
+ if (includeImageAlt && "alt" in value && value.alt) {
17444
+ return value.alt;
17234
17445
  }
17235
- }
17236
- for (const generatedRel of listAllGeneratedFiles(root)) {
17237
- if (!needed.has(generatedRel)) {
17238
- issues.push(issue("generated-references", generatedRel, "orphaned generated copy — run skeleton references sync"));
17446
+ if ("children" in value) {
17447
+ return all(value.children, includeImageAlt, includeHtml);
17239
17448
  }
17240
17449
  }
17241
- for (const plan of plans) {
17242
- const skillDir = join6(root, plan.skill);
17243
- if (!existsSync6(skillDir))
17244
- continue;
17245
- const walk = (dir) => {
17246
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
17247
- if (entry.name.startsWith("."))
17248
- continue;
17249
- const fullPath = join6(dir, entry.name);
17250
- if (entry.isDirectory()) {
17251
- walk(fullPath);
17252
- continue;
17253
- }
17254
- if (!entry.name.endsWith(".md"))
17255
- continue;
17256
- const relFile = normalizeRelPath(relative6(root, fullPath));
17257
- const content = readFileSync5(fullPath, "utf8");
17258
- if (content.match(SHARED_REF_LINK_RE)) {
17259
- issues.push(issue("generated-references", relFile, "still links to shared root references/ — run skeleton references sync"));
17260
- }
17261
- }
17262
- };
17263
- walk(skillDir);
17450
+ if (Array.isArray(value)) {
17451
+ return all(value, includeImageAlt, includeHtml);
17264
17452
  }
17265
- return issues;
17266
- }
17267
- function runGeneratedReferencesRule(ctx) {
17268
- return runGeneratedReferencesCheck(ctx.root);
17453
+ return "";
17269
17454
  }
17270
- var generatedReferencesRule = {
17271
- id: "generated-references",
17272
- global: true,
17273
- run: runGeneratedReferencesRule
17274
- };
17275
-
17276
- // src/audit/rules/banned.ts
17277
- function runBannedRule(ctx) {
17278
- const issues = [];
17279
- for (const abs of collectBannedFiles(ctx.config, ctx.root)) {
17280
- const rel = relPath(abs, ctx.root);
17281
- issues.push(issue("banned", rel, "file matches scan.banned — must not exist in repo"));
17455
+ function all(values, includeImageAlt, includeHtml) {
17456
+ const result = [];
17457
+ let index = -1;
17458
+ while (++index < values.length) {
17459
+ result[index] = one(values[index], includeImageAlt, includeHtml);
17282
17460
  }
17283
- return issues;
17284
- }
17285
- var bannedRule = { id: "banned", run: runBannedRule };
17286
-
17287
- // src/audit/rules/doc-meta.ts
17288
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
17289
- import { join as join7 } from "node:path";
17290
-
17291
- // src/audit/core/git-meta.ts
17292
- import { spawnSync } from "node:child_process";
17293
- function lastGitCommitDate(relPath2, root) {
17294
- const proc = spawnSync("git", ["log", "-1", "--format=%cs", "--", relPath2], {
17295
- cwd: root,
17296
- encoding: "utf8"
17297
- });
17298
- if (proc.status !== 0)
17299
- return null;
17300
- const date = proc.stdout.trim();
17301
- return date || null;
17302
- }
17303
-
17304
- // src/audit/rules/doc-meta.ts
17305
- function runDocMetaRule(ctx) {
17306
- const issues = [];
17307
- const today = new Date;
17308
- for (const relPath2 of ctx.docMetaPaths) {
17309
- const abs = join7(ctx.root, relPath2);
17310
- if (!existsSync7(abs))
17311
- continue;
17312
- const content = readFileSync6(abs, "utf8");
17313
- if (!DOC_META_RE.test(content)) {
17314
- issues.push(issue("doc-meta", relPath2, "missing doc-meta comment (owner + last-reviewed)"));
17315
- continue;
17316
- }
17317
- const match = DOC_META_LAST_REVIEWED_RE.exec(content);
17318
- if (!match?.[1])
17319
- continue;
17320
- const reviewed = new Date(`${match[1]}T00:00:00Z`);
17321
- if (Number.isNaN(reviewed.getTime()))
17322
- continue;
17323
- const ageDays = (today.getTime() - reviewed.getTime()) / 86400000;
17324
- if (ageDays > ctx.config.daysUntilStale) {
17325
- issues.push(issue("doc-meta", relPath2, `doc-meta last-reviewed ${match[1]} is stale (>${ctx.config.daysUntilStale} days)`, { severity: "warning" }));
17326
- }
17327
- const gitDate = lastGitCommitDate(relPath2, ctx.root);
17328
- if (!gitDate)
17329
- continue;
17330
- const committed = new Date(`${gitDate}T00:00:00Z`);
17331
- if (Number.isNaN(committed.getTime()))
17332
- continue;
17333
- if (committed.getTime() > reviewed.getTime()) {
17334
- issues.push(issue("doc-meta", relPath2, `content changed after last-reviewed ${match[1]} (git: ${gitDate}) — bump last-reviewed or confirm review`, { severity: "warning" }));
17335
- }
17336
- }
17337
- return issues;
17338
- }
17339
- var docMetaRule = { id: "doc-meta", run: runDocMetaRule };
17340
-
17341
- // src/audit/rules/links.ts
17342
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
17343
- import { dirname as dirname3, resolve as resolve4 } from "node:path";
17344
-
17345
- // node_modules/github-slugger/regex.js
17346
- var regex = /[\0-\x1F!-,\.\/:-@\[-\^`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482\u0530\u0557\u0558\u055A-\u055F\u0589-\u0590\u05BE\u05C0\u05C3\u05C6\u05C8-\u05CF\u05EB-\u05EE\u05F3-\u060F\u061B-\u061F\u066A-\u066D\u06D4\u06DD\u06DE\u06E9\u06FD\u06FE\u0700-\u070F\u074B\u074C\u07B2-\u07BF\u07F6-\u07F9\u07FB\u07FC\u07FE\u07FF\u082E-\u083F\u085C-\u085F\u086B-\u089F\u08B5\u08C8-\u08D2\u08E2\u0964\u0965\u0970\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09F2-\u09FB\u09FD\u09FF\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF0-\u0AF8\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B54\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B70\u0B72-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BF0-\u0BFF\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5B-\u0C5F\u0C64\u0C65\u0C70-\u0C7F\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0CFF\u0D0D\u0D11\u0D45\u0D49\u0D4F-\u0D53\u0D58-\u0D5E\u0D64\u0D65\u0D70-\u0D79\u0D80\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DE5\u0DF0\u0DF1\u0DF4-\u0E00\u0E3B-\u0E3F\u0E4F\u0E5A-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F01-\u0F17\u0F1A-\u0F1F\u0F2A-\u0F34\u0F36\u0F38\u0F3A-\u0F3D\u0F48\u0F6D-\u0F70\u0F85\u0F98\u0FBD-\u0FC5\u0FC7-\u0FFF\u104A-\u104F\u109E\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u1360-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16ED\u16F9-\u16FF\u170D\u1715-\u171F\u1735-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17D4-\u17D6\u17D8-\u17DB\u17DE\u17DF\u17EA-\u180A\u180E\u180F\u181A-\u181F\u1879-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191F\u192C-\u192F\u193C-\u1945\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DA-\u19FF\u1A1C-\u1A1F\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1AA6\u1AA8-\u1AAF\u1AC1-\u1AFF\u1B4C-\u1B4F\u1B5A-\u1B6A\u1B74-\u1B7F\u1BF4-\u1BFF\u1C38-\u1C3F\u1C4A-\u1C4C\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CCF\u1CD3\u1CFB-\u1CFF\u1DFA\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u203E\u2041-\u2053\u2055-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u20CF\u20F1-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u215F\u2189-\u24B5\u24EA-\u2BFF\u2C2F\u2C5F\u2CE5-\u2CEA\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E00-\u2E2E\u2E30-\u3004\u3008-\u3020\u3030\u3036\u3037\u303D-\u3040\u3097\u3098\u309B\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31C0-\u31EF\u3200-\u33FF\u4DC0-\u4DFF\u9FFD-\u9FFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA62C-\uA63F\uA673\uA67E\uA6F2-\uA716\uA720\uA721\uA789\uA78A\uA7C0\uA7C1\uA7CB-\uA7F4\uA828-\uA82B\uA82D-\uA83F\uA874-\uA87F\uA8C6-\uA8CF\uA8DA-\uA8DF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA954-\uA95F\uA97D-\uA97F\uA9C1-\uA9CE\uA9DA-\uA9DF\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A-\uAA5F\uAA77-\uAA79\uAAC3-\uAADA\uAADE\uAADF\uAAF0\uAAF1\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB6A-\uAB6F\uABEB\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFDFF\uFE10-\uFE1F\uFE30-\uFE32\uFE35-\uFE4C\uFE50-\uFE6F\uFE75\uFEFD-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3E\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDD3F\uDD75-\uDDFC\uDDFE-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEDF\uDEE1-\uDEFF\uDF20-\uDF2C\uDF4B-\uDF4F\uDF7B-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0\uDFD6-\uDFFF]|\uD801[\uDC9E\uDC9F\uDCAA-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE04\uDE07-\uDE0B\uDE14\uDE18\uDE36\uDE37\uDE3B-\uDE3E\uDE40-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE7-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD28-\uDD2F\uDD3A-\uDE7F\uDEAA\uDEAD-\uDEAF\uDEB2-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF51-\uDFAF\uDFC5-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC47-\uDC65\uDC70-\uDC7E\uDCBB-\uDCCF\uDCE9-\uDCEF\uDCFA-\uDCFF\uDD35\uDD40-\uDD43\uDD48-\uDD4F\uDD74\uDD75\uDD77-\uDD7F\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDFF\uDE12\uDE38-\uDE3D\uDE3F-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEEB-\uDEEF\uDEFA-\uDEFF\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A\uDF45\uDF46\uDF49\uDF4A\uDF4E\uDF4F\uDF51-\uDF56\uDF58-\uDF5C\uDF64\uDF65\uDF6D-\uDF6F\uDF75-\uDFFF]|\uD805[\uDC4B-\uDC4F\uDC5A-\uDC5D\uDC62-\uDC7F\uDCC6\uDCC8-\uDCCF\uDCDA-\uDD7F\uDDB6\uDDB7\uDDC1-\uDDD7\uDDDE-\uDDFF\uDE41-\uDE43\uDE45-\uDE4F\uDE5A-\uDE7F\uDEB9-\uDEBF\uDECA-\uDEFF\uDF1B\uDF1C\uDF2C-\uDF2F\uDF3A-\uDFFF]|\uD806[\uDC3B-\uDC9F\uDCEA-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD36\uDD39\uDD3A\uDD44-\uDD4F\uDD5A-\uDD9F\uDDA8\uDDA9\uDDD8\uDDD9\uDDE2\uDDE5-\uDDFF\uDE3F-\uDE46\uDE48-\uDE4F\uDE9A-\uDE9C\uDE9E-\uDEBF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC37\uDC41-\uDC4F\uDC5A-\uDC71\uDC90\uDC91\uDCA8\uDCB7-\uDCFF\uDD07\uDD0A\uDD37-\uDD39\uDD3B\uDD3E\uDD48-\uDD4F\uDD5A-\uDD5F\uDD66\uDD69\uDD8F\uDD92\uDD99-\uDD9F\uDDAA-\uDEDF\uDEF7-\uDFAF\uDFB1-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC6F-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80B\uD80E-\uD810\uD812-\uD819\uD824-\uD82B\uD82D\uD82E\uD830-\uD833\uD837\uD839\uD83D\uD83F\uD87B-\uD87D\uD87F\uD885-\uDB3F\uDB41-\uDBFF][\uDC00-\uDFFF]|\uD80D[\uDC2F-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F\uDE6A-\uDECF\uDEEE\uDEEF\uDEF5-\uDEFF\uDF37-\uDF3F\uDF44-\uDF4F\uDF5A-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4E\uDF88-\uDF8E\uDFA0-\uDFDF\uDFE2\uDFE5-\uDFEF\uDFF2-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82C[\uDD1F-\uDD4F\uDD53-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDC9C\uDC9F-\uDFFF]|\uD834[\uDC00-\uDD64\uDD6A-\uDD6C\uDD73-\uDD7A\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDE41\uDE45-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC\uDFCD]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85-\uDE9A\uDEA0\uDEB0-\uDFFF]|\uD838[\uDC07\uDC19\uDC1A\uDC22\uDC25\uDC2B-\uDCFF\uDD2D-\uDD2F\uDD3E\uDD3F\uDD4A-\uDD4D\uDD4F-\uDEBF\uDEFA-\uDFFF]|\uD83A[\uDCC5-\uDCCF\uDCD7-\uDCFF\uDD4C-\uDD4F\uDD5A-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD83C[\uDC00-\uDD2F\uDD4A-\uDD4F\uDD6A-\uDD6F\uDD8A-\uDFFF]|\uD83E[\uDC00-\uDFEF\uDFFA-\uDFFF]|\uD869[\uDEDE-\uDEFF]|\uD86D[\uDF35-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDFFF]|\uDB40[\uDC00-\uDCFF\uDDF0-\uDFFF]/g;
17347
-
17348
- // node_modules/github-slugger/index.js
17349
- var own = Object.hasOwnProperty;
17350
-
17351
- class BananaSlug {
17352
- constructor() {
17353
- this.occurrences;
17354
- this.reset();
17355
- }
17356
- slug(value, maintainCase) {
17357
- const self = this;
17358
- let result = slug(value, maintainCase === true);
17359
- const originalSlug = result;
17360
- while (own.call(self.occurrences, result)) {
17361
- self.occurrences[originalSlug]++;
17362
- result = originalSlug + "-" + self.occurrences[originalSlug];
17363
- }
17364
- self.occurrences[result] = 0;
17365
- return result;
17366
- }
17367
- reset() {
17368
- this.occurrences = Object.create(null);
17369
- }
17370
- }
17371
- function slug(value, maintainCase) {
17372
- if (typeof value !== "string")
17373
- return "";
17374
- if (!maintainCase)
17375
- value = value.toLowerCase();
17376
- return value.replace(regex, "").replace(/ /g, "-");
17377
- }
17378
-
17379
- // node_modules/devlop/lib/development.js
17380
- var codesWarned = new Set;
17381
-
17382
- class AssertionError extends Error {
17383
- name = "Assertion";
17384
- code = "ERR_ASSERTION";
17385
- constructor(message, actual, expected, operator, generated) {
17386
- super(message);
17387
- if (Error.captureStackTrace) {
17388
- Error.captureStackTrace(this, this.constructor);
17389
- }
17390
- this.actual = actual;
17391
- this.expected = expected;
17392
- this.generated = generated;
17393
- this.operator = operator;
17394
- }
17395
- }
17396
- function ok(value, message) {
17397
- assert(Boolean(value), false, true, "ok", "Expected value to be truthy", message);
17398
- }
17399
- function assert(bool, actual, expected, operator, defaultMessage, userMessage) {
17400
- if (!bool) {
17401
- throw userMessage instanceof Error ? userMessage : new AssertionError(userMessage || defaultMessage, actual, expected, operator, !userMessage);
17402
- }
17403
- }
17404
-
17405
- // node_modules/mdast-util-to-string/lib/index.js
17406
- var emptyOptions = {};
17407
- function toString(value, options) {
17408
- const settings = options || emptyOptions;
17409
- const includeImageAlt = typeof settings.includeImageAlt === "boolean" ? settings.includeImageAlt : true;
17410
- const includeHtml = typeof settings.includeHtml === "boolean" ? settings.includeHtml : true;
17411
- return one(value, includeImageAlt, includeHtml);
17412
- }
17413
- function one(value, includeImageAlt, includeHtml) {
17414
- if (node(value)) {
17415
- if ("value" in value) {
17416
- return value.type === "html" && !includeHtml ? "" : value.value;
17417
- }
17418
- if (includeImageAlt && "alt" in value && value.alt) {
17419
- return value.alt;
17420
- }
17421
- if ("children" in value) {
17422
- return all(value.children, includeImageAlt, includeHtml);
17423
- }
17424
- }
17425
- if (Array.isArray(value)) {
17426
- return all(value, includeImageAlt, includeHtml);
17427
- }
17428
- return "";
17429
- }
17430
- function all(values, includeImageAlt, includeHtml) {
17431
- const result = [];
17432
- let index = -1;
17433
- while (++index < values.length) {
17434
- result[index] = one(values[index], includeImageAlt, includeHtml);
17435
- }
17436
- return result.join("");
17461
+ return result.join("");
17437
17462
  }
17438
17463
  function node(value) {
17439
17464
  return Boolean(value && typeof value === "object");
@@ -20343,10 +20368,10 @@ function resolveAll(constructs2, events, context) {
20343
20368
  const called = [];
20344
20369
  let index = -1;
20345
20370
  while (++index < constructs2.length) {
20346
- const resolve4 = constructs2[index].resolveAll;
20347
- if (resolve4 && !called.includes(resolve4)) {
20348
- events = resolve4(events, context);
20349
- called.push(resolve4);
20371
+ const resolve5 = constructs2[index].resolveAll;
20372
+ if (resolve5 && !called.includes(resolve5)) {
20373
+ events = resolve5(events, context);
20374
+ called.push(resolve5);
20350
20375
  }
20351
20376
  }
20352
20377
  return events;
@@ -25520,7 +25545,7 @@ var handle = {
25520
25545
  };
25521
25546
 
25522
25547
  // node_modules/mdast-util-to-markdown/lib/join.js
25523
- var join8 = [joinDefaults];
25548
+ var join7 = [joinDefaults];
25524
25549
  function joinDefaults(left, right, parent, state) {
25525
25550
  if (right.type === "code" && formatCodeAsIndented(right, state) && (left.type === "list" || left.type === right.type && formatCodeAsIndented(left, state))) {
25526
25551
  return false;
@@ -25903,7 +25928,7 @@ function toMarkdown(tree, options) {
25903
25928
  handle: undefined,
25904
25929
  indentLines,
25905
25930
  indexStack: [],
25906
- join: [...join8],
25931
+ join: [...join7],
25907
25932
  options: {},
25908
25933
  safe: safeBound,
25909
25934
  stack: [],
@@ -26156,7 +26181,7 @@ import { default as default2 } from "node:path";
26156
26181
  import { default as default3 } from "node:process";
26157
26182
 
26158
26183
  // node_modules/vfile/lib/minurl.js
26159
- import { fileURLToPath as fileURLToPath3 } from "node:url";
26184
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
26160
26185
 
26161
26186
  // node_modules/vfile/lib/minurl.shared.js
26162
26187
  function isUrl(fileUrlOrPath) {
@@ -26209,40 +26234,40 @@ class VFile {
26209
26234
  get basename() {
26210
26235
  return typeof this.path === "string" ? default2.basename(this.path) : undefined;
26211
26236
  }
26212
- set basename(basename2) {
26213
- assertNonEmpty(basename2, "basename");
26214
- assertPart(basename2, "basename");
26215
- this.path = default2.join(this.dirname || "", basename2);
26237
+ set basename(basename3) {
26238
+ assertNonEmpty(basename3, "basename");
26239
+ assertPart(basename3, "basename");
26240
+ this.path = default2.join(this.dirname || "", basename3);
26216
26241
  }
26217
26242
  get dirname() {
26218
26243
  return typeof this.path === "string" ? default2.dirname(this.path) : undefined;
26219
26244
  }
26220
- set dirname(dirname3) {
26245
+ set dirname(dirname5) {
26221
26246
  assertPath(this.basename, "dirname");
26222
- this.path = default2.join(dirname3 || "", this.basename);
26247
+ this.path = default2.join(dirname5 || "", this.basename);
26223
26248
  }
26224
26249
  get extname() {
26225
26250
  return typeof this.path === "string" ? default2.extname(this.path) : undefined;
26226
26251
  }
26227
- set extname(extname) {
26228
- assertPart(extname, "extname");
26252
+ set extname(extname2) {
26253
+ assertPart(extname2, "extname");
26229
26254
  assertPath(this.dirname, "extname");
26230
- if (extname) {
26231
- if (extname.codePointAt(0) !== 46) {
26255
+ if (extname2) {
26256
+ if (extname2.codePointAt(0) !== 46) {
26232
26257
  throw new Error("`extname` must start with `.`");
26233
26258
  }
26234
- if (extname.includes(".", 1)) {
26259
+ if (extname2.includes(".", 1)) {
26235
26260
  throw new Error("`extname` cannot contain multiple dots");
26236
26261
  }
26237
26262
  }
26238
- this.path = default2.join(this.dirname, this.stem + (extname || ""));
26263
+ this.path = default2.join(this.dirname, this.stem + (extname2 || ""));
26239
26264
  }
26240
26265
  get path() {
26241
26266
  return this.history[this.history.length - 1];
26242
26267
  }
26243
26268
  set path(path) {
26244
26269
  if (isUrl(path)) {
26245
- path = fileURLToPath3(path);
26270
+ path = fileURLToPath4(path);
26246
26271
  }
26247
26272
  assertNonEmpty(path, "path");
26248
26273
  if (this.path !== path) {
@@ -26396,7 +26421,7 @@ class Processor extends CallableInstance {
26396
26421
  assertParser("process", this.parser || this.Parser);
26397
26422
  assertCompiler("process", this.compiler || this.Compiler);
26398
26423
  return done ? executor(undefined, done) : new Promise(executor);
26399
- function executor(resolve4, reject) {
26424
+ function executor(resolve5, reject) {
26400
26425
  const realFile = vfile(file);
26401
26426
  const parseTree = self.parse(realFile);
26402
26427
  self.run(parseTree, realFile, function(error, tree, file2) {
@@ -26415,8 +26440,8 @@ class Processor extends CallableInstance {
26415
26440
  function realDone(error, file2) {
26416
26441
  if (error || !file2) {
26417
26442
  reject(error);
26418
- } else if (resolve4) {
26419
- resolve4(file2);
26443
+ } else if (resolve5) {
26444
+ resolve5(file2);
26420
26445
  } else {
26421
26446
  ok(done, "`done` is defined if `resolve` is not");
26422
26447
  done(undefined, file2);
@@ -26449,7 +26474,7 @@ class Processor extends CallableInstance {
26449
26474
  file = undefined;
26450
26475
  }
26451
26476
  return done ? executor(undefined, done) : new Promise(executor);
26452
- function executor(resolve4, reject) {
26477
+ function executor(resolve5, reject) {
26453
26478
  ok(typeof file !== "function", "`file` can’t be a `done` anymore, we checked");
26454
26479
  const realFile = vfile(file);
26455
26480
  transformers.run(tree, realFile, realDone);
@@ -26457,8 +26482,8 @@ class Processor extends CallableInstance {
26457
26482
  const resultingTree = outputTree || tree;
26458
26483
  if (error) {
26459
26484
  reject(error);
26460
- } else if (resolve4) {
26461
- resolve4(resultingTree);
26485
+ } else if (resolve5) {
26486
+ resolve5(resultingTree);
26462
26487
  } else {
26463
26488
  ok(done, "`done` is defined if `resolve` is not");
26464
26489
  done(undefined, resultingTree, file2);
@@ -28492,214 +28517,984 @@ function flushCell(map4, context, range, rowKind, rowEnd, previousCell) {
28492
28517
  map4.add(a, b, []);
28493
28518
  }
28494
28519
  }
28495
- map4.add(range[3] + 1, 0, [["exit", valueToken, context]]);
28520
+ map4.add(range[3] + 1, 0, [["exit", valueToken, context]]);
28521
+ }
28522
+ if (rowEnd !== undefined) {
28523
+ previousCell.end = Object.assign({}, getPoint(context.events, rowEnd));
28524
+ map4.add(rowEnd, 0, [["exit", previousCell, context]]);
28525
+ previousCell = undefined;
28526
+ }
28527
+ return previousCell;
28528
+ }
28529
+ function flushTableEnd(map4, context, index2, table, tableBody) {
28530
+ const exits = [];
28531
+ const related = getPoint(context.events, index2);
28532
+ if (tableBody) {
28533
+ tableBody.end = Object.assign({}, related);
28534
+ exits.push(["exit", tableBody, context]);
28535
+ }
28536
+ table.end = Object.assign({}, related);
28537
+ exits.push(["exit", table, context]);
28538
+ map4.add(index2 + 1, 0, exits);
28539
+ }
28540
+ function getPoint(events, index2) {
28541
+ const event = events[index2];
28542
+ const side = event[0] === "enter" ? "start" : "end";
28543
+ return event[1][side];
28544
+ }
28545
+ // node_modules/micromark-extension-gfm-task-list-item/dev/lib/syntax.js
28546
+ var tasklistCheck = { name: "tasklistCheck", tokenize: tokenizeTasklistCheck };
28547
+ function gfmTaskListItem() {
28548
+ return {
28549
+ text: { [codes.leftSquareBracket]: tasklistCheck }
28550
+ };
28551
+ }
28552
+ function tokenizeTasklistCheck(effects, ok3, nok) {
28553
+ const self = this;
28554
+ return open;
28555
+ function open(code3) {
28556
+ ok(code3 === codes.leftSquareBracket, "expected `[`");
28557
+ if (self.previous !== codes.eof || !self._gfmTasklistFirstContentOfListItem) {
28558
+ return nok(code3);
28559
+ }
28560
+ effects.enter("taskListCheck");
28561
+ effects.enter("taskListCheckMarker");
28562
+ effects.consume(code3);
28563
+ effects.exit("taskListCheckMarker");
28564
+ return inside;
28565
+ }
28566
+ function inside(code3) {
28567
+ if (markdownLineEndingOrSpace(code3)) {
28568
+ effects.enter("taskListCheckValueUnchecked");
28569
+ effects.consume(code3);
28570
+ effects.exit("taskListCheckValueUnchecked");
28571
+ return close;
28572
+ }
28573
+ if (code3 === codes.uppercaseX || code3 === codes.lowercaseX) {
28574
+ effects.enter("taskListCheckValueChecked");
28575
+ effects.consume(code3);
28576
+ effects.exit("taskListCheckValueChecked");
28577
+ return close;
28578
+ }
28579
+ return nok(code3);
28580
+ }
28581
+ function close(code3) {
28582
+ if (code3 === codes.rightSquareBracket) {
28583
+ effects.enter("taskListCheckMarker");
28584
+ effects.consume(code3);
28585
+ effects.exit("taskListCheckMarker");
28586
+ effects.exit("taskListCheck");
28587
+ return after;
28588
+ }
28589
+ return nok(code3);
28590
+ }
28591
+ function after(code3) {
28592
+ if (markdownLineEnding(code3)) {
28593
+ return ok3(code3);
28594
+ }
28595
+ if (markdownSpace(code3)) {
28596
+ return effects.check({ tokenize: spaceThenNonSpace }, ok3, nok)(code3);
28597
+ }
28598
+ return nok(code3);
28599
+ }
28600
+ }
28601
+ function spaceThenNonSpace(effects, ok3, nok) {
28602
+ return factorySpace(effects, after, types.whitespace);
28603
+ function after(code3) {
28604
+ return code3 === codes.eof ? nok(code3) : ok3(code3);
28605
+ }
28606
+ }
28607
+ // node_modules/micromark-extension-gfm/index.js
28608
+ function gfm(options) {
28609
+ return combineExtensions([
28610
+ gfmAutolinkLiteral(),
28611
+ gfmFootnote(),
28612
+ gfmStrikethrough(options),
28613
+ gfmTable(),
28614
+ gfmTaskListItem()
28615
+ ]);
28616
+ }
28617
+
28618
+ // node_modules/remark-gfm/lib/index.js
28619
+ var emptyOptions2 = {};
28620
+ function remarkGfm(options) {
28621
+ const self = this;
28622
+ const settings = options || emptyOptions2;
28623
+ const data = self.data();
28624
+ const micromarkExtensions = data.micromarkExtensions || (data.micromarkExtensions = []);
28625
+ const fromMarkdownExtensions = data.fromMarkdownExtensions || (data.fromMarkdownExtensions = []);
28626
+ const toMarkdownExtensions = data.toMarkdownExtensions || (data.toMarkdownExtensions = []);
28627
+ micromarkExtensions.push(gfm(settings));
28628
+ fromMarkdownExtensions.push(gfmFromMarkdown());
28629
+ toMarkdownExtensions.push(gfmToMarkdown(settings));
28630
+ }
28631
+ // src/audit/core/markdown.ts
28632
+ var processor = remark().use(remarkGfm);
28633
+ function lineFromOffset(content3, offset) {
28634
+ if (offset === undefined)
28635
+ return;
28636
+ return content3.slice(0, offset).split(`
28637
+ `).length;
28638
+ }
28639
+ function stripYamlFrontmatter(content3) {
28640
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/.exec(content3);
28641
+ if (match)
28642
+ return content3.slice(match[0].length);
28643
+ const eof = /^---\r?\n([\s\S]*?)\r?\n---\s*$/.exec(content3);
28644
+ if (eof)
28645
+ return "";
28646
+ return content3;
28647
+ }
28648
+ function destinationConsumesToSliceEnd(slice, afterDest) {
28649
+ let i = afterDest;
28650
+ if (i >= slice.length)
28651
+ return false;
28652
+ if (slice[i] === ")") {
28653
+ return i === slice.length - 1;
28654
+ }
28655
+ if (!/\s/.test(slice[i]))
28656
+ return false;
28657
+ while (i < slice.length && /\s/.test(slice[i]))
28658
+ i++;
28659
+ if (i >= slice.length)
28660
+ return false;
28661
+ if (slice[i] === ")") {
28662
+ return i === slice.length - 1;
28663
+ }
28664
+ const open = slice[i];
28665
+ if (open !== '"' && open !== "'" && open !== "(")
28666
+ return false;
28667
+ const close = open === "(" ? ")" : open;
28668
+ i++;
28669
+ while (i < slice.length && slice[i] !== close)
28670
+ i++;
28671
+ if (i >= slice.length)
28672
+ return false;
28673
+ i++;
28674
+ while (i < slice.length && /\s/.test(slice[i]))
28675
+ i++;
28676
+ return i === slice.length - 1 && slice[i] === ")";
28677
+ }
28678
+ function findUrlSpanInSlice(content3, nodeStart, nodeEnd, url) {
28679
+ const slice = content3.slice(nodeStart, nodeEnd);
28680
+ let searchFrom = 0;
28681
+ while (searchFrom < slice.length) {
28682
+ const openParen = slice.indexOf("](", searchFrom);
28683
+ if (openParen === -1)
28684
+ break;
28685
+ const after = openParen + 2;
28686
+ if (slice.startsWith(`<${url}>`, after)) {
28687
+ const afterDest = after + 2 + url.length;
28688
+ if (destinationConsumesToSliceEnd(slice, afterDest)) {
28689
+ const urlStart = nodeStart + after + 1;
28690
+ return { urlStart, urlEnd: urlStart + url.length };
28691
+ }
28692
+ } else if (slice.startsWith(url, after)) {
28693
+ const next = slice[after + url.length];
28694
+ if (next === ")" || next !== undefined && /\s/.test(next)) {
28695
+ if (destinationConsumesToSliceEnd(slice, after + url.length)) {
28696
+ const urlStart = nodeStart + after;
28697
+ return { urlStart, urlEnd: urlStart + url.length };
28698
+ }
28699
+ }
28700
+ }
28701
+ searchFrom = openParen + 1;
28702
+ }
28703
+ const auto = `<${url}>`;
28704
+ if (slice === auto) {
28705
+ return { urlStart: nodeStart + 1, urlEnd: nodeStart + 1 + url.length };
28706
+ }
28707
+ if (slice === url) {
28708
+ return { urlStart: nodeStart, urlEnd: nodeEnd };
28709
+ }
28710
+ const trimmed = slice.trim();
28711
+ if (trimmed === auto) {
28712
+ const lead = slice.indexOf(auto);
28713
+ if (lead !== -1) {
28714
+ return { urlStart: nodeStart + lead + 1, urlEnd: nodeStart + lead + 1 + url.length };
28715
+ }
28716
+ }
28717
+ if (trimmed === url) {
28718
+ const lead = slice.indexOf(url);
28719
+ if (lead !== -1) {
28720
+ return { urlStart: nodeStart + lead, urlEnd: nodeStart + lead + url.length };
28721
+ }
28722
+ }
28723
+ return;
28724
+ }
28725
+ function findUrlInDefinitionSlice(content3, nodeStart, nodeEnd, url) {
28726
+ const slice = content3.slice(nodeStart, nodeEnd);
28727
+ const labelEnd2 = slice.indexOf("]:");
28728
+ if (labelEnd2 === -1)
28729
+ return;
28730
+ let i = labelEnd2 + 2;
28731
+ while (i < slice.length && /\s/.test(slice[i] ?? ""))
28732
+ i++;
28733
+ if (slice.startsWith(`<${url}>`, i)) {
28734
+ const urlStart = nodeStart + i + 1;
28735
+ return { urlStart, urlEnd: urlStart + url.length };
28736
+ }
28737
+ if (slice.startsWith(url, i)) {
28738
+ const urlStart = nodeStart + i;
28739
+ return { urlStart, urlEnd: urlStart + url.length };
28740
+ }
28741
+ return;
28742
+ }
28743
+ function collectReferenceDefinitions(content3, tree) {
28744
+ const defs = new Map;
28745
+ visit2(tree, (node2) => {
28746
+ if (node2.type !== "definition")
28747
+ return;
28748
+ if (!("identifier" in node2) || !("url" in node2))
28749
+ return;
28750
+ const id = String(node2.identifier).toLowerCase();
28751
+ if (defs.has(id))
28752
+ return;
28753
+ const url = typeof node2.url === "string" ? node2.url : "";
28754
+ if (!url)
28755
+ return;
28756
+ const start = node2.position?.start.offset;
28757
+ const end = node2.position?.end.offset;
28758
+ if (start === undefined || end === undefined)
28759
+ return;
28760
+ const span = findUrlInDefinitionSlice(content3, start, end, url);
28761
+ if (!span)
28762
+ return;
28763
+ defs.set(id, {
28764
+ url,
28765
+ urlStart: span.urlStart,
28766
+ urlEnd: span.urlEnd,
28767
+ line: lineFromOffset(content3, start) ?? 1
28768
+ });
28769
+ });
28770
+ return defs;
28771
+ }
28772
+ function extractLinksFromMarkdown(content3, _filePath) {
28773
+ const tree = processor.parse(content3);
28774
+ const refDefs = collectReferenceDefinitions(content3, tree);
28775
+ const links = [];
28776
+ visit2(tree, (node2) => {
28777
+ if (node2.type === "link" && "url" in node2 && typeof node2.url === "string") {
28778
+ const target = node2.url.trim();
28779
+ const start = node2.position?.start.offset;
28780
+ const end = node2.position?.end.offset;
28781
+ const span = start !== undefined && end !== undefined ? findUrlSpanInSlice(content3, start, end, target) : undefined;
28782
+ links.push({
28783
+ target,
28784
+ line: lineFromOffset(content3, node2.position?.start.offset),
28785
+ urlStart: span?.urlStart,
28786
+ urlEnd: span?.urlEnd
28787
+ });
28788
+ }
28789
+ if (node2.type === "linkReference" && "identifier" in node2) {
28790
+ const id = String(node2.identifier).toLowerCase();
28791
+ const def = refDefs.get(id);
28792
+ if (def) {
28793
+ links.push({
28794
+ target: def.url.trim(),
28795
+ line: def.line,
28796
+ urlStart: def.urlStart,
28797
+ urlEnd: def.urlEnd
28798
+ });
28799
+ }
28800
+ }
28801
+ });
28802
+ return links;
28803
+ }
28804
+ function phrasingText(nodes) {
28805
+ if (!nodes?.length)
28806
+ return "";
28807
+ let out = "";
28808
+ for (const node2 of nodes) {
28809
+ if (node2.type === "text" || node2.type === "inlineCode") {
28810
+ out += "value" in node2 && node2.value !== undefined ? String(node2.value) : "";
28811
+ continue;
28812
+ }
28813
+ if (node2.children?.length) {
28814
+ out += phrasingText(node2.children);
28815
+ }
28816
+ }
28817
+ return out;
28818
+ }
28819
+ function extractHeadingSlugs(content3, _filePath) {
28820
+ const body = stripYamlFrontmatter(content3);
28821
+ const slugger = new BananaSlug;
28822
+ const slugs = new Set;
28823
+ const tree = processor.parse(body);
28824
+ visit2(tree, (node2) => {
28825
+ if (node2.type === "heading" && "children" in node2) {
28826
+ const text5 = phrasingText(node2.children);
28827
+ if (text5)
28828
+ slugs.add(slugger.slug(text5));
28829
+ }
28830
+ });
28831
+ return slugs;
28832
+ }
28833
+ function slugifyAnchor(anchor) {
28834
+ const slugger = new BananaSlug;
28835
+ return slugger.slug(decodeURIComponent(anchor));
28836
+ }
28837
+
28838
+ // src/audit/fix/match-anchor.ts
28839
+ var ANCHOR_MATCH_MIN_SCORE = 2 / 3;
28840
+ var ANCHOR_MATCH_MIN_MARGIN = 0.15;
28841
+ function tokenize(slug2) {
28842
+ return new Set(slug2.split("-").filter(Boolean));
28843
+ }
28844
+ function jaccard(a, b) {
28845
+ if (a.size === 0 && b.size === 0)
28846
+ return 0;
28847
+ let intersection = 0;
28848
+ for (const token of a) {
28849
+ if (b.has(token))
28850
+ intersection++;
28851
+ }
28852
+ const union = a.size + b.size - intersection;
28853
+ return union === 0 ? 0 : intersection / union;
28854
+ }
28855
+ function scoreAnchorMatch(brokenSlug, candidateSlug) {
28856
+ if (brokenSlug === candidateSlug)
28857
+ return 1;
28858
+ if (candidateSlug.startsWith(`${brokenSlug}-`)) {
28859
+ return 1;
28860
+ }
28861
+ if (brokenSlug.startsWith(candidateSlug) && brokenSlug.length > candidateSlug.length) {
28862
+ return 0;
28863
+ }
28864
+ return jaccard(tokenize(brokenSlug), tokenize(candidateSlug));
28865
+ }
28866
+ function findBestAnchorMatch(brokenSlug, candidates) {
28867
+ const scored = [];
28868
+ for (const slug2 of candidates) {
28869
+ if (slug2 === brokenSlug)
28870
+ continue;
28871
+ scored.push({ slug: slug2, score: scoreAnchorMatch(brokenSlug, slug2) });
28872
+ }
28873
+ if (scored.length === 0)
28874
+ return null;
28875
+ scored.sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug));
28876
+ const best = scored[0];
28877
+ const second = scored[1];
28878
+ if (!best || best.score < ANCHOR_MATCH_MIN_SCORE)
28879
+ return null;
28880
+ if (second && best.score - second.score < ANCHOR_MATCH_MIN_MARGIN)
28881
+ return null;
28882
+ return best;
28883
+ }
28884
+
28885
+ // src/audit/fix/anchors.ts
28886
+ function resolveLink(sourceFile, target) {
28887
+ const withoutAnchor = target.split("#")[0]?.split("?")[0] ?? "";
28888
+ if (!withoutAnchor)
28889
+ return sourceFile;
28890
+ return resolve5(dirname5(sourceFile), withoutAnchor);
28891
+ }
28892
+ function replaceAnchorInTarget(target, oldAnchor, newAnchor) {
28893
+ const hashIndex = target.indexOf("#");
28894
+ if (hashIndex === -1)
28895
+ return target;
28896
+ const pathPart = target.slice(0, hashIndex);
28897
+ const fragment = target.slice(hashIndex + 1);
28898
+ const queryIndex = fragment.indexOf("?");
28899
+ const anchorPart = queryIndex === -1 ? fragment : fragment.slice(0, queryIndex);
28900
+ const queryPart = queryIndex === -1 ? "" : fragment.slice(queryIndex);
28901
+ if (anchorPart !== oldAnchor)
28902
+ return target;
28903
+ return `${pathPart}#${newAnchor}${queryPart}`;
28904
+ }
28905
+ function collectAnchorFixes(ctx) {
28906
+ const editsByFile = new Map;
28907
+ for (const filePath of ctx.files) {
28908
+ const content3 = readFileContent(filePath);
28909
+ const links = extractLinksFromMarkdown(content3, filePath);
28910
+ const pending = [];
28911
+ const relFile = relPath(filePath, ctx.root);
28912
+ for (const { target, line, urlStart, urlEnd } of links) {
28913
+ if (isExternalLink(target) && !target.startsWith("#"))
28914
+ continue;
28915
+ if (isPlaceholderLink(target))
28916
+ continue;
28917
+ const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
28918
+ if (!anchor)
28919
+ continue;
28920
+ const resolved = resolveLink(filePath, target);
28921
+ if (!existsSync8(resolved))
28922
+ continue;
28923
+ const targetContent = readFileSync6(resolved, "utf8");
28924
+ const slugs = extractHeadingSlugs(targetContent, resolved);
28925
+ const anchorSlug = slugifyAnchor(anchor);
28926
+ if (slugs.has(anchorSlug))
28927
+ continue;
28928
+ const match = findBestAnchorMatch(anchorSlug, slugs);
28929
+ if (!match)
28930
+ continue;
28931
+ const nextTarget = replaceAnchorInTarget(target, anchor, match.slug);
28932
+ if (nextTarget === target)
28933
+ continue;
28934
+ const lineLabel = line ? `${relFile}:${line}` : relFile;
28935
+ const description = `${lineLabel} #${anchor} → #${match.slug} (score ${match.score.toFixed(2)})`;
28936
+ if (urlStart !== undefined && urlEnd !== undefined && content3.slice(urlStart, urlEnd) === target) {
28937
+ pending.push({ urlStart, urlEnd, from: target, to: nextTarget, description });
28938
+ }
28939
+ }
28940
+ if (pending.length === 0)
28941
+ continue;
28942
+ const uniqueBySpan = new Map;
28943
+ for (const edit of pending) {
28944
+ uniqueBySpan.set(`${edit.urlStart}:${edit.urlEnd}:${edit.from}`, edit);
28945
+ }
28946
+ const uniquePending = [...uniqueBySpan.values()];
28947
+ uniquePending.sort((a, b) => b.urlStart - a.urlStart);
28948
+ let updated = content3;
28949
+ const descriptions = [];
28950
+ for (const edit of uniquePending) {
28951
+ if (updated.slice(edit.urlStart, edit.urlEnd) !== edit.from)
28952
+ continue;
28953
+ updated = updated.slice(0, edit.urlStart) + edit.to + updated.slice(edit.urlEnd);
28954
+ descriptions.push(edit.description);
28955
+ }
28956
+ if (updated === content3 || descriptions.length === 0)
28957
+ continue;
28958
+ editsByFile.set(filePath, { content: updated, descriptions });
28959
+ }
28960
+ const edits = [];
28961
+ for (const [absPath, { content: content3, descriptions }] of editsByFile) {
28962
+ edits.push({
28963
+ file: relPath(absPath, ctx.root),
28964
+ description: descriptions.join("; "),
28965
+ content: content3
28966
+ });
28967
+ }
28968
+ return edits;
28969
+ }
28970
+
28971
+ // src/audit/fix/doc-meta.ts
28972
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
28973
+ import { join as join8 } from "node:path";
28974
+
28975
+ // src/audit/core/git-meta.ts
28976
+ import { spawnSync } from "node:child_process";
28977
+ function lastGitCommitDate(relPath2, root2) {
28978
+ const proc = spawnSync("git", ["log", "-1", "--format=%cs", "--", relPath2], {
28979
+ cwd: root2,
28980
+ encoding: "utf8"
28981
+ });
28982
+ if (proc.status !== 0)
28983
+ return null;
28984
+ const date = proc.stdout.trim();
28985
+ return date || null;
28986
+ }
28987
+
28988
+ // src/audit/fix/doc-meta.ts
28989
+ function bumpDocMetaLastReviewed(content3, gitDate) {
28990
+ const reviewedStr = docMetaLastReviewed(content3);
28991
+ if (!reviewedStr)
28992
+ return null;
28993
+ const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
28994
+ const committed = new Date(`${gitDate}T00:00:00Z`);
28995
+ if (Number.isNaN(reviewed.getTime()) || Number.isNaN(committed.getTime()))
28996
+ return null;
28997
+ if (committed.getTime() <= reviewed.getTime())
28998
+ return null;
28999
+ return replaceDocMetaLastReviewed(content3, gitDate);
29000
+ }
29001
+ function collectDocMetaFixes(ctx) {
29002
+ const edits = [];
29003
+ for (const relPath2 of ctx.docMetaPaths) {
29004
+ const abs = join8(ctx.root, relPath2);
29005
+ if (!existsSync9(abs))
29006
+ continue;
29007
+ const content3 = readFileSync7(abs, "utf8");
29008
+ if (!DOC_META_RE.test(content3))
29009
+ continue;
29010
+ const reviewedStr = docMetaLastReviewed(content3);
29011
+ if (!reviewedStr)
29012
+ continue;
29013
+ const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
29014
+ if (Number.isNaN(reviewed.getTime()))
29015
+ continue;
29016
+ const gitDate = lastGitCommitDate(relPath2, ctx.root);
29017
+ if (!gitDate)
29018
+ continue;
29019
+ const updated = bumpDocMetaLastReviewed(content3, gitDate);
29020
+ if (!updated)
29021
+ continue;
29022
+ edits.push({
29023
+ file: relPath2,
29024
+ description: `last-reviewed ${reviewedStr} → ${gitDate}`,
29025
+ content: updated
29026
+ });
29027
+ }
29028
+ return edits;
29029
+ }
29030
+
29031
+ // src/audit/core/fix.ts
29032
+ function collectFixes(ctx, kinds) {
29033
+ const meta = kinds.has("doc-meta") ? collectDocMetaFixes(ctx) : [];
29034
+ const anchors = kinds.has("anchors") ? collectAnchorFixes(ctx) : [];
29035
+ return coalesceFixEdits(meta, anchors);
29036
+ }
29037
+ function coalesceFixEdits(metaEdits, anchorEdits) {
29038
+ const metaByFile = new Map(metaEdits.map((e) => [e.file, e]));
29039
+ const anchorByFile = new Map(anchorEdits.map((e) => [e.file, e]));
29040
+ const files = new Set([...metaByFile.keys(), ...anchorByFile.keys()]);
29041
+ const out = [];
29042
+ for (const file of [...files].sort()) {
29043
+ const meta = metaByFile.get(file);
29044
+ const anchors = anchorByFile.get(file);
29045
+ if (meta && anchors) {
29046
+ out.push({
29047
+ file,
29048
+ description: `${meta.description}; ${anchors.description}`,
29049
+ content: overlayLastReviewed(anchors.content, meta.content)
29050
+ });
29051
+ } else if (meta) {
29052
+ out.push(meta);
29053
+ } else if (anchors) {
29054
+ out.push(anchors);
29055
+ }
29056
+ }
29057
+ return out;
29058
+ }
29059
+ function overlayLastReviewed(targetContent, metaContent) {
29060
+ const date = docMetaLastReviewed(metaContent);
29061
+ if (!date)
29062
+ return targetContent;
29063
+ return replaceDocMetaLastReviewed(targetContent, date) ?? targetContent;
29064
+ }
29065
+ function underRoot(rootAbs, candidateAbs) {
29066
+ return candidateAbs === rootAbs || candidateAbs.startsWith(rootAbs + sep3);
29067
+ }
29068
+ function resolveWritePath(root2, relFile) {
29069
+ const rootResolved = resolve6(root2);
29070
+ const abs = resolve6(rootResolved, relFile);
29071
+ if (!underRoot(rootResolved, abs)) {
29072
+ throw new Error(`Refusing autofix outside repo root: ${relFile}`);
29073
+ }
29074
+ const rootReal = existsSync10(rootResolved) ? realpathSync4(rootResolved) : rootResolved;
29075
+ let cursor = abs;
29076
+ while (true) {
29077
+ if (existsSync10(cursor)) {
29078
+ const real = realpathSync4(cursor);
29079
+ if (!underRoot(rootReal, real)) {
29080
+ throw new Error(`Refusing autofix outside repo root: ${relFile}`);
29081
+ }
29082
+ return abs;
29083
+ }
29084
+ const parent = dirname6(cursor);
29085
+ if (parent === cursor || !underRoot(rootResolved, parent) && parent !== rootResolved) {
29086
+ return abs;
29087
+ }
29088
+ cursor = parent;
28496
29089
  }
28497
- if (rowEnd !== undefined) {
28498
- previousCell.end = Object.assign({}, getPoint(context.events, rowEnd));
28499
- map4.add(rowEnd, 0, [["exit", previousCell, context]]);
28500
- previousCell = undefined;
29090
+ }
29091
+ function applyFixes(ctx, options) {
29092
+ const kinds = new Set(options.kinds);
29093
+ const edits = collectFixes(ctx, kinds);
29094
+ const modifiedFiles = [];
29095
+ if (edits.length > 0) {
29096
+ console.error(`Doc audit autofix:
29097
+ `);
29098
+ for (const edit of edits) {
29099
+ console.error(`- ${edit.file}: ${edit.description}`);
29100
+ if (!options.dryRun) {
29101
+ const abs = resolveWritePath(ctx.root, edit.file);
29102
+ writeFileSync(abs, edit.content, "utf8");
29103
+ modifiedFiles.push(edit.file);
29104
+ }
29105
+ }
29106
+ console.error("");
28501
29107
  }
28502
- return previousCell;
29108
+ return { edits, modifiedFiles };
28503
29109
  }
28504
- function flushTableEnd(map4, context, index2, table, tableBody) {
28505
- const exits = [];
28506
- const related = getPoint(context.events, index2);
28507
- if (tableBody) {
28508
- tableBody.end = Object.assign({}, related);
28509
- exits.push(["exit", tableBody, context]);
29110
+ function parseFixKinds(raw) {
29111
+ if (raw === true)
29112
+ return ["doc-meta", "anchors"];
29113
+ switch (raw) {
29114
+ case "doc-meta":
29115
+ return ["doc-meta"];
29116
+ case "anchors":
29117
+ return ["anchors"];
29118
+ default:
29119
+ throw new Error(`Unknown --fix kind: ${raw}. Use doc-meta or anchors.`);
28510
29120
  }
28511
- table.end = Object.assign({}, related);
28512
- exits.push(["exit", table, context]);
28513
- map4.add(index2 + 1, 0, exits);
28514
29121
  }
28515
- function getPoint(events, index2) {
28516
- const event = events[index2];
28517
- const side = event[0] === "enter" ? "start" : "end";
28518
- return event[1][side];
29122
+ var FIX_KIND_RULE = {
29123
+ "doc-meta": "doc-meta",
29124
+ anchors: "links"
29125
+ };
29126
+ function fixKindsForOnly(kinds, only) {
29127
+ if (!only)
29128
+ return kinds;
29129
+ return kinds.filter((kind) => only.has(FIX_KIND_RULE[kind]));
28519
29130
  }
28520
- // node_modules/micromark-extension-gfm-task-list-item/dev/lib/syntax.js
28521
- var tasklistCheck = { name: "tasklistCheck", tokenize: tokenizeTasklistCheck };
28522
- function gfmTaskListItem() {
29131
+
29132
+ // src/audit/core/report.ts
29133
+ function issue(rule, file, message, opts) {
28523
29134
  return {
28524
- text: { [codes.leftSquareBracket]: tasklistCheck }
29135
+ rule,
29136
+ file,
29137
+ link: opts?.link,
29138
+ message,
29139
+ severity: opts?.severity ?? "error"
28525
29140
  };
28526
29141
  }
28527
- function tokenizeTasklistCheck(effects, ok3, nok) {
28528
- const self = this;
28529
- return open;
28530
- function open(code3) {
28531
- ok(code3 === codes.leftSquareBracket, "expected `[`");
28532
- if (self.previous !== codes.eof || !self._gfmTasklistFirstContentOfListItem) {
28533
- return nok(code3);
29142
+ function finalizeIssues(issues, strict) {
29143
+ if (!strict)
29144
+ return issues;
29145
+ return issues.map((i) => i.severity === "warning" ? { ...i, severity: "error" } : i);
29146
+ }
29147
+ function printReport(issues, options) {
29148
+ const finalized = finalizeIssues(issues, options.strict ?? false);
29149
+ const errors2 = finalized.filter((i) => i.severity === "error");
29150
+ const warnings = finalized.filter((i) => i.severity === "warning");
29151
+ const label = options.label ?? "Audit";
29152
+ if (options.json) {
29153
+ console.log(JSON.stringify({
29154
+ label,
29155
+ fileCount: options.fileCount,
29156
+ errors: errors2.length,
29157
+ warnings: warnings.length,
29158
+ issues: finalized
29159
+ }, null, 2));
29160
+ return errors2.length > 0 ? 1 : 0;
29161
+ }
29162
+ if (warnings.length > 0) {
29163
+ console.log(`${label} warnings:
29164
+ `);
29165
+ for (const i of warnings) {
29166
+ const linkPart = i.link ? ` (${i.link})` : "";
29167
+ console.log(`- ${i.file}${linkPart}: ${i.message}`);
28534
29168
  }
28535
- effects.enter("taskListCheck");
28536
- effects.enter("taskListCheckMarker");
28537
- effects.consume(code3);
28538
- effects.exit("taskListCheckMarker");
28539
- return inside;
29169
+ console.log("");
28540
29170
  }
28541
- function inside(code3) {
28542
- if (markdownLineEndingOrSpace(code3)) {
28543
- effects.enter("taskListCheckValueUnchecked");
28544
- effects.consume(code3);
28545
- effects.exit("taskListCheckValueUnchecked");
28546
- return close;
29171
+ if (errors2.length === 0) {
29172
+ const warnNote = warnings.length > 0 ? `, ${warnings.length} warning(s)` : "";
29173
+ const countNote = options.successSuffix ?? (options.fileCount !== undefined ? ` (${options.fileCount} files scanned${warnNote})` : "");
29174
+ console.log(`${label} passed${countNote}.`);
29175
+ return 0;
29176
+ }
29177
+ console.log(`${label} failed:
29178
+ `);
29179
+ for (const i of errors2) {
29180
+ const linkPart = i.link ? ` (${i.link})` : "";
29181
+ console.log(`- ${i.file}${linkPart}: ${i.message}`);
29182
+ }
29183
+ return 1;
29184
+ }
29185
+
29186
+ // src/references/check.ts
29187
+ import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
29188
+ import { join as join10, relative as relative7 } from "node:path";
29189
+
29190
+ // src/references/constants.ts
29191
+ var CANONICAL_REFS_DIR = ".skeleton/references";
29192
+ var GENERATED_MARKER_START = "<!-- skeleton: generated-reference";
29193
+ var GENERATED_MARKER_RE = /<!-- skeleton: generated-reference\s*\nsource: ([^\n]+)\s*\nredundancy: intentional\s*\n-->\s*\n?/;
29194
+ var SHARED_REF_LINK_RE = /\((?:\.\.\/)+references\/([^)]+)\)/g;
29195
+ function formatGeneratedHeader(sourceRelPath) {
29196
+ return `${GENERATED_MARKER_START}
29197
+ source: ${sourceRelPath}
29198
+ redundancy: intentional
29199
+ -->
29200
+
29201
+ `;
29202
+ }
29203
+ function stripGeneratedHeader(content3) {
29204
+ return content3.replace(GENERATED_MARKER_RE, "");
29205
+ }
29206
+ function isGeneratedReference(content3) {
29207
+ return content3.startsWith(GENERATED_MARKER_START);
29208
+ }
29209
+
29210
+ // src/references/discover.ts
29211
+ import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "node:fs";
29212
+ import { join as join9, relative as relative6 } from "node:path";
29213
+ function walkMarkdownFiles(dir, root2) {
29214
+ const files = [];
29215
+ if (!existsSync11(dir))
29216
+ return files;
29217
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
29218
+ if (entry.name.startsWith("."))
29219
+ continue;
29220
+ const fullPath = join9(dir, entry.name);
29221
+ if (entry.isDirectory()) {
29222
+ files.push(...walkMarkdownFiles(fullPath, root2));
29223
+ continue;
28547
29224
  }
28548
- if (code3 === codes.uppercaseX || code3 === codes.lowercaseX) {
28549
- effects.enter("taskListCheckValueChecked");
28550
- effects.consume(code3);
28551
- effects.exit("taskListCheckValueChecked");
28552
- return close;
29225
+ if (entry.name.endsWith(".md")) {
29226
+ files.push(normalizeRelPath(relative6(root2, fullPath)));
28553
29227
  }
28554
- return nok(code3);
28555
29228
  }
28556
- function close(code3) {
28557
- if (code3 === codes.rightSquareBracket) {
28558
- effects.enter("taskListCheckMarker");
28559
- effects.consume(code3);
28560
- effects.exit("taskListCheckMarker");
28561
- effects.exit("taskListCheck");
28562
- return after;
29229
+ return files;
29230
+ }
29231
+ function canonicalExists(root2, refPath) {
29232
+ return existsSync11(join9(root2, CANONICAL_REFS_DIR, refPath));
29233
+ }
29234
+ function findSharedRefLinks(content3, sourceFile) {
29235
+ const links = [];
29236
+ for (const match of content3.matchAll(SHARED_REF_LINK_RE)) {
29237
+ const refPath = match[1];
29238
+ if (!refPath)
29239
+ continue;
29240
+ links.push({ refPath: normalizeRelPath(refPath), sourceFile });
29241
+ }
29242
+ return links;
29243
+ }
29244
+ function findLocalCanonicalLinks(root2, content3, sourceFile) {
29245
+ const links = [];
29246
+ const localRefRe = /\((?:\.\/)?references\/([^)]+)\)/g;
29247
+ for (const match of content3.matchAll(localRefRe)) {
29248
+ const refPath = normalizeRelPath(match[1] ?? "");
29249
+ if (!refPath || !canonicalExists(root2, refPath))
29250
+ continue;
29251
+ links.push({ refPath, sourceFile });
29252
+ }
29253
+ const inReferencesDir = /\/references\//.test(sourceFile);
29254
+ if (inReferencesDir) {
29255
+ const refsIdx = sourceFile.lastIndexOf("/references/");
29256
+ const withinRefs = sourceFile.slice(refsIdx + "/references/".length);
29257
+ const withinDir = withinRefs.includes("/") ? withinRefs.slice(0, withinRefs.lastIndexOf("/")) : "";
29258
+ const siblingRe = /\((?!https?:|#|\.\.\/)([a-z0-9./_-]+\.md)\)/gi;
29259
+ for (const match of content3.matchAll(siblingRe)) {
29260
+ const raw = normalizeRelPath(match[1] ?? "");
29261
+ if (!raw)
29262
+ continue;
29263
+ const refPath = withinDir ? normalizeRelPath(join9(withinDir, raw)) : raw;
29264
+ if (!canonicalExists(root2, refPath))
29265
+ continue;
29266
+ links.push({ refPath, sourceFile });
28563
29267
  }
28564
- return nok(code3);
28565
29268
  }
28566
- function after(code3) {
28567
- if (markdownLineEnding(code3)) {
28568
- return ok3(code3);
29269
+ return links;
29270
+ }
29271
+ function discoverSkillReferencePlans(root2) {
29272
+ const index2 = buildSkillIndex(root2);
29273
+ const plans = [];
29274
+ for (const slug2 of index2.slugs) {
29275
+ const skillDir = join9(root2, slug2);
29276
+ if (!existsSync11(join9(skillDir, "SKILL.md")))
29277
+ continue;
29278
+ const refPaths = new Set;
29279
+ const links = [];
29280
+ for (const relFile of walkMarkdownFiles(skillDir, root2)) {
29281
+ const content3 = readFileSync8(join9(root2, relFile), "utf8");
29282
+ if (isGeneratedReference(content3))
29283
+ continue;
29284
+ for (const link2 of findSharedRefLinks(content3, relFile)) {
29285
+ refPaths.add(link2.refPath);
29286
+ links.push(link2);
29287
+ }
29288
+ for (const link2 of findLocalCanonicalLinks(root2, content3, relFile)) {
29289
+ refPaths.add(link2.refPath);
29290
+ links.push(link2);
29291
+ }
28569
29292
  }
28570
- if (markdownSpace(code3)) {
28571
- return effects.check({ tokenize: spaceThenNonSpace }, ok3, nok)(code3);
29293
+ const queue = [...refPaths];
29294
+ while (queue.length > 0) {
29295
+ const refPath = queue.pop();
29296
+ if (!refPath || !canonicalExists(root2, refPath))
29297
+ continue;
29298
+ const canonicalContent = readFileSync8(join9(root2, CANONICAL_REFS_DIR, refPath), "utf8");
29299
+ const syntheticSource = generatedRefPath(slug2, refPath);
29300
+ for (const link2 of findLocalCanonicalLinks(root2, canonicalContent, syntheticSource)) {
29301
+ if (refPaths.has(link2.refPath))
29302
+ continue;
29303
+ refPaths.add(link2.refPath);
29304
+ links.push(link2);
29305
+ queue.push(link2.refPath);
29306
+ }
29307
+ }
29308
+ if (refPaths.size > 0) {
29309
+ plans.push({ skill: slug2, refPaths, links });
28572
29310
  }
28573
- return nok(code3);
28574
29311
  }
29312
+ return plans.sort((a, b) => a.skill.localeCompare(b.skill));
28575
29313
  }
28576
- function spaceThenNonSpace(effects, ok3, nok) {
28577
- return factorySpace(effects, after, types.whitespace);
28578
- function after(code3) {
28579
- return code3 === codes.eof ? nok(code3) : ok3(code3);
29314
+ function generatedRefPath(skill, refPath) {
29315
+ return normalizeRelPath(join9(skill, "references", refPath));
29316
+ }
29317
+ function rewriteSharedRefTarget(sourceFile, skill, refPath) {
29318
+ const sourceDir = sourceFile.slice(0, sourceFile.lastIndexOf("/"));
29319
+ const target = generatedRefPath(skill, refPath);
29320
+ if (!sourceDir)
29321
+ return target;
29322
+ const fromParts = sourceDir.split("/");
29323
+ const toParts = target.split("/");
29324
+ let i = 0;
29325
+ while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) {
29326
+ i++;
28580
29327
  }
29328
+ const ups = fromParts.length - i;
29329
+ const down = toParts.slice(i);
29330
+ const rel = [...Array(ups).fill(".."), ...down].join("/");
29331
+ return rel || (toParts.at(-1) ?? refPath);
28581
29332
  }
28582
- // node_modules/micromark-extension-gfm/index.js
28583
- function gfm(options) {
28584
- return combineExtensions([
28585
- gfmAutolinkLiteral(),
28586
- gfmFootnote(),
28587
- gfmStrikethrough(options),
28588
- gfmTable(),
28589
- gfmTaskListItem()
28590
- ]);
29333
+ function rewriteSharedRefLinks(content3, sourceFile, skill) {
29334
+ return content3.replace(SHARED_REF_LINK_RE, (_match, refPath) => {
29335
+ const rewritten = rewriteSharedRefTarget(sourceFile, skill, normalizeRelPath(refPath));
29336
+ return `(${rewritten})`;
29337
+ });
28591
29338
  }
28592
29339
 
28593
- // node_modules/remark-gfm/lib/index.js
28594
- var emptyOptions2 = {};
28595
- function remarkGfm(options) {
28596
- const self = this;
28597
- const settings = options || emptyOptions2;
28598
- const data = self.data();
28599
- const micromarkExtensions = data.micromarkExtensions || (data.micromarkExtensions = []);
28600
- const fromMarkdownExtensions = data.fromMarkdownExtensions || (data.fromMarkdownExtensions = []);
28601
- const toMarkdownExtensions = data.toMarkdownExtensions || (data.toMarkdownExtensions = []);
28602
- micromarkExtensions.push(gfm(settings));
28603
- fromMarkdownExtensions.push(gfmFromMarkdown());
28604
- toMarkdownExtensions.push(gfmToMarkdown(settings));
28605
- }
28606
- // src/audit/core/markdown.ts
28607
- var MARKDOWN_LINK_RE = /(?<!!)\[([^\]]*)\]\(([^)]+)\)/g;
28608
- var REFERENCE_DEF_RE = /^\[([^\]]+)\]:\s+(\S+)/;
28609
- var HEADING_LINE_RE = /^(#{1,6})\s+(.+)$/;
28610
- var processor = remark().use(remarkGfm);
28611
- function lineFromOffset(content3, offset) {
28612
- if (offset === undefined)
28613
- return;
28614
- return content3.slice(0, offset).split(`
28615
- `).length;
29340
+ // src/references/check.ts
29341
+ function listAllGeneratedFiles(root2) {
29342
+ const files = [];
29343
+ const walk = (dir) => {
29344
+ if (!existsSync12(dir))
29345
+ return;
29346
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
29347
+ if (entry.name.startsWith("."))
29348
+ continue;
29349
+ const fullPath = join10(dir, entry.name);
29350
+ if (entry.isDirectory()) {
29351
+ walk(fullPath);
29352
+ continue;
29353
+ }
29354
+ if (!entry.name.endsWith(".md"))
29355
+ continue;
29356
+ const content3 = readFileSync9(fullPath, "utf8");
29357
+ if (isGeneratedReference(content3)) {
29358
+ files.push(normalizeRelPath(relative7(root2, fullPath)));
29359
+ }
29360
+ }
29361
+ };
29362
+ walk(root2);
29363
+ return files;
28616
29364
  }
28617
- function extractLinksFromMarkdown(content3, filePath) {
28618
- if (filePath.endsWith(".mdc")) {
28619
- return extractLinksRegex(content3);
28620
- }
28621
- const tree = processor.parse(content3);
28622
- const refDefs = new Map;
28623
- const links = [];
28624
- for (const line of content3.split(`
28625
- `)) {
28626
- const match = REFERENCE_DEF_RE.exec(line.trim());
28627
- if (match?.[1] && match[2]) {
28628
- refDefs.set(match[1].toLowerCase(), match[2]);
29365
+ function runGeneratedReferencesCheck(root2) {
29366
+ const issues = [];
29367
+ const canonicalDir = join10(root2, CANONICAL_REFS_DIR);
29368
+ if (!existsSync12(canonicalDir))
29369
+ return issues;
29370
+ const plans = discoverSkillReferencePlans(root2);
29371
+ const needed = new Set;
29372
+ for (const plan of plans) {
29373
+ for (const refPath of plan.refPaths) {
29374
+ needed.add(generatedRefPath(plan.skill, refPath));
28629
29375
  }
28630
29376
  }
28631
- visit2(tree, (node2) => {
28632
- if (node2.type === "link" && "url" in node2 && typeof node2.url === "string") {
28633
- links.push({
28634
- target: node2.url.trim(),
28635
- line: lineFromOffset(content3, node2.position?.start.offset)
28636
- });
29377
+ for (const targetRel of needed) {
29378
+ const targetPath = join10(root2, targetRel);
29379
+ if (!existsSync12(targetPath)) {
29380
+ issues.push(issue("generated-references", targetRel, "missing generated copy — run skeleton references sync"));
29381
+ continue;
28637
29382
  }
28638
- if (node2.type === "linkReference" && "identifier" in node2) {
28639
- const id = String(node2.identifier).toLowerCase();
28640
- const url = refDefs.get(id);
28641
- if (url) {
28642
- links.push({
28643
- target: url.trim(),
28644
- line: lineFromOffset(content3, node2.position?.start.offset)
28645
- });
28646
- }
29383
+ const generated = readFileSync9(targetPath, "utf8");
29384
+ if (!isGeneratedReference(generated)) {
29385
+ issues.push(issue("generated-references", targetRel, "expected generated-reference provenance header"));
29386
+ continue;
28647
29387
  }
28648
- });
28649
- return links;
28650
- }
28651
- function extractLinksRegex(content3) {
28652
- const links = [];
28653
- const lines = content3.split(`
28654
- `);
28655
- for (let i = 0;i < lines.length; i++) {
28656
- const line = lines[i] ?? "";
28657
- for (const match of line.matchAll(MARKDOWN_LINK_RE)) {
28658
- const target = match[2]?.trim();
28659
- if (target)
28660
- links.push({ target, line: i + 1 });
29388
+ const body = stripGeneratedHeader(generated);
29389
+ const sourceRel = normalizeRelPath(generated.match(/source: ([^\n]+)/)?.[1] ?? join10(CANONICAL_REFS_DIR, targetRel.split("/references/")[1] ?? ""));
29390
+ const canonicalPath = join10(root2, sourceRel);
29391
+ if (!existsSync12(canonicalPath)) {
29392
+ issues.push(issue("generated-references", targetRel, `canonical source missing: ${sourceRel}`));
29393
+ continue;
29394
+ }
29395
+ const canonical = readFileSync9(canonicalPath, "utf8");
29396
+ if (body !== canonical) {
29397
+ issues.push(issue("generated-references", targetRel, "stale generated copy — run skeleton references sync"));
28661
29398
  }
28662
29399
  }
28663
- return links;
28664
- }
28665
- function extractHeadingSlugs(content3, filePath) {
28666
- if (filePath.endsWith(".mdc")) {
28667
- return extractHeadingSlugsLineBased(content3);
28668
- }
28669
- const slugger = new BananaSlug;
28670
- const slugs = new Set;
28671
- const tree = processor.parse(content3);
28672
- visit2(tree, (node2) => {
28673
- if (node2.type === "heading" && "children" in node2) {
28674
- const text5 = node2.children.filter((c) => c.type === "text" || c.type === "inlineCode").map((c) => ("value" in c) ? String(c.value) : "").join("");
28675
- if (text5)
28676
- slugs.add(slugger.slug(text5));
29400
+ for (const generatedRel of listAllGeneratedFiles(root2)) {
29401
+ if (!needed.has(generatedRel)) {
29402
+ issues.push(issue("generated-references", generatedRel, "orphaned generated copy — run skeleton references sync"));
28677
29403
  }
28678
- });
28679
- return slugs;
29404
+ }
29405
+ for (const plan of plans) {
29406
+ const skillDir = join10(root2, plan.skill);
29407
+ if (!existsSync12(skillDir))
29408
+ continue;
29409
+ const walk = (dir) => {
29410
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
29411
+ if (entry.name.startsWith("."))
29412
+ continue;
29413
+ const fullPath = join10(dir, entry.name);
29414
+ if (entry.isDirectory()) {
29415
+ walk(fullPath);
29416
+ continue;
29417
+ }
29418
+ if (!entry.name.endsWith(".md"))
29419
+ continue;
29420
+ const relFile = normalizeRelPath(relative7(root2, fullPath));
29421
+ const content3 = readFileSync9(fullPath, "utf8");
29422
+ if (content3.match(SHARED_REF_LINK_RE)) {
29423
+ issues.push(issue("generated-references", relFile, "still links to shared root references/ — run skeleton references sync"));
29424
+ }
29425
+ }
29426
+ };
29427
+ walk(skillDir);
29428
+ }
29429
+ return issues;
28680
29430
  }
28681
- function extractHeadingSlugsLineBased(content3) {
28682
- const slugger = new BananaSlug;
28683
- const slugs = new Set;
28684
- for (const line of content3.split(`
28685
- `)) {
28686
- const match = HEADING_LINE_RE.exec(line);
28687
- if (match?.[2])
28688
- slugs.add(slugger.slug(match[2]));
29431
+ function runGeneratedReferencesRule(ctx) {
29432
+ return runGeneratedReferencesCheck(ctx.root);
29433
+ }
29434
+ var generatedReferencesRule = {
29435
+ id: "generated-references",
29436
+ global: true,
29437
+ run: runGeneratedReferencesRule
29438
+ };
29439
+
29440
+ // src/audit/rules/banned.ts
29441
+ function runBannedRule(ctx) {
29442
+ const issues = [];
29443
+ for (const abs of collectBannedFiles(ctx.config, ctx.root)) {
29444
+ const rel = relPath(abs, ctx.root);
29445
+ issues.push(issue("banned", rel, "file matches scan.banned — must not exist in repo"));
28689
29446
  }
28690
- return slugs;
29447
+ return issues;
28691
29448
  }
28692
- function slugifyAnchor(anchor) {
28693
- const slugger = new BananaSlug;
28694
- return slugger.slug(decodeURIComponent(anchor));
29449
+ var bannedRule = { id: "banned", run: runBannedRule };
29450
+
29451
+ // src/audit/rules/doc-meta.ts
29452
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
29453
+ import { join as join11 } from "node:path";
29454
+ function runDocMetaRule(ctx) {
29455
+ const issues = [];
29456
+ const today = new Date;
29457
+ for (const relPath2 of ctx.docMetaPaths) {
29458
+ const abs = join11(ctx.root, relPath2);
29459
+ if (!existsSync13(abs))
29460
+ continue;
29461
+ const content3 = readFileSync10(abs, "utf8");
29462
+ if (!DOC_META_RE.test(content3)) {
29463
+ issues.push(issue("doc-meta", relPath2, "missing doc-meta comment (owner + last-reviewed)"));
29464
+ continue;
29465
+ }
29466
+ const reviewedStr = docMetaLastReviewed(content3);
29467
+ if (!reviewedStr)
29468
+ continue;
29469
+ const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
29470
+ if (Number.isNaN(reviewed.getTime()))
29471
+ continue;
29472
+ const ageDays = (today.getTime() - reviewed.getTime()) / 86400000;
29473
+ if (ageDays > ctx.config.daysUntilStale) {
29474
+ issues.push(issue("doc-meta", relPath2, `doc-meta last-reviewed ${reviewedStr} is stale (>${ctx.config.daysUntilStale} days)`, { severity: "warning" }));
29475
+ }
29476
+ const gitDate = lastGitCommitDate(relPath2, ctx.root);
29477
+ if (!gitDate)
29478
+ continue;
29479
+ const committed = new Date(`${gitDate}T00:00:00Z`);
29480
+ if (Number.isNaN(committed.getTime()))
29481
+ continue;
29482
+ if (committed.getTime() > reviewed.getTime()) {
29483
+ issues.push(issue("doc-meta", relPath2, `content changed after last-reviewed ${reviewedStr} (git: ${gitDate}) — bump last-reviewed or confirm review`, { severity: "warning" }));
29484
+ }
29485
+ }
29486
+ return issues;
28695
29487
  }
29488
+ var docMetaRule = { id: "doc-meta", run: runDocMetaRule };
28696
29489
 
28697
29490
  // src/audit/rules/links.ts
28698
- function resolveLink(sourceFile, target) {
29491
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
29492
+ import { dirname as dirname7, resolve as resolve7 } from "node:path";
29493
+ function resolveLink2(sourceFile, target) {
28699
29494
  const withoutAnchor = target.split("#")[0]?.split("?")[0] ?? "";
28700
29495
  if (!withoutAnchor)
28701
29496
  return sourceFile;
28702
- return resolve4(dirname3(sourceFile), withoutAnchor);
29497
+ return resolve7(dirname7(sourceFile), withoutAnchor);
28703
29498
  }
28704
29499
  function validateTarget(ctx, sourceFile, target, linkLabel) {
28705
29500
  const issues = [];
@@ -28710,7 +29505,7 @@ function validateTarget(ctx, sourceFile, target, linkLabel) {
28710
29505
  const relSource = relPath(sourceFile, ctx.root);
28711
29506
  const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
28712
29507
  const pathPart = target.split("#")[0]?.split("?")[0] ?? "";
28713
- const resolved = resolveLink(sourceFile, target);
29508
+ const resolved = resolveLink2(sourceFile, target);
28714
29509
  const relTarget = relPath(resolved, ctx.root);
28715
29510
  const skillMatch = SKILL_LINK_IN_TARGET_RE.exec(target);
28716
29511
  if (skillMatch?.[1] && ctx.retiredSkills.has(skillMatch[1])) {
@@ -28730,19 +29525,19 @@ function validateTarget(ctx, sourceFile, target, linkLabel) {
28730
29525
  }
28731
29526
  if ((target.includes(".claude/agents/") || target.includes(".cursor/agents/")) && target.endsWith(".md")) {
28732
29527
  const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
28733
- if (!existsSync8(agentPath)) {
29528
+ if (!existsSync14(agentPath)) {
28734
29529
  issues.push(issue("links", relSource, "missing agent file", { link: linkLabel }));
28735
29530
  }
28736
29531
  return issues;
28737
29532
  }
28738
- if (pathPart && !existsSync8(resolved)) {
29533
+ if (pathPart && !existsSync14(resolved)) {
28739
29534
  issues.push(issue("links", relSource, `broken link → ${relTarget}`, {
28740
29535
  link: linkLabel
28741
29536
  }));
28742
29537
  return issues;
28743
29538
  }
28744
- if (anchor && existsSync8(resolved)) {
28745
- const targetContent = readFileSync7(resolved, "utf8");
29539
+ if (anchor && existsSync14(resolved)) {
29540
+ const targetContent = readFileSync11(resolved, "utf8");
28746
29541
  const slugs = extractHeadingSlugs(targetContent, resolved);
28747
29542
  const anchorSlug = slugifyAnchor(anchor);
28748
29543
  if (!slugs.has(anchorSlug)) {
@@ -28767,9 +29562,58 @@ function runLinksRule(ctx) {
28767
29562
  }
28768
29563
  var linksRule = { id: "links", run: runLinksRule };
28769
29564
 
29565
+ // src/audit/rules/prose-policy.ts
29566
+ function runProsePolicyRule(ctx) {
29567
+ if (ctx.policies.length === 0)
29568
+ return [];
29569
+ const issues = [];
29570
+ const draftPrefixes = ctx.config.draftPathPrefixes ?? [];
29571
+ for (const filePath of ctx.files) {
29572
+ const rel = relPath(filePath, ctx.root);
29573
+ const content3 = readFileContent(filePath);
29574
+ const lines = content3.split(`
29575
+ `);
29576
+ const policies = policiesForFile(ctx.policies, rel);
29577
+ for (const entry of policies) {
29578
+ if (entry.mode === "fingerprint")
29579
+ continue;
29580
+ if (!entry.regex)
29581
+ continue;
29582
+ if (entry.id === "draft-marker") {
29583
+ for (let i = 0;i < lines.length; i++) {
29584
+ if (entry.regex.test(lines[i] ?? "") && !isDraftPlacementAllowed(rel, draftPrefixes)) {
29585
+ issues.push(issue("prose-policy", rel, entry.message, {
29586
+ link: `line ${i + 1}`,
29587
+ severity: entry.severity
29588
+ }));
29589
+ }
29590
+ }
29591
+ continue;
29592
+ }
29593
+ const isMultiline = entry.pattern?.includes("[\\s\\S]");
29594
+ if (isMultiline) {
29595
+ if (entry.regex.test(content3)) {
29596
+ issues.push(issue("prose-policy", rel, entry.message, { severity: entry.severity }));
29597
+ }
29598
+ continue;
29599
+ }
29600
+ for (let i = 0;i < lines.length; i++) {
29601
+ if (entry.regex.test(lines[i] ?? "")) {
29602
+ issues.push(issue("prose-policy", rel, entry.message, {
29603
+ link: `line ${i + 1}`,
29604
+ severity: entry.severity
29605
+ }));
29606
+ }
29607
+ }
29608
+ }
29609
+ }
29610
+ return issues;
29611
+ }
29612
+ var prosePolicyRule = { id: "prose-policy", run: runProsePolicyRule };
29613
+
28770
29614
  // src/audit/rules/registry.ts
28771
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
28772
- import { join as join9 } from "node:path";
29615
+ import { existsSync as existsSync15, readFileSync as readFileSync12 } from "node:fs";
29616
+ import { join as join12 } from "node:path";
28773
29617
  function runRegistryRule(ctx) {
28774
29618
  const issues = [];
28775
29619
  const registry = new Set(ctx.registryPaths);
@@ -28777,12 +29621,12 @@ function runRegistryRule(ctx) {
28777
29621
  issues.push(issue("registry", REGISTRY_REL_PATH, "registry table header found but 0 rows parsed — check | Topic | Canonical file | format and link syntax"));
28778
29622
  }
28779
29623
  for (const rel of ctx.registryPaths) {
28780
- const abs = join9(ctx.root, rel);
28781
- if (!existsSync9(abs)) {
29624
+ const abs = join12(ctx.root, rel);
29625
+ if (!existsSync15(abs)) {
28782
29626
  issues.push(issue("registry", rel, "registry entry file missing"));
28783
29627
  continue;
28784
29628
  }
28785
- const content3 = readFileSync8(abs, "utf8");
29629
+ const content3 = readFileSync12(abs, "utf8");
28786
29630
  if (!SOURCE_OF_TRUTH_BANNER_RE.test(content3)) {
28787
29631
  issues.push(issue("registry", rel, "missing **Source of truth for** banner (required for registry entry)"));
28788
29632
  }
@@ -28793,7 +29637,7 @@ function runRegistryRule(ctx) {
28793
29637
  continue;
28794
29638
  if (!rel.endsWith(".md") && !rel.endsWith(".mdc"))
28795
29639
  continue;
28796
- const content3 = readFileSync8(filePath, "utf8");
29640
+ const content3 = readFileSync12(filePath, "utf8");
28797
29641
  if (!SOURCE_OF_TRUTH_BANNER_LINE_RE.test(content3))
28798
29642
  continue;
28799
29643
  if (!registry.has(rel)) {
@@ -28830,16 +29674,16 @@ function runScanRootsRule(ctx) {
28830
29674
  var scanRootsRule = { id: "scan-roots", run: runScanRootsRule };
28831
29675
 
28832
29676
  // src/audit/rules/skill-index.ts
28833
- import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync9 } from "node:fs";
28834
- import { join as join10, relative as relative7 } from "node:path";
29677
+ import { existsSync as existsSync16, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
29678
+ import { join as join13, relative as relative8 } from "node:path";
28835
29679
  function walkSkillMarkdown(dir) {
28836
29680
  const files = [];
28837
- if (!existsSync10(dir))
29681
+ if (!existsSync16(dir))
28838
29682
  return files;
28839
29683
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
28840
29684
  if (entry.name.startsWith("."))
28841
29685
  continue;
28842
- const fullPath = join10(dir, entry.name);
29686
+ const fullPath = join13(dir, entry.name);
28843
29687
  if (entry.isDirectory()) {
28844
29688
  files.push(...walkSkillMarkdown(fullPath));
28845
29689
  continue;
@@ -28865,8 +29709,8 @@ function parseReadmeTaxonomySlugs(content3) {
28865
29709
  }
28866
29710
  function scanFileForSkillLinks(ctx, filePath, index2) {
28867
29711
  const issues = [];
28868
- const rel = relative7(ctx.root, filePath).replace(/\\/g, "/");
28869
- const content3 = readFileSync9(filePath, "utf8");
29712
+ const rel = relative8(ctx.root, filePath).replace(/\\/g, "/");
29713
+ const content3 = readFileSync13(filePath, "utf8");
28870
29714
  if (isGeneratedReference(content3))
28871
29715
  return issues;
28872
29716
  for (const match of content3.matchAll(SKILL_LINK_RE)) {
@@ -28889,14 +29733,14 @@ function validateReadmeTaxonomy(ctx, index2, diskSlugs) {
28889
29733
  for (const skillRoot of index2.roots) {
28890
29734
  if (skillRoot.kind !== "nested")
28891
29735
  continue;
28892
- const readmePath = join10(ctx.root, skillRoot.relPath, "README.md");
28893
- if (!existsSync10(readmePath))
29736
+ const readmePath = join13(ctx.root, skillRoot.relPath, "README.md");
29737
+ if (!existsSync16(readmePath))
28894
29738
  continue;
28895
- const readme = readFileSync9(readmePath, "utf8");
29739
+ const readme = readFileSync13(readmePath, "utf8");
28896
29740
  if (!readme.includes("## Taxonomy"))
28897
29741
  continue;
28898
29742
  const taxonomySlugs = parseReadmeTaxonomySlugs(readme);
28899
- const nestedSlugs = diskSlugs.filter((slug2) => existsSync10(join10(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
29743
+ const nestedSlugs = diskSlugs.filter((slug2) => existsSync16(join13(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
28900
29744
  const publicSlugs = nestedSlugs.filter((slug2) => !nonPublic.has(slug2));
28901
29745
  const relReadme = `${skillRoot.relPath}/README.md`;
28902
29746
  for (const slug2 of publicSlugs) {
@@ -28918,16 +29762,16 @@ function runSkillIndexRule(ctx) {
28918
29762
  const diskSlugs = listSkillSlugs(index2);
28919
29763
  issues.push(...validateReadmeTaxonomy(ctx, index2, diskSlugs));
28920
29764
  for (const skillRoot of index2.roots) {
28921
- const scanRoot = skillRoot.kind === "nested" ? join10(ctx.root, skillRoot.relPath) : join10(ctx.root);
28922
- if (skillRoot.kind === "nested" && existsSync10(scanRoot)) {
29765
+ const scanRoot = skillRoot.kind === "nested" ? join13(ctx.root, skillRoot.relPath) : join13(ctx.root);
29766
+ if (skillRoot.kind === "nested" && existsSync16(scanRoot)) {
28923
29767
  for (const skillMd of walkSkillMarkdown(scanRoot)) {
28924
29768
  issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
28925
29769
  }
28926
29770
  }
28927
29771
  if (skillRoot.kind === "flat") {
28928
29772
  for (const slug2 of index2.slugs) {
28929
- const skillDir = join10(ctx.root, slug2);
28930
- if (existsSync10(skillDir)) {
29773
+ const skillDir = join13(ctx.root, slug2);
29774
+ if (existsSync16(skillDir)) {
28931
29775
  for (const skillMd of walkSkillMarkdown(skillDir)) {
28932
29776
  issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
28933
29777
  }
@@ -28949,21 +29793,54 @@ var docsRules = [
28949
29793
  { ...coverageGapsRule, global: true },
28950
29794
  linksRule,
28951
29795
  docMetaRule,
28952
- { ...bannedRule, global: true }
29796
+ { ...bannedRule, global: true },
29797
+ prosePolicyRule
28953
29798
  ];
28954
29799
  var skillsRules = [
28955
29800
  { ...skillIndexRule, global: true },
28956
- { ...generatedReferencesRule, global: true }
29801
+ { ...generatedReferencesRule, global: true },
29802
+ prosePolicyRule
28957
29803
  ];
28958
29804
  var allRules = [...docsRules, ...skillsRules];
28959
- function rulesForSuite(suite) {
29805
+ function assembleRules(pluginRules = []) {
29806
+ const coreIds = new Set(allRules.map((rule) => rule.id));
29807
+ const seenPlugin = new Set;
29808
+ for (const rule of pluginRules) {
29809
+ if (seenPlugin.has(rule.id) || coreIds.has(rule.id)) {
29810
+ throw new Error(`Duplicate audit rule id: ${rule.id}`);
29811
+ }
29812
+ seenPlugin.add(rule.id);
29813
+ }
29814
+ const docs = [...docsRules];
29815
+ const skills = [...skillsRules];
29816
+ for (const rule of pluginRules) {
29817
+ const suites = rule.suites ?? ["docs"];
29818
+ const inDocs = suites.includes("docs");
29819
+ const inSkills = suites.includes("skills");
29820
+ if (!inDocs && !inSkills) {
29821
+ const listed = suites.length === 0 ? "(empty)" : suites.join(", ");
29822
+ throw new Error(`Plugin rule "${rule.id}" suites attach to no known suite (got ${listed}; allowed: docs, skills). ` + `"self" is the union of docs+skills — put the rule in docs and/or skills.`);
29823
+ }
29824
+ if (inDocs)
29825
+ docs.push(rule);
29826
+ if (inSkills)
29827
+ skills.push(rule);
29828
+ }
29829
+ const selfById = new Map;
29830
+ for (const rule of [...docs, ...skills]) {
29831
+ selfById.set(rule.id, rule);
29832
+ }
29833
+ return { docs, skills, self: [...selfById.values()] };
29834
+ }
29835
+ function rulesForSuite(suite, pluginRules = []) {
29836
+ const assembled = assembleRules(pluginRules);
28960
29837
  switch (suite) {
28961
29838
  case "docs":
28962
- return docsRules;
29839
+ return assembled.docs;
28963
29840
  case "skills":
28964
- return skillsRules;
29841
+ return assembled.skills;
28965
29842
  case "self":
28966
- return allRules;
29843
+ return assembled.self;
28967
29844
  default:
28968
29845
  throw new Error(`Unknown suite: ${suite}`);
28969
29846
  }
@@ -28974,22 +29851,42 @@ function parseAuditArgs(argv) {
28974
29851
  let suite = "docs";
28975
29852
  let strict = false;
28976
29853
  let json = false;
29854
+ let dryRun = false;
28977
29855
  let paths = [];
28978
29856
  let only = null;
28979
- for (const arg of argv) {
29857
+ let fix = null;
29858
+ for (let i = 0;i < argv.length; i++) {
29859
+ const arg = argv[i] ?? "";
28980
29860
  if (arg.startsWith("--suite=")) {
28981
29861
  suite = arg.slice("--suite=".length);
28982
29862
  } else if (arg === "--strict") {
28983
29863
  strict = true;
28984
29864
  } else if (arg === "--json") {
28985
29865
  json = true;
29866
+ } else if (arg === "--dry-run") {
29867
+ dryRun = true;
29868
+ } else if (arg.startsWith("--dry-run=")) {
29869
+ throw new Error("audit: use --dry-run (boolean flag), not --dry-run=<value>");
29870
+ } else if (arg === "--fix") {
29871
+ const next = argv[i + 1];
29872
+ if (next && !next.startsWith("-")) {
29873
+ if (next !== "doc-meta" && next !== "anchors") {
29874
+ throw new Error(`Unknown --fix kind: ${next}. Use --fix, --fix=doc-meta, or --fix=anchors.`);
29875
+ }
29876
+ fix = next;
29877
+ i++;
29878
+ } else {
29879
+ fix = true;
29880
+ }
29881
+ } else if (arg.startsWith("--fix=")) {
29882
+ fix = arg.slice("--fix=".length);
28986
29883
  } else if (arg.startsWith("--paths=")) {
28987
29884
  paths = arg.slice("--paths=".length).split(",").map((path2) => path2.trim()).filter(Boolean);
28988
29885
  } else if (arg.startsWith("--only=")) {
28989
29886
  only = new Set(arg.slice("--only=".length).split(",").filter(Boolean));
28990
29887
  }
28991
29888
  }
28992
- return { suite, strict, json, paths, only };
29889
+ return { suite, strict, json, paths, only, fix, dryRun };
28993
29890
  }
28994
29891
  function labelForSuite(suite) {
28995
29892
  switch (suite) {
@@ -29012,16 +29909,40 @@ function shouldRunRule(rule, options, pathScoped) {
29012
29909
  return false;
29013
29910
  return true;
29014
29911
  }
29015
- function runAudit(options) {
29016
- const ctx = createContext({
29912
+ async function runAudit(options) {
29913
+ const pathScoped = options.paths.length > 0;
29914
+ const base = createContext({
29017
29915
  root: options.root,
29018
- paths: options.paths.length > 0 ? options.paths : undefined
29916
+ paths: pathScoped ? options.paths : undefined,
29917
+ includeExcludedSkillTrees: options.suite === "skills" && !pathScoped
29019
29918
  });
29020
- const rules = rulesForSuite(options.suite).filter((r) => !options.only || options.only.has(r.id));
29021
- const pathScoped = options.paths.length > 0 && !options.globalOnly;
29919
+ const loaded = await loadPlugins(base.root, base.config);
29920
+ const ctx = { ...base, policies: loaded.policies };
29921
+ if (options.fix !== null && options.fix !== undefined) {
29922
+ if (options.suite !== "docs") {
29923
+ console.error("--fix is supported only for audit docs");
29924
+ return 1;
29925
+ }
29926
+ const kinds = fixKindsForOnly(parseFixKinds(options.fix), options.only);
29927
+ if (kinds.length === 0) {
29928
+ console.error("--fix has no overlapping rules with --only (doc-meta → doc-meta, anchors → links).");
29929
+ return 1;
29930
+ }
29931
+ applyFixes(ctx, { kinds, dryRun: options.dryRun });
29932
+ if (!options.dryRun) {
29933
+ const refreshed = createContext({
29934
+ root: options.root,
29935
+ paths: options.paths.length > 0 ? options.paths : undefined,
29936
+ policies: loaded.policies
29937
+ });
29938
+ Object.assign(ctx, refreshed);
29939
+ }
29940
+ }
29941
+ const rules = rulesForSuite(options.suite, loaded.rules).filter((r) => !options.only || options.only.has(r.id));
29942
+ const skipGlobalsForPaths = pathScoped && !options.globalOnly;
29022
29943
  const issues = [];
29023
29944
  for (const rule of rules) {
29024
- if (!shouldRunRule(rule, options, pathScoped))
29945
+ if (!shouldRunRule(rule, options, skipGlobalsForPaths))
29025
29946
  continue;
29026
29947
  issues.push(...rule.run(ctx));
29027
29948
  }
@@ -29036,19 +29957,19 @@ function runAudit(options) {
29036
29957
  }
29037
29958
 
29038
29959
  // src/customize/resolve.ts
29039
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
29040
- import { basename as basename2, join as join11, relative as relative8 } from "node:path";
29960
+ import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
29961
+ import { basename as basename3, join as join14, relative as relative9 } from "node:path";
29041
29962
  var CUSTOMIZE_PREFIX = "Customize: ";
29042
29963
  function customizeDir(root2) {
29043
- return join11(root2, REGISTRY_DIR_REL, "customize");
29964
+ return join14(root2, REGISTRY_DIR_REL, "customize");
29044
29965
  }
29045
29966
  function customizePathForSlug(root2, slug2) {
29046
- return join11(customizeDir(root2), `${slug2}.md`);
29967
+ return join14(customizeDir(root2), `${slug2}.md`);
29047
29968
  }
29048
29969
  function findCustomizeViaRegistry(root2, slug2) {
29049
29970
  for (const rel of parseRegistryPaths(root2)) {
29050
29971
  const expected = `${REGISTRY_DIR_REL}/customize/${slug2}.md`;
29051
- if (normalizeRelPath(rel) === expected && existsSync11(join11(root2, rel))) {
29972
+ if (normalizeRelPath(rel) === expected && existsSync17(join14(root2, rel))) {
29052
29973
  return rel;
29053
29974
  }
29054
29975
  }
@@ -29056,17 +29977,17 @@ function findCustomizeViaRegistry(root2, slug2) {
29056
29977
  }
29057
29978
  function resolveSlugFile(root2, slug2) {
29058
29979
  const direct = customizePathForSlug(root2, slug2);
29059
- if (existsSync11(direct)) {
29980
+ if (existsSync17(direct)) {
29060
29981
  return {
29061
- content: readFileSync10(direct, "utf8"),
29062
- path: normalizeRelPath(relative8(root2, direct))
29982
+ content: readFileSync14(direct, "utf8"),
29983
+ path: normalizeRelPath(relative9(root2, direct))
29063
29984
  };
29064
29985
  }
29065
29986
  const registryPath = findCustomizeViaRegistry(root2, slug2);
29066
29987
  if (registryPath) {
29067
- const abs = join11(root2, registryPath);
29988
+ const abs = join14(root2, registryPath);
29068
29989
  return {
29069
- content: readFileSync10(abs, "utf8"),
29990
+ content: readFileSync14(abs, "utf8"),
29070
29991
  path: registryPath
29071
29992
  };
29072
29993
  }
@@ -29085,21 +30006,21 @@ function readAlwaysInclude(root2, basenames, skipBasename) {
29085
30006
  const paths = [];
29086
30007
  const dir = customizeDir(root2);
29087
30008
  for (const name of basenames) {
29088
- const file = basename2(name);
30009
+ const file = basename3(name);
29089
30010
  if (skipBasename && file === skipBasename)
29090
30011
  continue;
29091
- const abs = join11(dir, file);
29092
- if (!existsSync11(abs))
30012
+ const abs = join14(dir, file);
30013
+ if (!existsSync17(abs))
29093
30014
  continue;
29094
- parts.push(readFileSync10(abs, "utf8").trimEnd());
29095
- paths.push(normalizeRelPath(relative8(root2, abs)));
30015
+ parts.push(readFileSync14(abs, "utf8").trimEnd());
30016
+ paths.push(normalizeRelPath(relative9(root2, abs)));
29096
30017
  }
29097
30018
  return { parts, paths };
29098
30019
  }
29099
30020
  function resolveCustomize(root2, slug2) {
29100
30021
  const slugFile = resolveSlugFile(root2, slug2);
29101
30022
  const alwaysNames = alwaysIncludeBasenames(root2);
29102
- const skip = slugFile.path != null ? basename2(slugFile.path) : null;
30023
+ const skip = slugFile.path != null ? basename3(slugFile.path) : null;
29103
30024
  const always = readAlwaysInclude(root2, alwaysNames, skip);
29104
30025
  const parts = [];
29105
30026
  const included = [];
@@ -29132,49 +30053,49 @@ function resolveCustomizeFromRoot(slug2, startDir) {
29132
30053
 
29133
30054
  // src/init/init.ts
29134
30055
  import { spawnSync as spawnSync2 } from "node:child_process";
29135
- import { copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync2, readFileSync as readFileSync12 } from "node:fs";
29136
- import { join as join15 } from "node:path";
30056
+ import { copyFileSync, existsSync as existsSync21, mkdirSync as mkdirSync2, readFileSync as readFileSync16 } from "node:fs";
30057
+ import { join as join18 } from "node:path";
29137
30058
 
29138
30059
  // src/init/merge-hooks.ts
29139
- import { existsSync as existsSync14, mkdirSync, readFileSync as readFileSync11, writeFileSync } from "node:fs";
29140
- import { dirname as dirname6, join as join14 } from "node:path";
30060
+ import { existsSync as existsSync20, mkdirSync, readFileSync as readFileSync15, writeFileSync as writeFileSync2 } from "node:fs";
30061
+ import { dirname as dirname10, join as join17 } from "node:path";
29141
30062
 
29142
30063
  // src/init/package-paths.ts
29143
- import { existsSync as existsSync12 } from "node:fs";
29144
- import { dirname as dirname4, join as join12 } from "node:path";
29145
- import { fileURLToPath as fileURLToPath4 } from "node:url";
29146
- var MODULE_DIR = dirname4(fileURLToPath4(import.meta.url));
29147
- var PACKAGE_ROOT_CANDIDATES = [join12(MODULE_DIR, "../.."), join12(MODULE_DIR, "..")];
30064
+ import { existsSync as existsSync18 } from "node:fs";
30065
+ import { dirname as dirname8, join as join15 } from "node:path";
30066
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
30067
+ var MODULE_DIR = dirname8(fileURLToPath5(import.meta.url));
30068
+ var PACKAGE_ROOT_CANDIDATES = [join15(MODULE_DIR, "../.."), join15(MODULE_DIR, "..")];
29148
30069
  function resolvePackageRoot() {
29149
30070
  for (const candidate of PACKAGE_ROOT_CANDIDATES) {
29150
- if (existsSync12(join12(candidate, "package.json")))
30071
+ if (existsSync18(join15(candidate, "package.json")))
29151
30072
  return candidate;
29152
30073
  }
29153
30074
  throw new Error("Could not resolve @csark0812/skeleton package root");
29154
30075
  }
29155
30076
  function resolveTemplatesDir() {
29156
- const dir = join12(resolvePackageRoot(), "templates/skeleton-init");
29157
- if (!existsSync12(dir)) {
30077
+ const dir = join15(resolvePackageRoot(), "templates/skeleton-init");
30078
+ if (!existsSync18(dir)) {
29158
30079
  throw new Error("Missing templates/skeleton-init in package");
29159
30080
  }
29160
30081
  return dir;
29161
30082
  }
29162
30083
 
29163
30084
  // src/init/resolve-hook-command.ts
29164
- import { existsSync as existsSync13 } from "node:fs";
30085
+ import { existsSync as existsSync19 } from "node:fs";
29165
30086
  import { createRequire as createRequire3 } from "node:module";
29166
- import { dirname as dirname5, join as join13, relative as relative9, resolve as resolve5 } from "node:path";
30087
+ import { dirname as dirname9, join as join16, relative as relative10, resolve as resolve8 } from "node:path";
29167
30088
  var PACKAGE_NAME = "@csark0812/skeleton";
29168
30089
  var HOOK_DIST = "dist/hooks/customize-on-skill-read.js";
29169
30090
  var HOOK_SRC = "src/hooks/customize-on-skill-read.ts";
29170
30091
  var PACKAGE_ROOT = resolvePackageRoot();
29171
30092
  function toRepoRelative(cwd, absPath) {
29172
- const rel = relative9(cwd, absPath).replace(/\\/g, "/");
30093
+ const rel = relative10(cwd, absPath).replace(/\\/g, "/");
29173
30094
  return rel.startsWith("..") ? absPath.replace(/\\/g, "/") : rel;
29174
30095
  }
29175
30096
  function tryResolvePublished(cwd) {
29176
30097
  try {
29177
- const req = createRequire3(join13(cwd, "package.json"));
30098
+ const req = createRequire3(join16(cwd, "package.json"));
29178
30099
  return req.resolve(`${PACKAGE_NAME}/${HOOK_DIST}`);
29179
30100
  } catch {
29180
30101
  return null;
@@ -29183,10 +30104,10 @@ function tryResolvePublished(cwd) {
29183
30104
  function walkNodeModules(cwd) {
29184
30105
  let dir = cwd;
29185
30106
  while (true) {
29186
- const candidate = join13(dir, "node_modules", PACKAGE_NAME, HOOK_DIST);
29187
- if (existsSync13(candidate))
30107
+ const candidate = join16(dir, "node_modules", PACKAGE_NAME, HOOK_DIST);
30108
+ if (existsSync19(candidate))
29188
30109
  return candidate;
29189
- const parent = dirname5(dir);
30110
+ const parent = dirname9(dir);
29190
30111
  if (parent === dir)
29191
30112
  break;
29192
30113
  dir = parent;
@@ -29194,7 +30115,7 @@ function walkNodeModules(cwd) {
29194
30115
  return null;
29195
30116
  }
29196
30117
  function isInsidePackageRoot(cwd) {
29197
- const rel = relative9(PACKAGE_ROOT, resolve5(cwd)).replace(/\\/g, "/");
30118
+ const rel = relative10(PACKAGE_ROOT, resolve8(cwd)).replace(/\\/g, "/");
29198
30119
  return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
29199
30120
  }
29200
30121
  function resolveHookCommand(cwd) {
@@ -29205,11 +30126,11 @@ function resolveHookCommand(cwd) {
29205
30126
  if (hoisted)
29206
30127
  return toRepoRelative(cwd, hoisted);
29207
30128
  if (isInsidePackageRoot(cwd)) {
29208
- const distHook = join13(PACKAGE_ROOT, HOOK_DIST);
29209
- if (existsSync13(distHook))
30129
+ const distHook = join16(PACKAGE_ROOT, HOOK_DIST);
30130
+ if (existsSync19(distHook))
29210
30131
  return toRepoRelative(cwd, distHook);
29211
- const srcHook = join13(PACKAGE_ROOT, HOOK_SRC);
29212
- if (existsSync13(srcHook)) {
30132
+ const srcHook = join16(PACKAGE_ROOT, HOOK_SRC);
30133
+ if (existsSync19(srcHook)) {
29213
30134
  const rel = toRepoRelative(cwd, srcHook);
29214
30135
  return rel.includes("/") ? `bun ${rel}` : `bun ./${rel}`;
29215
30136
  }
@@ -29228,21 +30149,21 @@ function identityKey(platform, event, matcher) {
29228
30149
  return `skeleton:customize:${platform}:${event}:${matcher}`;
29229
30150
  }
29230
30151
  function loadFragment(name, hookCommand) {
29231
- const raw = readFileSync11(join14(TEMPLATES_DIR, name), "utf8");
30152
+ const raw = readFileSync15(join17(TEMPLATES_DIR, name), "utf8");
29232
30153
  return JSON.parse(raw.replaceAll("{{HOOK_COMMAND}}", hookCommand));
29233
30154
  }
29234
30155
  function readJson(path2) {
29235
- if (!existsSync14(path2))
30156
+ if (!existsSync20(path2))
29236
30157
  return null;
29237
30158
  try {
29238
- return JSON.parse(readFileSync11(path2, "utf8"));
30159
+ return JSON.parse(readFileSync15(path2, "utf8"));
29239
30160
  } catch (error) {
29240
30161
  throw new Error(`Invalid JSON in ${path2}: ${error}`);
29241
30162
  }
29242
30163
  }
29243
30164
  function writeJson(path2, value) {
29244
- mkdirSync(dirname6(path2), { recursive: true });
29245
- writeFileSync(path2, `${JSON.stringify(value, null, 2)}
30165
+ mkdirSync(dirname10(path2), { recursive: true });
30166
+ writeFileSync2(path2, `${JSON.stringify(value, null, 2)}
29246
30167
  `, "utf8");
29247
30168
  }
29248
30169
  function deepEqual(a, b) {
@@ -29341,14 +30262,14 @@ function mergeNestedHooks(platform, targetPath, fragment, eventName, opts) {
29341
30262
  }
29342
30263
  function mergeHookConfigs(opts) {
29343
30264
  const results = [];
29344
- const cursorPath = join14(opts.cwd, ".cursor/hooks.json");
30265
+ const cursorPath = join17(opts.cwd, ".cursor/hooks.json");
29345
30266
  const cursorFragment = loadFragment("cursor-hooks.fragment.json", opts.hookCommand);
29346
30267
  results.push(mergeCursorHooks(cursorPath, cursorFragment, opts));
29347
- const claudePath = join14(opts.cwd, ".claude/settings.json");
30268
+ const claudePath = join17(opts.cwd, ".claude/settings.json");
29348
30269
  const claudeFragment = loadFragment("claude-settings.fragment.json", opts.hookCommand);
29349
30270
  results.push(mergeNestedHooks("claude", claudePath, claudeFragment, "PostToolUse", opts));
29350
- const codexPath = join14(opts.cwd, ".codex/hooks.json");
29351
- if (existsSync14(join14(opts.cwd, ".codex"))) {
30271
+ const codexPath = join17(opts.cwd, ".codex/hooks.json");
30272
+ if (existsSync20(join17(opts.cwd, ".codex"))) {
29352
30273
  const codexFragment = loadFragment("codex-hooks.fragment.json", opts.hookCommand);
29353
30274
  results.push(mergeNestedHooks("codex", codexPath, codexFragment, "PostToolUse", opts));
29354
30275
  } else {
@@ -29357,11 +30278,11 @@ function mergeHookConfigs(opts) {
29357
30278
  return results;
29358
30279
  }
29359
30280
  function mergePackageJsonScripts(cwd) {
29360
- const pkgPath = join14(cwd, "package.json");
29361
- if (!existsSync14(pkgPath))
30281
+ const pkgPath = join17(cwd, "package.json");
30282
+ if (!existsSync20(pkgPath))
29362
30283
  return "skipped";
29363
- const fragment = JSON.parse(readFileSync11(join14(TEMPLATES_DIR, "package.json.scripts.fragment.json"), "utf8"));
29364
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
30284
+ const fragment = JSON.parse(readFileSync15(join17(TEMPLATES_DIR, "package.json.scripts.fragment.json"), "utf8"));
30285
+ const pkg = JSON.parse(readFileSync15(pkgPath, "utf8"));
29365
30286
  pkg.scripts ??= {};
29366
30287
  let changed = false;
29367
30288
  for (const [key, value] of Object.entries(fragment)) {
@@ -29372,7 +30293,7 @@ function mergePackageJsonScripts(cwd) {
29372
30293
  }
29373
30294
  if (!changed)
29374
30295
  return "skipped";
29375
- writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
30296
+ writeFileSync2(pkgPath, `${JSON.stringify(pkg, null, 2)}
29376
30297
  `, "utf8");
29377
30298
  return "updated";
29378
30299
  }
@@ -29417,27 +30338,27 @@ function skillsAddArgs(options = {}) {
29417
30338
  // src/init/init.ts
29418
30339
  var TEMPLATES_DIR2 = resolveTemplatesDir();
29419
30340
  function writeScaffold(cwd) {
29420
- const skeletonDir = join15(cwd, ".skeleton");
29421
- mkdirSync2(skeletonDir, { recursive: true });
30341
+ const skeletonDir2 = join18(cwd, ".skeleton");
30342
+ mkdirSync2(skeletonDir2, { recursive: true });
29422
30343
  let created = false;
29423
- const configPath = join15(skeletonDir, "config.yaml");
29424
- if (!existsSync15(configPath)) {
29425
- copyFileSync(join15(TEMPLATES_DIR2, "config.yaml"), configPath);
30344
+ const configPath = join18(skeletonDir2, "config.yaml");
30345
+ if (!existsSync21(configPath)) {
30346
+ copyFileSync(join18(TEMPLATES_DIR2, "config.yaml"), configPath);
29426
30347
  created = true;
29427
30348
  }
29428
- const registryPath = join15(skeletonDir, "registry.md");
29429
- if (!existsSync15(registryPath)) {
29430
- copyFileSync(join15(TEMPLATES_DIR2, "registry.md"), registryPath);
30349
+ const registryPath = join18(skeletonDir2, "registry.md");
30350
+ if (!existsSync21(registryPath)) {
30351
+ copyFileSync(join18(TEMPLATES_DIR2, "registry.md"), registryPath);
29431
30352
  created = true;
29432
30353
  }
29433
- mkdirSync2(join15(skeletonDir, "customize"), { recursive: true });
30354
+ mkdirSync2(join18(skeletonDir2, "customize"), { recursive: true });
29434
30355
  return created ? "created" : "skipped";
29435
30356
  }
29436
30357
  function assertPackageResolvable(cwd) {
29437
- const pkgPath = join15(cwd, "package.json");
29438
- if (!existsSync15(pkgPath))
30358
+ const pkgPath = join18(cwd, "package.json");
30359
+ if (!existsSync21(pkgPath))
29439
30360
  return;
29440
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
30361
+ const pkg = JSON.parse(readFileSync16(pkgPath, "utf8"));
29441
30362
  const hasDep = pkg.devDependencies?.["@csark0812/skeleton"] || pkg.dependencies?.["@csark0812/skeleton"];
29442
30363
  if (!hasDep) {
29443
30364
  try {
@@ -29518,51 +30439,188 @@ function parseInitArgs(argv) {
29518
30439
  return { forceHooks, skills, noSkills, skillsFlags };
29519
30440
  }
29520
30441
 
30442
+ // src/plugins/build.ts
30443
+ import { spawnSync as spawnSync3 } from "node:child_process";
30444
+ import { createHash } from "node:crypto";
30445
+ import { existsSync as existsSync22, readFileSync as readFileSync17, writeFileSync as writeFileSync3 } from "node:fs";
30446
+ import { basename as basename4, dirname as dirname11, resolve as resolve9 } from "node:path";
30447
+ function parseBuildPluginArgs(argv) {
30448
+ let check = false;
30449
+ let entry;
30450
+ for (const arg of argv) {
30451
+ if (arg === "--check") {
30452
+ check = true;
30453
+ } else if (arg.startsWith("--check=")) {
30454
+ throw new Error("build-plugin: use --check (boolean flag), not --check=<value>");
30455
+ } else if (arg.startsWith("-")) {
30456
+ throw new Error(`build-plugin: unknown flag ${arg}`);
30457
+ } else if (!entry) {
30458
+ entry = arg;
30459
+ }
30460
+ }
30461
+ return { entry, check };
30462
+ }
30463
+ function collectPluginEntries(root2, config, entry) {
30464
+ if (entry) {
30465
+ const abs = entry.startsWith("/") || /^[A-Za-z]:[\\/]/.test(entry) ? resolveAbsolutePluginTsPath(root2, entry) : resolvePluginTsPath(root2, entry);
30466
+ return [abs];
30467
+ }
30468
+ return (config.plugins ?? []).map((e) => resolvePluginTsPath(root2, e));
30469
+ }
30470
+ function localImportPaths(tsAbs, content3) {
30471
+ const dir = dirname11(tsAbs);
30472
+ const deps = [];
30473
+ const re = /(?:from\s+|import\s*\(\s*|import\s+)["'](\.[^"']+)["']/g;
30474
+ for (const match of content3.matchAll(re)) {
30475
+ const spec = match[1];
30476
+ if (!spec)
30477
+ continue;
30478
+ const candidates = [];
30479
+ if (spec.endsWith(".js")) {
30480
+ const withoutJs = spec.slice(0, -".js".length);
30481
+ candidates.push(resolve9(dir, `${withoutJs}.ts`), resolve9(dir, spec), resolve9(dir, withoutJs, "index.ts"));
30482
+ } else {
30483
+ candidates.push(resolve9(dir, spec), resolve9(dir, `${spec}.ts`), resolve9(dir, `${spec}.js`), resolve9(dir, spec, "index.ts"));
30484
+ }
30485
+ for (const candidate of candidates) {
30486
+ if (existsSync22(candidate) && candidate.endsWith(".ts")) {
30487
+ deps.push(candidate);
30488
+ break;
30489
+ }
30490
+ }
30491
+ }
30492
+ return deps;
30493
+ }
30494
+ function stampPathForMjs(mjsAbs) {
30495
+ return `${mjsAbs}.stamp`;
30496
+ }
30497
+ function sourceFingerprint(tsAbs, seen = new Set) {
30498
+ const hash = createHash("sha256");
30499
+ function walk(abs) {
30500
+ if (seen.has(abs))
30501
+ return;
30502
+ seen.add(abs);
30503
+ const content3 = readFileSync17(abs, "utf8");
30504
+ hash.update(basename4(abs));
30505
+ hash.update("\x00");
30506
+ hash.update(content3);
30507
+ hash.update("\x00");
30508
+ for (const dep of localImportPaths(abs, content3).sort()) {
30509
+ walk(dep);
30510
+ }
30511
+ }
30512
+ walk(tsAbs);
30513
+ return hash.digest("hex");
30514
+ }
30515
+ function writeStamp(tsAbs, mjsAbs) {
30516
+ writeFileSync3(stampPathForMjs(mjsAbs), `${sourceFingerprint(tsAbs)}
30517
+ `, "utf8");
30518
+ }
30519
+ async function buildOne(tsAbs) {
30520
+ const mjsAbs = mjsPathForTs(tsAbs);
30521
+ if (!existsSync22(tsAbs)) {
30522
+ throw new Error(`Plugin source not found: ${tsAbs}`);
30523
+ }
30524
+ const proc = spawnSync3("bun", ["build", tsAbs, "--target=node", "--format=esm", `--outfile=${mjsAbs}`, "--packages=external"], { encoding: "utf8" });
30525
+ if (proc.error) {
30526
+ const code3 = proc.error.code;
30527
+ if (code3 === "ENOENT") {
30528
+ throw new Error("skeleton build-plugin requires the bun binary on PATH (bun build). Install Bun 1.2.x.");
30529
+ }
30530
+ throw new Error(`skeleton build-plugin failed to spawn bun: ${proc.error.message}`);
30531
+ }
30532
+ if (proc.status !== 0) {
30533
+ throw new Error(`skeleton build-plugin failed for ${tsAbs}:
30534
+ ${proc.stderr || proc.stdout || `exit ${proc.status}`}`);
30535
+ }
30536
+ writeStamp(tsAbs, mjsAbs);
30537
+ return mjsAbs;
30538
+ }
30539
+ function checkOne(tsAbs) {
30540
+ const mjsAbs = mjsPathForTs(tsAbs);
30541
+ if (!existsSync22(mjsAbs)) {
30542
+ throw new Error(`Plugin not built: ${tsAbs} (missing ${mjsAbs}). Run: skeleton build-plugin`);
30543
+ }
30544
+ if (!existsSync22(tsAbs)) {
30545
+ throw new Error(`Plugin source not found: ${tsAbs}`);
30546
+ }
30547
+ const stampAbs = stampPathForMjs(mjsAbs);
30548
+ if (!existsSync22(stampAbs)) {
30549
+ throw new Error(`Plugin stale: ${mjsAbs} has no fingerprint stamp. Run: skeleton build-plugin`);
30550
+ }
30551
+ const expected = readFileSync17(stampAbs, "utf8").trim();
30552
+ const actual = sourceFingerprint(tsAbs);
30553
+ if (expected !== actual) {
30554
+ throw new Error(`Plugin stale: ${mjsAbs} does not match ${tsAbs} (or local imports). Run: skeleton build-plugin`);
30555
+ }
30556
+ }
30557
+ async function runBuildPlugin(options = {}) {
30558
+ const root2 = options.root ?? findRepoRoot();
30559
+ const config = loadConfig(root2);
30560
+ const entries = collectPluginEntries(root2, config, options.entry);
30561
+ if (entries.length === 0) {
30562
+ return { built: [], checked: [] };
30563
+ }
30564
+ const built = [];
30565
+ const checked = [];
30566
+ if (options.check) {
30567
+ for (const entry of entries) {
30568
+ checkOne(entry);
30569
+ checked.push(mjsPathForTs(entry));
30570
+ }
30571
+ return { built, checked };
30572
+ }
30573
+ for (const entry of entries) {
30574
+ built.push(await buildOne(entry));
30575
+ }
30576
+ return { built, checked };
30577
+ }
30578
+
29521
30579
  // src/references/sync.ts
29522
30580
  import {
29523
- existsSync as existsSync16,
30581
+ existsSync as existsSync23,
29524
30582
  mkdirSync as mkdirSync3,
29525
30583
  readdirSync as readdirSync6,
29526
- readFileSync as readFileSync13,
30584
+ readFileSync as readFileSync18,
29527
30585
  unlinkSync,
29528
- writeFileSync as writeFileSync2
30586
+ writeFileSync as writeFileSync4
29529
30587
  } from "node:fs";
29530
- import { dirname as dirname7, join as join16, relative as relative10 } from "node:path";
30588
+ import { dirname as dirname12, join as join19, relative as relative11 } from "node:path";
29531
30589
  function walkMarkdownFiles2(dir, root2) {
29532
30590
  const files = [];
29533
- if (!existsSync16(dir))
30591
+ if (!existsSync23(dir))
29534
30592
  return files;
29535
30593
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
29536
30594
  if (entry.name.startsWith("."))
29537
30595
  continue;
29538
- const fullPath = join16(dir, entry.name);
30596
+ const fullPath = join19(dir, entry.name);
29539
30597
  if (entry.isDirectory()) {
29540
30598
  files.push(...walkMarkdownFiles2(fullPath, root2));
29541
30599
  continue;
29542
30600
  }
29543
30601
  if (entry.name.endsWith(".md")) {
29544
- files.push(normalizeRelPath(relative10(root2, fullPath)));
30602
+ files.push(normalizeRelPath(relative11(root2, fullPath)));
29545
30603
  }
29546
30604
  }
29547
30605
  return files;
29548
30606
  }
29549
30607
  function listGeneratedReferenceFiles(skillDir, skill) {
29550
- const refsDir = join16(skillDir, "references");
29551
- if (!existsSync16(refsDir))
30608
+ const refsDir = join19(skillDir, "references");
30609
+ if (!existsSync23(refsDir))
29552
30610
  return [];
29553
30611
  const files = [];
29554
30612
  const walk = (dir) => {
29555
30613
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
29556
- const fullPath = join16(dir, entry.name);
30614
+ const fullPath = join19(dir, entry.name);
29557
30615
  if (entry.isDirectory()) {
29558
30616
  walk(fullPath);
29559
30617
  continue;
29560
30618
  }
29561
30619
  if (!entry.name.endsWith(".md"))
29562
30620
  continue;
29563
- const content3 = readFileSync13(fullPath, "utf8");
30621
+ const content3 = readFileSync18(fullPath, "utf8");
29564
30622
  if (isGeneratedReference(content3)) {
29565
- const refPath = normalizeRelPath(relative10(refsDir, fullPath));
30623
+ const refPath = normalizeRelPath(relative11(refsDir, fullPath));
29566
30624
  files.push(generatedRefPath(skill, refPath));
29567
30625
  }
29568
30626
  }
@@ -29572,8 +30630,8 @@ function listGeneratedReferenceFiles(skillDir, skill) {
29572
30630
  }
29573
30631
  function syncReferences(options = {}) {
29574
30632
  const root2 = options.root ?? process.cwd();
29575
- const canonicalDir = join16(root2, CANONICAL_REFS_DIR);
29576
- if (!existsSync16(canonicalDir)) {
30633
+ const canonicalDir = join19(root2, CANONICAL_REFS_DIR);
30634
+ if (!existsSync23(canonicalDir)) {
29577
30635
  throw new Error(`canonical references dir not found: ${CANONICAL_REFS_DIR}`);
29578
30636
  }
29579
30637
  const result = {
@@ -29584,24 +30642,24 @@ function syncReferences(options = {}) {
29584
30642
  };
29585
30643
  const plans = discoverSkillReferencePlans(root2);
29586
30644
  for (const plan of plans) {
29587
- const skillDir = join16(root2, plan.skill);
30645
+ const skillDir = join19(root2, plan.skill);
29588
30646
  for (const refPath of plan.refPaths) {
29589
- const sourceRel = normalizeRelPath(join16(CANONICAL_REFS_DIR, refPath));
29590
- const canonicalPath = join16(root2, sourceRel);
29591
- if (!existsSync16(canonicalPath)) {
30647
+ const sourceRel = normalizeRelPath(join19(CANONICAL_REFS_DIR, refPath));
30648
+ const canonicalPath = join19(root2, sourceRel);
30649
+ if (!existsSync23(canonicalPath)) {
29592
30650
  throw new Error(`canonical reference missing: ${sourceRel}`);
29593
30651
  }
29594
30652
  const targetRel = generatedRefPath(plan.skill, refPath);
29595
- const targetPath = join16(root2, targetRel);
29596
- const canonicalContent = readFileSync13(canonicalPath, "utf8");
30653
+ const targetPath = join19(root2, targetRel);
30654
+ const canonicalContent = readFileSync18(canonicalPath, "utf8");
29597
30655
  const nextContent = formatGeneratedHeader(sourceRel) + canonicalContent;
29598
30656
  if (!options.dryRun) {
29599
- mkdirSync3(dirname7(targetPath), { recursive: true });
30657
+ mkdirSync3(dirname12(targetPath), { recursive: true });
29600
30658
  }
29601
- const existing = existsSync16(targetPath) ? readFileSync13(targetPath, "utf8") : null;
30659
+ const existing = existsSync23(targetPath) ? readFileSync18(targetPath, "utf8") : null;
29602
30660
  if (existing !== nextContent) {
29603
30661
  if (!options.dryRun)
29604
- writeFileSync2(targetPath, nextContent, "utf8");
30662
+ writeFileSync4(targetPath, nextContent, "utf8");
29605
30663
  result.written.push(targetRel);
29606
30664
  } else {
29607
30665
  result.skipped.push(targetRel);
@@ -29609,12 +30667,12 @@ function syncReferences(options = {}) {
29609
30667
  }
29610
30668
  if (options.rewriteLinks !== false) {
29611
30669
  for (const relFile of walkMarkdownFiles2(skillDir, root2)) {
29612
- const filePath = join16(root2, relFile);
29613
- const content3 = readFileSync13(filePath, "utf8");
30670
+ const filePath = join19(root2, relFile);
30671
+ const content3 = readFileSync18(filePath, "utf8");
29614
30672
  const next = rewriteSharedRefLinks(content3, relFile, plan.skill);
29615
30673
  if (next !== content3) {
29616
30674
  if (!options.dryRun)
29617
- writeFileSync2(filePath, next, "utf8");
30675
+ writeFileSync4(filePath, next, "utf8");
29618
30676
  result.rewritten.push(relFile);
29619
30677
  }
29620
30678
  }
@@ -29622,7 +30680,7 @@ function syncReferences(options = {}) {
29622
30680
  for (const generatedRel of listGeneratedReferenceFiles(skillDir, plan.skill)) {
29623
30681
  const refPath = generatedRel.slice(`${plan.skill}/references/`.length);
29624
30682
  if (!plan.refPaths.has(refPath)) {
29625
- const fullPath = join16(root2, generatedRel);
30683
+ const fullPath = join19(root2, generatedRel);
29626
30684
  if (!options.dryRun)
29627
30685
  unlinkSync(fullPath);
29628
30686
  result.removed.push(generatedRel);
@@ -29667,8 +30725,8 @@ function printSyncResult(result) {
29667
30725
  }
29668
30726
 
29669
30727
  // src/register.ts
29670
- import { existsSync as existsSync17, readFileSync as readFileSync14, writeFileSync as writeFileSync3 } from "node:fs";
29671
- import { dirname as dirname8, join as join17, relative as relative11 } from "node:path";
30728
+ import { existsSync as existsSync24, readFileSync as readFileSync19, writeFileSync as writeFileSync5 } from "node:fs";
30729
+ import { dirname as dirname13, join as join20, relative as relative12 } from "node:path";
29672
30730
  var REGISTRY_TABLE_ROW_RE2 = /^\|\s*([^|]+)\|\s*\[[^\]]*\]\(([^)]+)\)\s*\|/;
29673
30731
  var REGISTRY_TABLE_HEADER = "| Topic | Canonical file |";
29674
30732
  function extractTopic(content3) {
@@ -29676,8 +30734,8 @@ function extractTopic(content3) {
29676
30734
  return match?.[1]?.trim().replace(/\s+$/, "") ?? null;
29677
30735
  }
29678
30736
  function toRegistryLink(root2, absPath) {
29679
- const fromRegistry = join17(root2, REGISTRY_DIR_REL);
29680
- return normalizeRelPath(relative11(fromRegistry, absPath));
30737
+ const fromRegistry = join20(root2, REGISTRY_DIR_REL);
30738
+ return normalizeRelPath(relative12(fromRegistry, absPath));
29681
30739
  }
29682
30740
  function inferSection(registryLink) {
29683
30741
  return registryLink.startsWith("customize/") ? "Customizations" : "Documentation";
@@ -29701,12 +30759,12 @@ function parseRegistryRows(content3) {
29701
30759
  return rows;
29702
30760
  }
29703
30761
  function pathFromRegistryLink(root2, link2) {
29704
- return normalizeRelPath(relative11(root2, join17(root2, REGISTRY_DIR_REL, link2)));
30762
+ return normalizeRelPath(relative12(root2, join20(root2, REGISTRY_DIR_REL, link2)));
29705
30763
  }
29706
30764
  function isOutsideScan(root2, relPath2) {
29707
30765
  const config = loadConfig(root2);
29708
30766
  const skillIndex = buildSkillIndex(root2);
29709
- const scanned = collectScanFiles(config, root2, skillIndex).map((abs) => normalizeRelPath(relative11(root2, abs)));
30767
+ const scanned = collectScanFiles(config, root2, skillIndex).map((abs) => normalizeRelPath(relative12(root2, abs)));
29710
30768
  if (scanned.includes(relPath2))
29711
30769
  return false;
29712
30770
  return !config.scan.include.some((pattern) => matchesGlobScope(relPath2, pattern));
@@ -29768,11 +30826,11 @@ ${newLine}
29768
30826
  function registerPath(options) {
29769
30827
  const root2 = options.root ?? findRepoRoot();
29770
30828
  const relPath2 = normalizeRelPath(options.path);
29771
- const absPath = join17(root2, relPath2);
29772
- if (!existsSync17(absPath)) {
30829
+ const absPath = join20(root2, relPath2);
30830
+ if (!existsSync24(absPath)) {
29773
30831
  throw new Error(`File not found: ${relPath2}`);
29774
30832
  }
29775
- const content3 = readFileSync14(absPath, "utf8");
30833
+ const content3 = readFileSync19(absPath, "utf8");
29776
30834
  let topic = options.topic ?? extractTopic(content3);
29777
30835
  if (!topic) {
29778
30836
  throw new Error(`No **Source of truth for** banner in ${relPath2} — add banner or pass --topic`);
@@ -29780,9 +30838,9 @@ function registerPath(options) {
29780
30838
  const registryLink = toRegistryLink(root2, absPath);
29781
30839
  topic = ensureCustomizeTopic(topic, registryLink);
29782
30840
  const section = inferSection(registryLink);
29783
- const registryAbs = join17(root2, REGISTRY_REL_PATH);
29784
- let registryContent = existsSync17(registryAbs) ? readFileSync14(registryAbs, "utf8") : defaultRegistryContent();
29785
- if (!existsSync17(registryAbs) && !existsSync17(join17(root2, ".skeleton/config.yaml"))) {
30841
+ const registryAbs = join20(root2, REGISTRY_REL_PATH);
30842
+ let registryContent = existsSync24(registryAbs) ? readFileSync19(registryAbs, "utf8") : defaultRegistryContent();
30843
+ if (!existsSync24(registryAbs) && !existsSync24(join20(root2, ".skeleton/config.yaml"))) {
29786
30844
  throw new Error("Missing .skeleton/config.yaml — run skeleton init first");
29787
30845
  }
29788
30846
  const { content: updated, action } = upsertRow(registryContent, topic, registryLink, section, root2);
@@ -29795,11 +30853,11 @@ function registerPath(options) {
29795
30853
  warnOutsideScan: isOutsideScan(root2, relPath2)
29796
30854
  };
29797
30855
  if (!options.dryRun && action !== "noop") {
29798
- const dir = dirname8(registryAbs);
29799
- if (!existsSync17(dir)) {
30856
+ const dir = dirname13(registryAbs);
30857
+ if (!existsSync24(dir)) {
29800
30858
  throw new Error(`Missing ${REGISTRY_DIR_REL}/ directory`);
29801
30859
  }
29802
- writeFileSync3(registryAbs, registryContent, "utf8");
30860
+ writeFileSync5(registryAbs, registryContent, "utf8");
29803
30861
  }
29804
30862
  if (result.warnOutsideScan) {
29805
30863
  console.error(`warning: ${relPath2} is outside scan.include — register succeeded but audit will not scan it`);
@@ -29817,12 +30875,12 @@ function registerPath(options) {
29817
30875
  }
29818
30876
 
29819
30877
  // src/validate/changed.ts
29820
- import { spawnSync as spawnSync4 } from "node:child_process";
29821
- import { existsSync as existsSync18, readFileSync as readFileSync15 } from "node:fs";
29822
- import { basename as basename3, extname, join as join18 } from "node:path";
30878
+ import { spawnSync as spawnSync5 } from "node:child_process";
30879
+ import { existsSync as existsSync25, readFileSync as readFileSync20 } from "node:fs";
30880
+ import { basename as basename5, extname as extname2, join as join21 } from "node:path";
29823
30881
 
29824
30882
  // src/validate/git-diff.ts
29825
- import { spawnSync as spawnSync3 } from "node:child_process";
30883
+ import { spawnSync as spawnSync4 } from "node:child_process";
29826
30884
  function gitDiffChangedFiles(options = {}) {
29827
30885
  const root2 = options.root ?? findRepoRoot();
29828
30886
  let args;
@@ -29833,7 +30891,7 @@ function gitDiffChangedFiles(options = {}) {
29833
30891
  } else {
29834
30892
  args = ["diff", "HEAD", "--name-only", "--diff-filter=ACMR"];
29835
30893
  }
29836
- const proc = spawnSync3("git", args, { cwd: root2, encoding: "utf8" });
30894
+ const proc = spawnSync4("git", args, { cwd: root2, encoding: "utf8" });
29837
30895
  if (proc.status !== 0) {
29838
30896
  throw new Error(proc.stderr?.trim() || "git diff failed");
29839
30897
  }
@@ -29843,17 +30901,34 @@ function gitDiffChangedFiles(options = {}) {
29843
30901
 
29844
30902
  // src/validate/changed.ts
29845
30903
  var DOC_EXTENSIONS = new Set([".md", ".mdc", ".yaml", ".yml"]);
30904
+ var POLICY_EXTENSIONS = new Set([".yaml", ".yml"]);
29846
30905
  var SHELL_EXTENSIONS = new Set([".sh", ".bash", ".zsh"]);
29847
30906
  var SKIP_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py"]);
29848
30907
  var COMMAND_CONFIG_NAMES = new Set(["package.json", "project.json"]);
29849
- function bucketFor(relPath2, root2) {
30908
+ function isSkeletonYamlCandidate(normalized, ext) {
30909
+ if (!POLICY_EXTENSIONS.has(ext))
30910
+ return false;
30911
+ if (!(normalized.startsWith(".skeleton/") || normalized.startsWith(".skeleton\\"))) {
30912
+ return false;
30913
+ }
30914
+ const name = basename5(normalized).toLowerCase();
30915
+ if (name === "config.yaml" || name === "config.yml")
30916
+ return false;
30917
+ return true;
30918
+ }
30919
+ function bucketFor(relPath2, root2, wiredPolicies) {
29850
30920
  const normalized = normalizeRelPath(relPath2);
29851
- const ext = extname(normalized).toLowerCase();
29852
- const name = basename3(normalized);
30921
+ const ext = extname2(normalized).toLowerCase();
30922
+ const name = basename5(normalized);
29853
30923
  if (SKIP_EXTENSIONS.has(ext))
29854
30924
  return "skip";
29855
30925
  if (COMMAND_CONFIG_NAMES.has(name))
29856
30926
  return "skip";
30927
+ if (isSkeletonYamlCandidate(normalized, ext)) {
30928
+ if (wiredPolicies.has(normalized))
30929
+ return "policy";
30930
+ return "skip";
30931
+ }
29857
30932
  const skillIndex = buildSkillIndex(root2);
29858
30933
  if (isSkillPath(normalized, skillIndex))
29859
30934
  return "skills";
@@ -29885,21 +30960,31 @@ function parseJsonContent(content3) {
29885
30960
  }
29886
30961
  }
29887
30962
  function validateJson(relPath2, root2) {
29888
- const abs = join18(root2, relPath2);
30963
+ const abs = join21(root2, relPath2);
29889
30964
  try {
29890
- parseJsonContent(readFileSync15(abs, "utf8"));
30965
+ parseJsonContent(readFileSync20(abs, "utf8"));
29891
30966
  return 0;
29892
30967
  } catch (error) {
29893
30968
  console.error(`validate changed: invalid JSON in ${relPath2}: ${error}`);
29894
30969
  return 1;
29895
30970
  }
29896
30971
  }
30972
+ function validatePolicy(relPath2, root2) {
30973
+ const abs = join21(root2, relPath2);
30974
+ try {
30975
+ loadPolicyFile(abs, readFileSync20(abs, "utf8"));
30976
+ return 0;
30977
+ } catch (error) {
30978
+ console.error(`validate changed: invalid policy ${relPath2}: ${error}`);
30979
+ return 1;
30980
+ }
30981
+ }
29897
30982
  function validateShell(relPath2, root2) {
29898
- const abs = join18(root2, relPath2);
29899
- const shellcheck = spawnSync4("shellcheck", [abs], { encoding: "utf8" });
30983
+ const abs = join21(root2, relPath2);
30984
+ const shellcheck = spawnSync5("shellcheck", [abs], { encoding: "utf8" });
29900
30985
  if (shellcheck.status === 0)
29901
30986
  return 0;
29902
- const bash = spawnSync4("bash", ["-n", abs], { encoding: "utf8" });
30987
+ const bash = spawnSync5("bash", ["-n", abs], { encoding: "utf8" });
29903
30988
  if (bash.status === 0)
29904
30989
  return 0;
29905
30990
  console.error(`validate changed: shell syntax check failed for ${relPath2}: ${bash.stderr || shellcheck.stderr}`);
@@ -29917,23 +31002,23 @@ function resolvePaths(options) {
29917
31002
  }
29918
31003
  function codeValidationHint(root2) {
29919
31004
  let pm2 = null;
29920
- const pkgPath = join18(root2, "package.json");
29921
- if (existsSync18(pkgPath)) {
31005
+ const pkgPath = join21(root2, "package.json");
31006
+ if (existsSync25(pkgPath)) {
29922
31007
  try {
29923
- const pkg = JSON.parse(readFileSync15(pkgPath, "utf8"));
31008
+ const pkg = JSON.parse(readFileSync20(pkgPath, "utf8"));
29924
31009
  const raw = pkg.packageManager?.split("@")[0];
29925
31010
  if (raw === "bun" || raw === "npm" || raw === "pnpm" || raw === "yarn")
29926
31011
  pm2 = raw;
29927
31012
  } catch {}
29928
31013
  }
29929
31014
  if (!pm2) {
29930
- if (existsSync18(join18(root2, "bun.lock")) || existsSync18(join18(root2, "bun.lockb")))
31015
+ if (existsSync25(join21(root2, "bun.lock")) || existsSync25(join21(root2, "bun.lockb")))
29931
31016
  pm2 = "bun";
29932
- else if (existsSync18(join18(root2, "pnpm-lock.yaml")))
31017
+ else if (existsSync25(join21(root2, "pnpm-lock.yaml")))
29933
31018
  pm2 = "pnpm";
29934
- else if (existsSync18(join18(root2, "yarn.lock")))
31019
+ else if (existsSync25(join21(root2, "yarn.lock")))
29935
31020
  pm2 = "yarn";
29936
- else if (existsSync18(join18(root2, "package-lock.json")))
31021
+ else if (existsSync25(join21(root2, "package-lock.json")))
29937
31022
  pm2 = "npm";
29938
31023
  }
29939
31024
  switch (pm2) {
@@ -29949,43 +31034,66 @@ function codeValidationHint(root2) {
29949
31034
  return " Run your local code validation gates (test + typecheck + build).";
29950
31035
  }
29951
31036
  }
29952
- function runValidateChanged(options = {}) {
31037
+ async function runValidateChanged(options = {}) {
29953
31038
  const root2 = options.root ?? findRepoRoot();
29954
31039
  const relPaths = resolvePaths(options);
29955
31040
  if (relPaths.length === 0) {
29956
31041
  console.log("validate changed: no changed files.");
29957
31042
  return 0;
29958
31043
  }
31044
+ const config = loadConfig(root2);
31045
+ let wiredPolicies;
31046
+ try {
31047
+ wiredPolicies = await collectWiredPolicyRelPaths(root2, config);
31048
+ } catch (error) {
31049
+ console.error(`validate changed: ${error instanceof Error ? error.message : error}`);
31050
+ return 1;
31051
+ }
29959
31052
  const buckets = {
29960
31053
  docs: [],
29961
31054
  skills: [],
29962
31055
  shell: [],
29963
- json: []
31056
+ json: [],
31057
+ policy: []
29964
31058
  };
29965
31059
  let missing = 0;
29966
31060
  let skipped = 0;
31061
+ const orphans = [];
29967
31062
  for (const relPath2 of relPaths) {
29968
- const abs = join18(root2, relPath2);
29969
- if (!existsSync18(abs)) {
31063
+ const normalized = normalizeRelPath(relPath2);
31064
+ const abs = join21(root2, normalized);
31065
+ if (!existsSync25(abs)) {
29970
31066
  missing++;
29971
31067
  console.error(`validate changed: path not found: ${relPath2}`);
29972
31068
  continue;
29973
31069
  }
29974
- const bucket = bucketFor(relPath2, root2);
31070
+ const ext = extname2(normalized).toLowerCase();
31071
+ if (isSkeletonYamlCandidate(normalized, ext) && !wiredPolicies.has(normalized)) {
31072
+ orphans.push(normalized);
31073
+ continue;
31074
+ }
31075
+ const bucket = bucketFor(normalized, root2, wiredPolicies);
29975
31076
  if (bucket === "skip") {
29976
31077
  skipped++;
29977
31078
  continue;
29978
31079
  }
29979
- buckets[bucket].push(relPath2);
31080
+ buckets[bucket].push(normalized);
29980
31081
  }
29981
- const audited = buckets.docs.length + buckets.skills.length + buckets.shell.length + buckets.json.length;
31082
+ if (orphans.length > 0) {
31083
+ for (const orphan of orphans) {
31084
+ console.error(`validate changed: ${orphan} is under .skeleton/ but not referenced by any plugin policies glob.
31085
+ ` + " Export it from a plugin `policies` array (see docs/developer/plugins.md), or remove the file.");
31086
+ }
31087
+ return 1;
31088
+ }
31089
+ const audited = buckets.docs.length + buckets.skills.length + buckets.shell.length + buckets.json.length + buckets.policy.length;
29982
31090
  let exitCode = 0;
29983
31091
  if (missing > 0 && audited === 0 && skipped === 0) {
29984
31092
  console.error("validate changed: no paths existed on disk. Pass real paths or use --staged / --base.");
29985
31093
  return 1;
29986
31094
  }
29987
31095
  if (options.base) {
29988
- const globalExit = runAudit({
31096
+ const globalExit = await runAudit({
29989
31097
  suite: "self",
29990
31098
  strict: false,
29991
31099
  json: false,
@@ -30003,7 +31111,7 @@ function runValidateChanged(options = {}) {
30003
31111
  return 1;
30004
31112
  }
30005
31113
  if (buckets.docs.length > 0) {
30006
- const docExit = runAudit({
31114
+ const docExit = await runAudit({
30007
31115
  suite: "docs",
30008
31116
  strict: false,
30009
31117
  json: false,
@@ -30016,14 +31124,14 @@ function runValidateChanged(options = {}) {
30016
31124
  exitCode = 1;
30017
31125
  }
30018
31126
  if (buckets.skills.length > 0) {
30019
- const skillsOnly = buckets.docs.length === 0 && buckets.shell.length === 0 && buckets.json.length === 0;
31127
+ const skillsOnly = buckets.docs.length === 0 && buckets.shell.length === 0 && buckets.json.length === 0 && buckets.policy.length === 0;
30020
31128
  if (skillsOnly && !options.base) {
30021
31129
  console.error(`validate changed: skill paths need the full skills suite (path-scoped skill rules are empty).
30022
31130
  ` + ` Run: skeleton audit skills
30023
- ` + " Or: skeleton audit self");
31131
+ ` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
30024
31132
  return 1;
30025
31133
  }
30026
- const skillExit = runAudit({
31134
+ const skillExit = await runAudit({
30027
31135
  suite: "skills",
30028
31136
  strict: false,
30029
31137
  json: false,
@@ -30043,6 +31151,46 @@ function runValidateChanged(options = {}) {
30043
31151
  if (validateJson(relPath2, root2) !== 0)
30044
31152
  exitCode = 1;
30045
31153
  }
31154
+ for (const relPath2 of buckets.policy) {
31155
+ if (validatePolicy(relPath2, root2) !== 0)
31156
+ exitCode = 1;
31157
+ }
31158
+ if (buckets.policy.length > 0) {
31159
+ if (options.base) {
31160
+ const proseExit = await runAudit({
31161
+ suite: "docs",
31162
+ strict: false,
31163
+ json: false,
31164
+ paths: [],
31165
+ only: null,
31166
+ root: root2
31167
+ });
31168
+ if (proseExit !== 0)
31169
+ exitCode = 1;
31170
+ const skillPaths = listSkillMarkdownPaths(root2, buildSkillIndex(root2));
31171
+ if (skillPaths.length > 0) {
31172
+ const skillProseExit = await runAudit({
31173
+ suite: "skills",
31174
+ strict: false,
31175
+ json: false,
31176
+ paths: skillPaths,
31177
+ only: null,
31178
+ root: root2,
31179
+ pathScopedOnly: true
31180
+ });
31181
+ if (skillProseExit !== 0)
31182
+ exitCode = 1;
31183
+ }
31184
+ } else {
31185
+ if (exitCode === 0) {
31186
+ console.error(`validate changed: policy YAML changes need a full prose-policy pass (path-scoped docs are not enough).
31187
+ ` + ` Run: skeleton audit docs
31188
+ ` + ` And: skeleton audit skills
31189
+ ` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
31190
+ }
31191
+ return 1;
31192
+ }
31193
+ }
30046
31194
  if (exitCode === 0) {
30047
31195
  const note = skipped > 0 ? ` (${skipped} path(s) skipped)` : "";
30048
31196
  console.log(`validate changed passed${note}.`);
@@ -30057,6 +31205,8 @@ function usage() {
30057
31205
  Commands:
30058
31206
  init [--force-hooks] [--skills] [--no-skills] [skills add flags…]
30059
31207
  audit docs|self|skills [--strict] [--json] [--paths=a,b] [--only=rule]
31208
+ [--fix[=doc-meta|anchors]] [--dry-run]
31209
+ build-plugin [path] [--check]
30060
31210
  validate changed [paths…] [--staged] [--base <ref>]
30061
31211
  register <path> [--topic=…] [--dry-run] [--json]
30062
31212
  customize resolve <slug> [--json]
@@ -30080,7 +31230,7 @@ function parseRegisterArgs(argv) {
30080
31230
  }
30081
31231
  return { path: path2, topic, dryRun, json };
30082
31232
  }
30083
- function main() {
31233
+ async function main() {
30084
31234
  const argv = process.argv.slice(2);
30085
31235
  const command = argv[0];
30086
31236
  if (!command || command === "--help" || command === "-h") {
@@ -30096,7 +31246,18 @@ function main() {
30096
31246
  }
30097
31247
  const options = parseAuditArgs(argv.slice(2));
30098
31248
  options.suite = sub;
30099
- process.exit(runAudit(options));
31249
+ process.exit(await runAudit(options));
31250
+ }
31251
+ if (command === "build-plugin") {
31252
+ const { entry, check } = parseBuildPluginArgs(argv.slice(1));
31253
+ const root2 = findRepoRoot();
31254
+ const result = await runBuildPlugin({ root: root2, entry, check });
31255
+ if (check) {
31256
+ console.log(result.checked.length === 0 ? "build-plugin --check: no plugins configured." : `build-plugin --check: ${result.checked.length} plugin(s) up to date.`);
31257
+ } else {
31258
+ console.log(result.built.length === 0 ? "build-plugin: no plugins configured." : `build-plugin: built ${result.built.length} plugin(s).`);
31259
+ }
31260
+ process.exit(0);
30100
31261
  }
30101
31262
  if (command === "validate" && argv[1] === "changed") {
30102
31263
  const rest = argv.slice(2);
@@ -30114,7 +31275,7 @@ function main() {
30114
31275
  else if (arg && !arg.startsWith("-"))
30115
31276
  paths.push(arg);
30116
31277
  }
30117
- process.exit(runValidateChanged({ paths, staged, base }));
31278
+ process.exit(await runValidateChanged({ paths, staged, base }));
30118
31279
  }
30119
31280
  if (command === "register") {
30120
31281
  const opts = parseRegisterArgs(argv.slice(1));