@csark0812/skeleton 1.5.6 → 1.5.7

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
@@ -3037,6 +3037,7 @@ var require_utils = __commonJS((exports, module) => {
3037
3037
  var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
3038
3038
  var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
3039
3039
  var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
3040
+ var isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/?]$/iu);
3040
3041
  function stringArrayToHexStripped(input) {
3041
3042
  let acc = "";
3042
3043
  let code = 0;
@@ -3180,7 +3181,7 @@ var require_utils = __commonJS((exports, module) => {
3180
3181
  continue;
3181
3182
  }
3182
3183
  } else if (input[0] === "/") {
3183
- if (input[1] === "." || input[1] === "/") {
3184
+ if (input[1] === ".") {
3184
3185
  output.push("/");
3185
3186
  break;
3186
3187
  }
@@ -3262,10 +3263,30 @@ var require_utils = __commonJS((exports, module) => {
3262
3263
  }
3263
3264
  return output;
3264
3265
  }
3266
+ var BYTE_HEX = new Array(256);
3267
+ {
3268
+ const HEX_DIGITS = "0123456789ABCDEF";
3269
+ for (let i = 0;i < 256; i++) {
3270
+ BYTE_HEX[i] = "%" + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 15];
3271
+ }
3272
+ }
3273
+ function isEscapeSafe(cp) {
3274
+ return cp >= 48 && cp <= 57 || cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122 || cp === 42 || cp === 43 || cp === 45 || cp === 46 || cp === 47 || cp === 64 || cp === 95;
3275
+ }
3276
+ function percentEncodeNonAscii(cp) {
3277
+ if (cp < 2048) {
3278
+ return BYTE_HEX[192 | cp >> 6] + BYTE_HEX[128 | cp & 63];
3279
+ }
3280
+ if (cp < 65536) {
3281
+ return BYTE_HEX[224 | cp >> 12] + BYTE_HEX[128 | cp >> 6 & 63] + BYTE_HEX[128 | cp & 63];
3282
+ }
3283
+ return BYTE_HEX[240 | cp >> 18] + BYTE_HEX[128 | cp >> 12 & 63] + BYTE_HEX[128 | cp >> 6 & 63] + BYTE_HEX[128 | cp & 63];
3284
+ }
3265
3285
  function normalizePathEncoding(input) {
3266
3286
  let output = "";
3267
3287
  for (let i = 0;i < input.length; i++) {
3268
- if (input[i] === "%" && i + 2 < input.length) {
3288
+ const ch = input[i];
3289
+ if (ch === "%" && i + 2 < input.length) {
3269
3290
  const hex = input.slice(i + 1, i + 3);
3270
3291
  if (isHexPair(hex)) {
3271
3292
  const normalizedHex = hex.toUpperCase();
@@ -3279,10 +3300,66 @@ var require_utils = __commonJS((exports, module) => {
3279
3300
  continue;
3280
3301
  }
3281
3302
  }
3282
- if (isPathCharacter(input[i])) {
3283
- output += input[i];
3303
+ if (isPathCharacter(ch)) {
3304
+ output += ch;
3305
+ } else {
3306
+ const code = input.charCodeAt(i);
3307
+ if (code < 128) {
3308
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
3309
+ } else if (code < 55296 || code > 57343) {
3310
+ output += percentEncodeNonAscii(code);
3311
+ } else if (code <= 56319 && i + 1 < input.length) {
3312
+ const low = input.charCodeAt(i + 1);
3313
+ if (low >= 56320 && low <= 57343) {
3314
+ output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
3315
+ i++;
3316
+ } else {
3317
+ output += percentEncodeNonAscii(65533);
3318
+ }
3319
+ } else {
3320
+ output += percentEncodeNonAscii(65533);
3321
+ }
3322
+ }
3323
+ }
3324
+ return output;
3325
+ }
3326
+ function normalizeQueryFragmentEncoding(input) {
3327
+ let output = "";
3328
+ for (let i = 0;i < input.length; i++) {
3329
+ const ch = input[i];
3330
+ if (ch === "%" && i + 2 < input.length) {
3331
+ const hex = input.slice(i + 1, i + 3);
3332
+ if (isHexPair(hex)) {
3333
+ const normalizedHex = hex.toUpperCase();
3334
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3335
+ if (isUnreserved(decoded)) {
3336
+ output += decoded;
3337
+ } else {
3338
+ output += "%" + normalizedHex;
3339
+ }
3340
+ i += 2;
3341
+ continue;
3342
+ }
3343
+ }
3344
+ if (isQueryFragmentCharacter(ch)) {
3345
+ output += ch;
3284
3346
  } else {
3285
- output += escape(input[i]);
3347
+ const code = input.charCodeAt(i);
3348
+ if (code < 128) {
3349
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
3350
+ } else if (code < 55296 || code > 57343) {
3351
+ output += percentEncodeNonAscii(code);
3352
+ } else if (code <= 56319 && i + 1 < input.length) {
3353
+ const low = input.charCodeAt(i + 1);
3354
+ if (low >= 56320 && low <= 57343) {
3355
+ output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
3356
+ i++;
3357
+ } else {
3358
+ output += percentEncodeNonAscii(65533);
3359
+ }
3360
+ } else {
3361
+ output += percentEncodeNonAscii(65533);
3362
+ }
3286
3363
  }
3287
3364
  }
3288
3365
  return output;
@@ -3290,7 +3367,8 @@ var require_utils = __commonJS((exports, module) => {
3290
3367
  function escapePreservingEscapes(input) {
3291
3368
  let output = "";
3292
3369
  for (let i = 0;i < input.length; i++) {
3293
- if (input[i] === "%" && i + 2 < input.length) {
3370
+ const ch = input[i];
3371
+ if (ch === "%" && i + 2 < input.length) {
3294
3372
  const hex = input.slice(i + 1, i + 3);
3295
3373
  if (isHexPair(hex)) {
3296
3374
  output += "%" + hex.toUpperCase();
@@ -3298,7 +3376,22 @@ var require_utils = __commonJS((exports, module) => {
3298
3376
  continue;
3299
3377
  }
3300
3378
  }
3301
- output += escape(input[i]);
3379
+ const code = input.charCodeAt(i);
3380
+ if (code < 128) {
3381
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
3382
+ } else if (code < 55296 || code > 57343) {
3383
+ output += percentEncodeNonAscii(code);
3384
+ } else if (code <= 56319 && i + 1 < input.length) {
3385
+ const low = input.charCodeAt(i + 1);
3386
+ if (low >= 56320 && low <= 57343) {
3387
+ output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
3388
+ i++;
3389
+ } else {
3390
+ output += percentEncodeNonAscii(65533);
3391
+ }
3392
+ } else {
3393
+ output += percentEncodeNonAscii(65533);
3394
+ }
3302
3395
  }
3303
3396
  return output;
3304
3397
  }
@@ -3332,6 +3425,7 @@ var require_utils = __commonJS((exports, module) => {
3332
3425
  reescapeHostDelimiters,
3333
3426
  normalizePercentEncoding,
3334
3427
  normalizePathEncoding,
3428
+ normalizeQueryFragmentEncoding,
3335
3429
  escapePreservingEscapes,
3336
3430
  removeDotSegments,
3337
3431
  isIPv4,
@@ -3517,7 +3611,7 @@ var require_schemes = __commonJS((exports, module) => {
3517
3611
 
3518
3612
  // node_modules/fast-uri/index.js
3519
3613
  var require_fast_uri = __commonJS((exports, module) => {
3520
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3614
+ var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, normalizeQueryFragmentEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3521
3615
  var { SCHEMES, getSchemeHandler } = require_schemes();
3522
3616
  function normalize(uri, options) {
3523
3617
  if (typeof uri === "string") {
@@ -3529,7 +3623,12 @@ var require_fast_uri = __commonJS((exports, module) => {
3529
3623
  }
3530
3624
  function resolve(baseURI, relativeURI, options) {
3531
3625
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3532
- const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true);
3626
+ const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
3627
+ const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
3628
+ if (baseMalformed || relativeMalformed) {
3629
+ throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed.");
3630
+ }
3631
+ const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
3533
3632
  schemelessOptions.skipEscape = true;
3534
3633
  return serialize(resolved, schemelessOptions);
3535
3634
  }
@@ -3655,6 +3754,8 @@ var require_fast_uri = __commonJS((exports, module) => {
3655
3754
  return uriTokens.join("");
3656
3755
  }
3657
3756
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
3757
+ var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
3758
+ var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
3658
3759
  function getParseError(parsed, matches) {
3659
3760
  if (matches[2] !== undefined && parsed.path && parsed.path[0] !== "/") {
3660
3761
  return 'URI path must start with "/" when authority is present.';
@@ -3684,9 +3785,28 @@ var require_fast_uri = __commonJS((exports, module) => {
3684
3785
  uri = "//" + uri;
3685
3786
  }
3686
3787
  }
3788
+ const authorityMatch = uri.match(AUTHORITY_PREFIX);
3789
+ if (authorityMatch !== null && authorityMatch[1].indexOf("\\") !== -1) {
3790
+ parsed.error = "URI authority must not contain a literal backslash.";
3791
+ malformedAuthorityOrPort = true;
3792
+ }
3793
+ const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION);
3794
+ if (introducerMatch !== null) {
3795
+ const region = introducerMatch[1];
3796
+ const normalizedRegion = region.replace(/[\t\n\r]/g, "");
3797
+ if (normalizedRegion.length >= 2) {
3798
+ if (normalizedRegion.slice(0, 2) !== "//") {
3799
+ parsed.error = parsed.error || "URI authority must not contain a literal backslash.";
3800
+ malformedAuthorityOrPort = true;
3801
+ } else if (region.length !== normalizedRegion.length) {
3802
+ parsed.error = parsed.error || "URI authority introducer must not contain whitespace.";
3803
+ malformedAuthorityOrPort = true;
3804
+ }
3805
+ }
3806
+ }
3687
3807
  const matches = uri.match(URI_PARSE);
3688
3808
  if (matches) {
3689
- parsed.scheme = matches[1];
3809
+ parsed.scheme = matches[1] === undefined ? undefined : matches[1].toLowerCase();
3690
3810
  parsed.userinfo = matches[3];
3691
3811
  parsed.host = matches[4];
3692
3812
  parsed.port = parseInt(matches[5], 10);
@@ -3745,12 +3865,11 @@ var require_fast_uri = __commonJS((exports, module) => {
3745
3865
  if (parsed.path) {
3746
3866
  parsed.path = normalizePathEncoding(parsed.path);
3747
3867
  }
3868
+ if (parsed.query) {
3869
+ parsed.query = normalizeQueryFragmentEncoding(parsed.query);
3870
+ }
3748
3871
  if (parsed.fragment) {
3749
- try {
3750
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3751
- } catch {
3752
- parsed.error = parsed.error || "URI malformed";
3753
- }
3872
+ parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment);
3754
3873
  }
3755
3874
  }
3756
3875
  if (schemeHandler && schemeHandler.parse) {
@@ -15653,11 +15772,13 @@ var require_extend = __commonJS((exports, module) => {
15653
15772
 
15654
15773
  // src/cli.ts
15655
15774
  import { readFileSync as readFileSync22 } from "node:fs";
15775
+ import process7 from "node:process";
15656
15776
 
15657
15777
  // src/audit/config/load.ts
15658
15778
  var import_ajv = __toESM(require_ajv(), 1);
15659
15779
  import { existsSync, readFileSync } from "node:fs";
15660
15780
  import { dirname, join } from "node:path";
15781
+ import process2 from "node:process";
15661
15782
  import { fileURLToPath } from "node:url";
15662
15783
 
15663
15784
  // node_modules/yaml/dist/index.js
@@ -15745,7 +15866,7 @@ function isExternalLink(target) {
15745
15866
  return EXTERNAL_LINK_RE.test(target);
15746
15867
  }
15747
15868
  function isPlaceholderLink(target) {
15748
- return !target.includes("/") && !target.includes(".") && !target.startsWith("#");
15869
+ return !(target.includes("/") || target.includes(".") || target.startsWith("#"));
15749
15870
  }
15750
15871
  function escapeRegexLiteral(s) {
15751
15872
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -15826,7 +15947,7 @@ var COVERAGE_BUILTIN_EXCLUDES = [
15826
15947
  "**/fixtures/**",
15827
15948
  "templates/**"
15828
15949
  ];
15829
- function findRepoRoot(startDir = process.cwd()) {
15950
+ function findRepoRoot(startDir = process2.cwd()) {
15830
15951
  let dir = startDir;
15831
15952
  while (true) {
15832
15953
  if (existsSync(join(dir, ".skeleton", "config.yaml")))
@@ -16861,6 +16982,44 @@ function assertUnderSkeleton(root, absPath) {
16861
16982
  }
16862
16983
 
16863
16984
  // src/plugins/load.ts
16985
+ function rulesAgree(a, b) {
16986
+ return a.length === b.length && !a.some((rule, i) => rule.id !== b[i]?.id || rule.run !== b[i]?.run);
16987
+ }
16988
+ function policiesAgree(a, b) {
16989
+ return Array.isArray(a) && Array.isArray(b) && a.length === b.length && !a.some((p, i) => p !== b[i]);
16990
+ }
16991
+ function resolveRules(defaultRules, namedRules) {
16992
+ if (defaultRules !== undefined && namedRules !== undefined && !rulesAgree(defaultRules, namedRules)) {
16993
+ throw new Error("Plugin exports disagree on rules: default and named `rules` must match when both are set");
16994
+ }
16995
+ return defaultRules ?? namedRules;
16996
+ }
16997
+ function resolvePolicies(defaultPolicies, namedPolicies) {
16998
+ if (defaultPolicies !== undefined && namedPolicies !== undefined && !policiesAgree(defaultPolicies, namedPolicies)) {
16999
+ throw new Error("Plugin exports disagree on policies: default and named `policies` must match when both are set");
17000
+ }
17001
+ return defaultPolicies ?? namedPolicies;
17002
+ }
17003
+ function validateRules(rules) {
17004
+ for (const rule of rules) {
17005
+ if (!rule || typeof rule.id !== "string" || typeof rule.run !== "function") {
17006
+ throw new Error("Plugin rules must each have string id and run()");
17007
+ }
17008
+ }
17009
+ }
17010
+ function validatePolicies(policies) {
17011
+ if (policies === undefined)
17012
+ return;
17013
+ if (!Array.isArray(policies) || policies.some((p) => typeof p !== "string")) {
17014
+ throw new Error("Plugin policies must be string[] (globs relative to .skeleton/) — got non-array");
17015
+ }
17016
+ }
17017
+ function readDefaultExport(record) {
17018
+ const value = record.default;
17019
+ if (!("default" in record) || typeof value !== "object" || value === null)
17020
+ return null;
17021
+ return value;
17022
+ }
16864
17023
  function normalizeExport(mod) {
16865
17024
  if (!mod || typeof mod !== "object") {
16866
17025
  throw new Error("Plugin module must export { rules: AuditRule[]; policies?: string[] }");
@@ -16868,44 +17027,17 @@ function normalizeExport(mod) {
16868
17027
  const record = mod;
16869
17028
  const namedRules = Array.isArray(record.rules) ? record.rules : undefined;
16870
17029
  const namedPoliciesRaw = "policies" in record ? record.policies : undefined;
16871
- const def = "default" in record && record.default && typeof record.default === "object" ? record.default : null;
17030
+ const def = readDefaultExport(record);
16872
17031
  const defaultRules = def && Array.isArray(def.rules) ? def.rules : undefined;
16873
17032
  const defaultPoliciesRaw = def && "policies" in def ? def.policies : undefined;
16874
- let rules;
16875
- if (defaultRules !== undefined && namedRules !== undefined) {
16876
- if (defaultRules.length !== namedRules.length || defaultRules.some((rule, i) => rule.id !== namedRules[i]?.id || rule.run !== namedRules[i]?.run)) {
16877
- throw new Error("Plugin exports disagree on rules: default and named `rules` must match when both are set");
16878
- }
16879
- rules = defaultRules;
16880
- } else {
16881
- rules = defaultRules ?? namedRules;
16882
- }
16883
- let policies;
16884
- if (defaultPoliciesRaw !== undefined && namedPoliciesRaw !== undefined) {
16885
- if (!Array.isArray(defaultPoliciesRaw) || !Array.isArray(namedPoliciesRaw) || defaultPoliciesRaw.length !== namedPoliciesRaw.length || defaultPoliciesRaw.some((p, i) => p !== namedPoliciesRaw[i])) {
16886
- throw new Error("Plugin exports disagree on policies: default and named `policies` must match when both are set");
16887
- }
16888
- policies = defaultPoliciesRaw;
16889
- } else {
16890
- policies = defaultPoliciesRaw ?? namedPoliciesRaw;
16891
- }
17033
+ const rules = resolveRules(defaultRules, namedRules);
17034
+ const policies = resolvePolicies(defaultPoliciesRaw, namedPoliciesRaw);
16892
17035
  if (!Array.isArray(rules)) {
16893
17036
  throw new Error("Plugin module must export { rules: AuditRule[]; policies?: string[] }");
16894
17037
  }
16895
- for (const rule of rules) {
16896
- if (!rule || typeof rule.id !== "string" || typeof rule.run !== "function") {
16897
- throw new Error("Plugin rules must each have string id and run()");
16898
- }
16899
- }
16900
- if (policies !== undefined) {
16901
- if (!Array.isArray(policies) || policies.some((p) => typeof p !== "string")) {
16902
- throw new Error("Plugin policies must be string[] (globs relative to .skeleton/) — got non-array");
16903
- }
16904
- }
16905
- return {
16906
- rules,
16907
- policies
16908
- };
17038
+ validateRules(rules);
17039
+ validatePolicies(policies);
17040
+ return { rules, policies };
16909
17041
  }
16910
17042
  function expandPolicyGlobs(root, globs) {
16911
17043
  const base = skeletonDir(root);
@@ -16940,20 +17072,23 @@ function loadPoliciesFromGlobs(root, globs) {
16940
17072
  }
16941
17073
  return policies;
16942
17074
  }
17075
+ async function importBuiltPlugin(root, entry) {
17076
+ const tsAbs = resolvePluginTsPath(root, entry);
17077
+ const mjsAbs = mjsPathForTs(tsAbs);
17078
+ if (!existsSync4(mjsAbs)) {
17079
+ const rel = relative2(skeletonDir(root), tsAbs) || entry;
17080
+ throw new Error(`Plugin not built: ${rel} (missing ${relative2(root, mjsAbs) || mjsAbs}). Run: skeleton build-plugin`);
17081
+ }
17082
+ const mod = await import(pathToFileURL(mjsAbs).href);
17083
+ return normalizeExport(mod);
17084
+ }
16943
17085
  async function collectWiredPolicyRelPaths(root, config) {
16944
17086
  const entries = config.plugins ?? [];
16945
17087
  const wired = new Set;
16946
17088
  if (entries.length === 0)
16947
17089
  return wired;
16948
17090
  for (const entry of entries) {
16949
- const tsAbs = resolvePluginTsPath(root, entry);
16950
- const mjsAbs = mjsPathForTs(tsAbs);
16951
- if (!existsSync4(mjsAbs)) {
16952
- const rel = relative2(skeletonDir(root), tsAbs) || entry;
16953
- throw new Error(`Plugin not built: ${rel} (missing ${relative2(root, mjsAbs) || mjsAbs}). Run: skeleton build-plugin`);
16954
- }
16955
- const mod = await import(pathToFileURL(mjsAbs).href);
16956
- const normalized = normalizeExport(mod);
17091
+ const normalized = await importBuiltPlugin(root, entry);
16957
17092
  if (!normalized.policies?.length)
16958
17093
  continue;
16959
17094
  for (const abs of expandPolicyGlobs(root, normalized.policies)) {
@@ -16970,14 +17105,7 @@ async function loadPlugins(root, config) {
16970
17105
  const rules = [];
16971
17106
  const policies = [];
16972
17107
  for (const entry of entries) {
16973
- const tsAbs = resolvePluginTsPath(root, entry);
16974
- const mjsAbs = mjsPathForTs(tsAbs);
16975
- if (!existsSync4(mjsAbs)) {
16976
- const rel = relative2(skeletonDir(root), tsAbs) || entry;
16977
- throw new Error(`Plugin not built: ${rel} (missing ${relative2(root, mjsAbs) || mjsAbs}). Run: skeleton build-plugin`);
16978
- }
16979
- const mod = await import(pathToFileURL(mjsAbs).href);
16980
- const normalized = normalizeExport(mod);
17108
+ const normalized = await importBuiltPlugin(root, entry);
16981
17109
  rules.push(...normalized.rules);
16982
17110
  if (normalized.policies?.length) {
16983
17111
  policies.push(...loadPoliciesFromGlobs(root, normalized.policies));
@@ -17001,6 +17129,39 @@ var DEFAULT_SKILLS_LOCKFILE = "skills-lock.json";
17001
17129
  function isForeignLockSourceType(sourceType) {
17002
17130
  return sourceType !== "local";
17003
17131
  }
17132
+ function parseLockSkillEntry(input) {
17133
+ const { lockfileRel, slug, value, warnings } = input;
17134
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
17135
+ warnings.push(`${lockfileRel}: skill "${slug}" has invalid entry`);
17136
+ return null;
17137
+ }
17138
+ const entry = value;
17139
+ const source = entry.source;
17140
+ const sourceType = entry.sourceType;
17141
+ if (typeof source !== "string" || source.length === 0) {
17142
+ warnings.push(`${lockfileRel}: skill "${slug}" missing source`);
17143
+ return null;
17144
+ }
17145
+ if (typeof sourceType !== "string" || sourceType.length === 0) {
17146
+ warnings.push(`${lockfileRel}: skill "${slug}" missing sourceType`);
17147
+ return null;
17148
+ }
17149
+ const parsed = { source, sourceType };
17150
+ if (typeof entry.skillPath === "string")
17151
+ parsed.skillPath = entry.skillPath;
17152
+ if (typeof entry.computedHash === "string")
17153
+ parsed.computedHash = entry.computedHash;
17154
+ return parsed;
17155
+ }
17156
+ function parseLockSkills(lockfileRel, skillsRaw, warnings) {
17157
+ const entries = {};
17158
+ for (const [slug, value] of Object.entries(skillsRaw)) {
17159
+ const parsed = parseLockSkillEntry({ lockfileRel, slug, value, warnings });
17160
+ if (parsed)
17161
+ entries[slug] = parsed;
17162
+ }
17163
+ return entries;
17164
+ }
17004
17165
  function loadSkillsLock(root, lockfileRel = DEFAULT_SKILLS_LOCKFILE) {
17005
17166
  const warnings = [];
17006
17167
  const abs = join4(root, lockfileRel);
@@ -17028,31 +17189,11 @@ function loadSkillsLock(root, lockfileRel = DEFAULT_SKILLS_LOCKFILE) {
17028
17189
  warnings.push(`${lockfileRel}: missing or invalid "skills" object`);
17029
17190
  return { lockfile: lockfileRel, entries: {}, warnings };
17030
17191
  }
17031
- const entries = {};
17032
- for (const [slug, value] of Object.entries(skillsRaw)) {
17033
- if (!value || typeof value !== "object" || Array.isArray(value)) {
17034
- warnings.push(`${lockfileRel}: skill "${slug}" has invalid entry`);
17035
- continue;
17036
- }
17037
- const entry = value;
17038
- const source = entry.source;
17039
- const sourceType = entry.sourceType;
17040
- if (typeof source !== "string" || source.length === 0) {
17041
- warnings.push(`${lockfileRel}: skill "${slug}" missing source`);
17042
- continue;
17043
- }
17044
- if (typeof sourceType !== "string" || sourceType.length === 0) {
17045
- warnings.push(`${lockfileRel}: skill "${slug}" missing sourceType`);
17046
- continue;
17047
- }
17048
- const parsed = { source, sourceType };
17049
- if (typeof entry.skillPath === "string")
17050
- parsed.skillPath = entry.skillPath;
17051
- if (typeof entry.computedHash === "string")
17052
- parsed.computedHash = entry.computedHash;
17053
- entries[slug] = parsed;
17054
- }
17055
- return { lockfile: lockfileRel, entries, warnings };
17192
+ return {
17193
+ lockfile: lockfileRel,
17194
+ entries: parseLockSkills(lockfileRel, skillsRaw, warnings),
17195
+ warnings
17196
+ };
17056
17197
  }
17057
17198
  function classifySkillOwnership(slug, provenance, ownership) {
17058
17199
  const ownedOverrides = new Set(ownership?.ownedSlugs ?? []);
@@ -17111,7 +17252,7 @@ function listNestedSlugs(root, relRoot) {
17111
17252
  const absRoot = join5(root, relRoot);
17112
17253
  if (!existsSync6(absRoot))
17113
17254
  return [];
17114
- return readdirSync2(absRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !NESTED_EXCLUDED_DIRS.has(entry.name)).filter((entry) => existsSync6(join5(absRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
17255
+ return readdirSync2(absRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !NESTED_EXCLUDED_DIRS.has(entry.name)).filter((entry) => existsSync6(join5(absRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
17115
17256
  }
17116
17257
  function listFlatSlugs(root) {
17117
17258
  const slugs = [];
@@ -17124,34 +17265,46 @@ function listFlatSlugs(root) {
17124
17265
  slugs.push(entry.name);
17125
17266
  }
17126
17267
  }
17127
- return slugs.sort();
17268
+ return slugs.sort((a, b) => a.localeCompare(b));
17269
+ }
17270
+ function shouldSkipAgentsRoot(root, relRoot, claudeReal) {
17271
+ if (relRoot !== ".agents/skills" || !claudeReal)
17272
+ return false;
17273
+ const abs = join5(root, relRoot);
17274
+ const agentsReal = safeRealpath(abs);
17275
+ if (agentsReal && agentsReal === claudeReal)
17276
+ return true;
17277
+ const linkTarget = safeRealpath(abs);
17278
+ if (linkTarget === claudeReal)
17279
+ return true;
17280
+ try {
17281
+ const link = readlinkSync(abs);
17282
+ if (link && safeRealpath(join5(root, link)) === claudeReal)
17283
+ return true;
17284
+ } catch {}
17285
+ return false;
17286
+ }
17287
+ function addNestedSkillRoot(input) {
17288
+ const { roots, root, relRoot } = input;
17289
+ let { claudeReal } = input;
17290
+ const abs = join5(root, relRoot);
17291
+ if (!existsSync6(abs))
17292
+ return claudeReal;
17293
+ if (relRoot === ".claude/skills") {
17294
+ claudeReal = safeRealpath(abs);
17295
+ }
17296
+ if (shouldSkipAgentsRoot(root, relRoot, claudeReal))
17297
+ return claudeReal;
17298
+ if (listNestedSlugs(root, relRoot).length > 0 || existsSync6(abs)) {
17299
+ roots.push({ kind: "nested", relPath: relRoot });
17300
+ }
17301
+ return claudeReal;
17128
17302
  }
17129
17303
  function detectSkillRoots(root) {
17130
17304
  const roots = [];
17131
17305
  let claudeReal = null;
17132
17306
  for (const relRoot of NESTED_SKILL_ROOTS) {
17133
- const abs = join5(root, relRoot);
17134
- if (!existsSync6(abs))
17135
- continue;
17136
- if (relRoot === ".claude/skills") {
17137
- claudeReal = safeRealpath(abs);
17138
- }
17139
- if (relRoot === ".agents/skills" && claudeReal) {
17140
- const agentsReal = safeRealpath(abs);
17141
- if (agentsReal && agentsReal === claudeReal)
17142
- continue;
17143
- const linkTarget = safeRealpath(abs);
17144
- if (linkTarget === claudeReal)
17145
- continue;
17146
- try {
17147
- const link = readlinkSync(abs);
17148
- if (link && claudeReal && safeRealpath(join5(root, link)) === claudeReal)
17149
- continue;
17150
- } catch {}
17151
- }
17152
- if (listNestedSlugs(root, relRoot).length > 0 || existsSync6(abs)) {
17153
- roots.push({ kind: "nested", relPath: relRoot });
17154
- }
17307
+ claudeReal = addNestedSkillRoot({ roots, root, relRoot, claudeReal });
17155
17308
  }
17156
17309
  const flatSlugs = listFlatSlugs(root);
17157
17310
  if (flatSlugs.length > 0) {
@@ -17193,22 +17346,30 @@ function resolveSkillPath(index, root, slug) {
17193
17346
  function isSkillPath(relPath, index) {
17194
17347
  return skillSlugForPath(relPath, index) !== null;
17195
17348
  }
17349
+ function nestedSlugForPath(normalized, skillRoot, index) {
17350
+ const prefix = `${skillRoot.relPath}/`;
17351
+ if (!normalized.startsWith(prefix))
17352
+ return null;
17353
+ const slug = normalized.slice(prefix.length).split("/")[0];
17354
+ return slug && index.slugs.includes(slug) ? slug : null;
17355
+ }
17356
+ function flatSlugForPath(normalized, index) {
17357
+ const flat = new Set(index.flatSlugs);
17358
+ const first = normalized.split("/")[0];
17359
+ return first && flat.has(first) ? first : null;
17360
+ }
17196
17361
  function skillSlugForPath(relPath, index) {
17197
17362
  const normalized = normalizeRelPath(relPath);
17198
- const flat = new Set(index.flatSlugs);
17199
17363
  for (const skillRoot of index.roots) {
17200
17364
  if (skillRoot.kind === "nested") {
17201
- const prefix = `${skillRoot.relPath}/`;
17202
- if (!normalized.startsWith(prefix))
17203
- continue;
17204
- const slug = normalized.slice(prefix.length).split("/")[0];
17205
- if (slug && index.slugs.includes(slug))
17206
- return slug;
17365
+ const slug2 = nestedSlugForPath(normalized, skillRoot, index);
17366
+ if (slug2)
17367
+ return slug2;
17207
17368
  continue;
17208
17369
  }
17209
- const first = normalized.split("/")[0];
17210
- if (first && flat.has(first))
17211
- return first;
17370
+ const slug = flatSlugForPath(normalized, index);
17371
+ if (slug)
17372
+ return slug;
17212
17373
  }
17213
17374
  return null;
17214
17375
  }
@@ -17249,46 +17410,54 @@ function slugFromPath(filePath, workspaceRoot) {
17249
17410
  }
17250
17411
  return flatSlug;
17251
17412
  }
17413
+ function augmentPatternsForRoot(skillRoot, index, owned) {
17414
+ if (skillRoot.kind === "nested") {
17415
+ return index.ownedSlugs.map((slug) => `${skillRoot.relPath}/${slug}/**`);
17416
+ }
17417
+ return index.flatSlugs.filter((slug) => owned.has(slug)).map((slug) => `${slug}/**`);
17418
+ }
17252
17419
  function skillCollectAugments(index) {
17253
17420
  const owned = new Set(index.ownedSlugs);
17254
17421
  const patterns = [];
17255
17422
  for (const skillRoot of index.roots) {
17256
- if (skillRoot.kind === "nested") {
17257
- for (const slug of index.ownedSlugs) {
17258
- patterns.push(`${skillRoot.relPath}/${slug}/**`);
17259
- }
17260
- } else {
17261
- for (const slug of index.flatSlugs) {
17262
- if (!owned.has(slug))
17263
- continue;
17264
- patterns.push(`${slug}/**`);
17265
- }
17266
- }
17423
+ patterns.push(...augmentPatternsForRoot(skillRoot, index, owned));
17267
17424
  }
17268
17425
  return patterns;
17269
17426
  }
17427
+ function markdownPathsForSkillDir(root, absDir) {
17428
+ const paths = [];
17429
+ for (const abs of globSync("**/*.{md,mdc}", {
17430
+ cwd: absDir,
17431
+ absolute: true,
17432
+ onlyFiles: true,
17433
+ dot: true
17434
+ })) {
17435
+ paths.push(normalizeRelPath(relative3(root, abs)));
17436
+ }
17437
+ return paths;
17438
+ }
17439
+ function markdownPathsForRoot(root, skillRoot, owned) {
17440
+ const paths = [];
17441
+ const slugs = skillRoot.kind === "nested" ? listNestedSlugs(root, skillRoot.relPath) : listFlatSlugs(root);
17442
+ for (const slug of slugs) {
17443
+ if (!owned.has(slug))
17444
+ continue;
17445
+ const absDir = skillRoot.kind === "nested" ? join5(root, skillRoot.relPath, slug) : join5(root, slug);
17446
+ if (!existsSync6(absDir))
17447
+ continue;
17448
+ paths.push(...markdownPathsForSkillDir(root, absDir));
17449
+ }
17450
+ return paths;
17451
+ }
17270
17452
  function listSkillMarkdownPaths(root, index) {
17271
17453
  const owned = new Set(index.ownedSlugs);
17272
17454
  const paths = new Set;
17273
17455
  for (const skillRoot of index.roots) {
17274
- const slugs = skillRoot.kind === "nested" ? listNestedSlugs(root, skillRoot.relPath) : listFlatSlugs(root);
17275
- for (const slug of slugs) {
17276
- if (!owned.has(slug))
17277
- continue;
17278
- const absDir = skillRoot.kind === "nested" ? join5(root, skillRoot.relPath, slug) : join5(root, slug);
17279
- if (!existsSync6(absDir))
17280
- continue;
17281
- for (const abs of globSync("**/*.{md,mdc}", {
17282
- cwd: absDir,
17283
- absolute: true,
17284
- onlyFiles: true,
17285
- dot: true
17286
- })) {
17287
- paths.add(normalizeRelPath(relative3(root, abs)));
17288
- }
17456
+ for (const rel of markdownPathsForRoot(root, skillRoot, owned)) {
17457
+ paths.add(rel);
17289
17458
  }
17290
17459
  }
17291
- return [...paths].sort();
17460
+ return [...paths].sort((a, b) => a.localeCompare(b));
17292
17461
  }
17293
17462
  function listSkillSlugs(index) {
17294
17463
  return index.slugs;
@@ -17306,6 +17475,24 @@ function isMarkdownFile(absPath) {
17306
17475
  function shouldExclude(relPath, exclude) {
17307
17476
  return exclude.some((pattern) => matchesGlobScope(relPath, pattern));
17308
17477
  }
17478
+ function rememberMarkdownFile(input) {
17479
+ const { byReal, root, abs, exclude } = input;
17480
+ if (!isMarkdownFile(abs))
17481
+ return;
17482
+ const rel = normalizeRelPath(relative4(root, abs));
17483
+ if (shouldExclude(rel, exclude))
17484
+ return;
17485
+ let real;
17486
+ try {
17487
+ real = realpathSync4(abs);
17488
+ } catch {
17489
+ real = abs;
17490
+ }
17491
+ const existing = byReal.get(real);
17492
+ if (existing === undefined || abs === real && existing !== real) {
17493
+ byReal.set(real, abs);
17494
+ }
17495
+ }
17309
17496
  function expandPatterns(root, patterns, exclude) {
17310
17497
  const byReal = new Map;
17311
17498
  for (const pattern of patterns) {
@@ -17316,21 +17503,7 @@ function expandPatterns(root, patterns, exclude) {
17316
17503
  dot: true,
17317
17504
  ignore: exclude
17318
17505
  })) {
17319
- if (!isMarkdownFile(abs))
17320
- continue;
17321
- const rel = normalizeRelPath(relative4(root, abs));
17322
- if (shouldExclude(rel, exclude))
17323
- continue;
17324
- let real;
17325
- try {
17326
- real = realpathSync4(abs);
17327
- } catch {
17328
- real = abs;
17329
- }
17330
- const existing = byReal.get(real);
17331
- if (existing === undefined || abs === real && existing !== real) {
17332
- byReal.set(real, abs);
17333
- }
17506
+ rememberMarkdownFile({ byReal, root, abs, exclude });
17334
17507
  }
17335
17508
  }
17336
17509
  return [...byReal.values()];
@@ -17393,31 +17566,41 @@ function excludeForeignSkillDocMetaPaths(docMetaPaths, skillIndex) {
17393
17566
  return docMetaPaths;
17394
17567
  return docMetaPaths.filter((rel) => !isForeignSkillPath(rel, skillIndex));
17395
17568
  }
17396
- function collectDocMetaPaths(config, root, registryPaths, skillIndex) {
17569
+ function collectRegistryDocMeta(ctx) {
17397
17570
  const paths = [];
17398
- for (const abs of expandPatterns(root, ["docs/*/README.md"], mergedExcludes(config))) {
17399
- paths.push(normalizeRelPath(relative4(root, abs)));
17400
- }
17401
- const extras = ["docs/README.md", ".skeleton/registry.md"];
17402
- for (const file of extras) {
17403
- const abs = join6(root, file);
17404
- if (existsSync7(abs))
17405
- paths.push(normalizeRelPath(file));
17406
- }
17407
- for (const rel of registryPaths) {
17408
- if (!rel.endsWith(".md") && !rel.endsWith(".mdc"))
17571
+ for (const rel of ctx.registryPaths) {
17572
+ if (!(rel.endsWith(".md") || rel.endsWith(".mdc")))
17409
17573
  continue;
17410
- const abs = join6(root, rel);
17574
+ const abs = join6(ctx.root, rel);
17411
17575
  if (existsSync7(abs))
17412
17576
  paths.push(normalizeRelPath(rel));
17413
17577
  }
17414
- for (const abs of collectScanFiles(config, root, skillIndex)) {
17578
+ return paths;
17579
+ }
17580
+ function collectScannedDocMeta(ctx) {
17581
+ const paths = [];
17582
+ for (const abs of collectScanFiles(ctx.config, ctx.root, ctx.skillIndex)) {
17415
17583
  const content = readFileSync5(abs, "utf8");
17416
17584
  if (/<!--\s*doc-meta:/.test(content)) {
17417
- paths.push(normalizeRelPath(relative4(root, abs)));
17585
+ paths.push(normalizeRelPath(relative4(ctx.root, abs)));
17418
17586
  }
17419
17587
  }
17420
- return excludeForeignSkillDocMetaPaths([...new Set(paths)], skillIndex);
17588
+ return paths;
17589
+ }
17590
+ function collectDocMetaPaths(ctx) {
17591
+ const paths = [];
17592
+ for (const abs of expandPatterns(ctx.root, ["docs/*/README.md"], mergedExcludes(ctx.config))) {
17593
+ paths.push(normalizeRelPath(relative4(ctx.root, abs)));
17594
+ }
17595
+ const extras = ["docs/README.md", ".skeleton/registry.md"];
17596
+ for (const file of extras) {
17597
+ const abs = join6(ctx.root, file);
17598
+ if (existsSync7(abs))
17599
+ paths.push(normalizeRelPath(file));
17600
+ }
17601
+ paths.push(...collectRegistryDocMeta(ctx));
17602
+ paths.push(...collectScannedDocMeta(ctx));
17603
+ return excludeForeignSkillDocMetaPaths([...new Set(paths)], ctx.skillIndex);
17421
17604
  }
17422
17605
  function validateScanRoots(config, root) {
17423
17606
  const missing = [];
@@ -17440,31 +17623,34 @@ function filterToPaths(files, paths, root) {
17440
17623
  return normalizedPaths.some((path) => rel === path || rel.startsWith(`${path}/`));
17441
17624
  });
17442
17625
  }
17626
+ function addExplicitPath(out, root, raw) {
17627
+ const rel = normalizeRelPath(raw);
17628
+ const abs = join6(root, rel);
17629
+ if (!existsSync7(abs))
17630
+ return;
17631
+ if (isMarkdownFile(rel)) {
17632
+ out.add(abs);
17633
+ return;
17634
+ }
17635
+ try {
17636
+ if (!statSync2(abs).isDirectory())
17637
+ return;
17638
+ } catch {
17639
+ return;
17640
+ }
17641
+ for (const md of globSync("**/*.{md,mdc}", {
17642
+ cwd: abs,
17643
+ absolute: true,
17644
+ onlyFiles: true,
17645
+ dot: true
17646
+ })) {
17647
+ out.add(md);
17648
+ }
17649
+ }
17443
17650
  function includeExplicitMarkdownPaths(files, paths, root) {
17444
17651
  const out = new Set(files);
17445
17652
  for (const raw of paths) {
17446
- const rel = normalizeRelPath(raw);
17447
- const abs = join6(root, rel);
17448
- if (!existsSync7(abs))
17449
- continue;
17450
- if (isMarkdownFile(rel)) {
17451
- out.add(abs);
17452
- continue;
17453
- }
17454
- try {
17455
- if (!statSync2(abs).isDirectory())
17456
- continue;
17457
- } catch {
17458
- continue;
17459
- }
17460
- for (const md of globSync("**/*.{md,mdc}", {
17461
- cwd: abs,
17462
- absolute: true,
17463
- onlyFiles: true,
17464
- dot: true
17465
- })) {
17466
- out.add(md);
17467
- }
17653
+ addExplicitPath(out, root, raw);
17468
17654
  }
17469
17655
  return [...out];
17470
17656
  }
@@ -17501,9 +17687,6 @@ function parseRegistry(root) {
17501
17687
  }
17502
17688
  return { paths: [...new Set(paths)], hasTableHeader };
17503
17689
  }
17504
- function parseRegistryPaths(root) {
17505
- return parseRegistry(root).paths;
17506
- }
17507
17690
 
17508
17691
  // src/audit/core/context.ts
17509
17692
  function createContext(options = {}) {
@@ -17519,7 +17702,12 @@ function createContext(options = {}) {
17519
17702
  files = filterToPaths(files, options.paths, root);
17520
17703
  }
17521
17704
  const registry = parseRegistry(root);
17522
- const allDocMetaPaths = collectDocMetaPaths(config, root, registry.paths, skillIndex);
17705
+ const allDocMetaPaths = collectDocMetaPaths({
17706
+ config,
17707
+ root,
17708
+ registryPaths: registry.paths,
17709
+ skillIndex
17710
+ });
17523
17711
  return {
17524
17712
  root,
17525
17713
  config,
@@ -21791,9 +21979,9 @@ function factoryTitle(effects, ok2, nok, type, markerType, stringType) {
21791
21979
  return atBreak(code);
21792
21980
  }
21793
21981
  effects.consume(code);
21794
- return code === codes.backslash ? escape2 : inside;
21982
+ return code === codes.backslash ? escape : inside;
21795
21983
  }
21796
- function escape2(code) {
21984
+ function escape(code) {
21797
21985
  if (code === marker || code === codes.backslash) {
21798
21986
  effects.consume(code);
21799
21987
  return inside;
@@ -28820,183 +29008,213 @@ function stripYamlFrontmatter(content3) {
28820
29008
  return "";
28821
29009
  return content3;
28822
29010
  }
28823
- function destinationConsumesToSliceEnd(slice, afterDest) {
28824
- let i = afterDest;
28825
- if (i >= slice.length)
28826
- return false;
28827
- if (slice[i] === ")") {
28828
- return i === slice.length - 1;
28829
- }
28830
- const first = slice[i];
28831
- if (first === undefined || !/\s/.test(first))
28832
- return false;
29011
+ function skipWhitespace(slice, start) {
29012
+ let i = start;
28833
29013
  while (i < slice.length) {
28834
29014
  const ch = slice[i];
28835
29015
  if (ch === undefined || !/\s/.test(ch))
28836
29016
  break;
28837
29017
  i++;
28838
29018
  }
28839
- if (i >= slice.length)
28840
- return false;
28841
- if (slice[i] === ")") {
28842
- return i === slice.length - 1;
28843
- }
28844
- const open = slice[i];
29019
+ return i;
29020
+ }
29021
+ function skipTitle(slice, start) {
29022
+ if (start >= slice.length)
29023
+ return start;
29024
+ const open = slice[start];
28845
29025
  if (open !== '"' && open !== "'" && open !== "(")
28846
- return false;
29026
+ return start;
28847
29027
  const close = open === "(" ? ")" : open;
28848
- i++;
29028
+ let i = start + 1;
28849
29029
  while (i < slice.length && slice[i] !== close)
28850
29030
  i++;
29031
+ return i < slice.length ? i + 1 : i;
29032
+ }
29033
+ function destinationConsumesToSliceEnd(slice, afterDest) {
29034
+ if (afterDest >= slice.length)
29035
+ return false;
29036
+ if (slice[afterDest] === ")")
29037
+ return afterDest === slice.length - 1;
29038
+ const first = slice[afterDest];
29039
+ if (first === undefined || !/\s/.test(first))
29040
+ return false;
29041
+ let i = skipWhitespace(slice, afterDest);
28851
29042
  if (i >= slice.length)
28852
29043
  return false;
28853
- i++;
28854
- while (i < slice.length) {
28855
- const ch = slice[i];
28856
- if (ch === undefined || !/\s/.test(ch))
28857
- break;
28858
- i++;
28859
- }
29044
+ if (slice[i] === ")")
29045
+ return i === slice.length - 1;
29046
+ i = skipTitle(slice, i);
29047
+ i = skipWhitespace(slice, i);
28860
29048
  return i === slice.length - 1 && slice[i] === ")";
28861
29049
  }
28862
- function findUrlSpanInSlice(content3, nodeStart, nodeEnd, url) {
28863
- const slice = content3.slice(nodeStart, nodeEnd);
29050
+ function spanForAngleUrl(slice, after) {
29051
+ if (!slice.content.startsWith(`<${slice.url}>`, after))
29052
+ return null;
29053
+ const afterDest = after + 2 + slice.url.length;
29054
+ if (!destinationConsumesToSliceEnd(slice.content, afterDest))
29055
+ return null;
29056
+ const urlStart = slice.nodeStart + after + 1;
29057
+ return { urlStart, urlEnd: urlStart + slice.url.length };
29058
+ }
29059
+ function spanForBareUrl(slice, after) {
29060
+ if (!slice.content.startsWith(slice.url, after))
29061
+ return null;
29062
+ const next = slice.content[after + slice.url.length];
29063
+ if (!(next === ")" || next !== undefined && /\s/.test(next)))
29064
+ return null;
29065
+ if (!destinationConsumesToSliceEnd(slice.content, after + slice.url.length))
29066
+ return null;
29067
+ const urlStart = slice.nodeStart + after;
29068
+ return { urlStart, urlEnd: urlStart + slice.url.length };
29069
+ }
29070
+ function tryLinkDestination(slice, openParen) {
29071
+ const after = openParen + 2;
29072
+ return spanForAngleUrl(slice, after) ?? spanForBareUrl(slice, after);
29073
+ }
29074
+ function findUrlSpanInSlice(slice) {
29075
+ const nodeSlice = slice.content.slice(slice.nodeStart, slice.nodeEnd);
28864
29076
  let searchFrom = 0;
28865
- while (searchFrom < slice.length) {
28866
- const openParen = slice.indexOf("](", searchFrom);
29077
+ while (searchFrom < nodeSlice.length) {
29078
+ const openParen = nodeSlice.indexOf("](", searchFrom);
28867
29079
  if (openParen === -1)
28868
29080
  break;
28869
- const after = openParen + 2;
28870
- if (slice.startsWith(`<${url}>`, after)) {
28871
- const afterDest = after + 2 + url.length;
28872
- if (destinationConsumesToSliceEnd(slice, afterDest)) {
28873
- const urlStart = nodeStart + after + 1;
28874
- return { urlStart, urlEnd: urlStart + url.length };
28875
- }
28876
- } else if (slice.startsWith(url, after)) {
28877
- const next = slice[after + url.length];
28878
- if (next === ")" || next !== undefined && /\s/.test(next)) {
28879
- if (destinationConsumesToSliceEnd(slice, after + url.length)) {
28880
- const urlStart = nodeStart + after;
28881
- return { urlStart, urlEnd: urlStart + url.length };
28882
- }
28883
- }
28884
- }
29081
+ const span = tryLinkDestination({ content: nodeSlice, nodeStart: slice.nodeStart, nodeEnd: slice.nodeEnd, url: slice.url }, openParen);
29082
+ if (span)
29083
+ return span;
28885
29084
  searchFrom = openParen + 1;
28886
29085
  }
28887
- const auto = `<${url}>`;
28888
- if (slice === auto) {
28889
- return { urlStart: nodeStart + 1, urlEnd: nodeStart + 1 + url.length };
29086
+ return matchAutolinkOrBare(nodeSlice, slice);
29087
+ }
29088
+ function matchAutolinkOrBare(nodeSlice, slice) {
29089
+ const auto = `<${slice.url}>`;
29090
+ if (nodeSlice === auto) {
29091
+ return { urlStart: slice.nodeStart + 1, urlEnd: slice.nodeStart + 1 + slice.url.length };
28890
29092
  }
28891
- if (slice === url) {
28892
- return { urlStart: nodeStart, urlEnd: nodeEnd };
29093
+ if (nodeSlice === slice.url) {
29094
+ return { urlStart: slice.nodeStart, urlEnd: slice.nodeEnd };
28893
29095
  }
28894
- const trimmed = slice.trim();
29096
+ const trimmed = nodeSlice.trim();
28895
29097
  if (trimmed === auto) {
28896
- const lead = slice.indexOf(auto);
29098
+ const lead = nodeSlice.indexOf(auto);
28897
29099
  if (lead !== -1) {
28898
- return { urlStart: nodeStart + lead + 1, urlEnd: nodeStart + lead + 1 + url.length };
29100
+ return {
29101
+ urlStart: slice.nodeStart + lead + 1,
29102
+ urlEnd: slice.nodeStart + lead + 1 + slice.url.length
29103
+ };
28899
29104
  }
28900
29105
  }
28901
- if (trimmed === url) {
28902
- const lead = slice.indexOf(url);
29106
+ if (trimmed === slice.url) {
29107
+ const lead = nodeSlice.indexOf(slice.url);
28903
29108
  if (lead !== -1) {
28904
- return { urlStart: nodeStart + lead, urlEnd: nodeStart + lead + url.length };
29109
+ return {
29110
+ urlStart: slice.nodeStart + lead,
29111
+ urlEnd: slice.nodeStart + lead + slice.url.length
29112
+ };
28905
29113
  }
28906
29114
  }
28907
- return;
28908
29115
  }
28909
- function findUrlInDefinitionSlice(content3, nodeStart, nodeEnd, url) {
28910
- const slice = content3.slice(nodeStart, nodeEnd);
28911
- const labelEnd2 = slice.indexOf("]:");
29116
+ function findUrlInDefinitionSlice(slice) {
29117
+ const nodeSlice = slice.content.slice(slice.nodeStart, slice.nodeEnd);
29118
+ const labelEnd2 = nodeSlice.indexOf("]:");
28912
29119
  if (labelEnd2 === -1)
28913
29120
  return;
28914
29121
  let i = labelEnd2 + 2;
28915
- while (i < slice.length && /\s/.test(slice[i] ?? ""))
29122
+ while (i < nodeSlice.length && /\s/.test(nodeSlice[i] ?? ""))
28916
29123
  i++;
28917
- if (slice.startsWith(`<${url}>`, i)) {
28918
- const urlStart = nodeStart + i + 1;
28919
- return { urlStart, urlEnd: urlStart + url.length };
29124
+ if (nodeSlice.startsWith(`<${slice.url}>`, i)) {
29125
+ const urlStart = slice.nodeStart + i + 1;
29126
+ return { urlStart, urlEnd: urlStart + slice.url.length };
28920
29127
  }
28921
- if (slice.startsWith(url, i)) {
28922
- const urlStart = nodeStart + i;
28923
- return { urlStart, urlEnd: urlStart + url.length };
29128
+ if (nodeSlice.startsWith(slice.url, i)) {
29129
+ const urlStart = slice.nodeStart + i;
29130
+ return { urlStart, urlEnd: urlStart + slice.url.length };
28924
29131
  }
28925
- return;
29132
+ }
29133
+ function definitionFromNode(content3, node2, defs) {
29134
+ if (!(("identifier" in node2) && ("url" in node2)))
29135
+ return;
29136
+ const id = String(node2.identifier).toLowerCase();
29137
+ if (defs.has(id))
29138
+ return;
29139
+ const url = typeof node2.url === "string" ? node2.url : "";
29140
+ if (!url)
29141
+ return;
29142
+ const start = node2.position?.start?.offset;
29143
+ const end = node2.position?.end?.offset;
29144
+ if (start === undefined || end === undefined)
29145
+ return;
29146
+ const span = findUrlInDefinitionSlice({ content: content3, nodeStart: start, nodeEnd: end, url });
29147
+ if (!span)
29148
+ return;
29149
+ defs.set(id, {
29150
+ url,
29151
+ urlStart: span.urlStart,
29152
+ urlEnd: span.urlEnd,
29153
+ line: lineFromOffset(content3, start) ?? 1
29154
+ });
28926
29155
  }
28927
29156
  function collectReferenceDefinitions(content3, tree) {
28928
29157
  const defs = new Map;
28929
29158
  visit2(tree, (node2) => {
28930
29159
  if (node2.type !== "definition")
28931
29160
  return;
28932
- if (!("identifier" in node2) || !("url" in node2))
28933
- return;
28934
- const id = String(node2.identifier).toLowerCase();
28935
- if (defs.has(id))
28936
- return;
28937
- const url = typeof node2.url === "string" ? node2.url : "";
28938
- if (!url)
28939
- return;
28940
- const start = node2.position?.start.offset;
28941
- const end = node2.position?.end.offset;
28942
- if (start === undefined || end === undefined)
28943
- return;
28944
- const span = findUrlInDefinitionSlice(content3, start, end, url);
28945
- if (!span)
28946
- return;
28947
- defs.set(id, {
28948
- url,
28949
- urlStart: span.urlStart,
28950
- urlEnd: span.urlEnd,
28951
- line: lineFromOffset(content3, start) ?? 1
28952
- });
29161
+ definitionFromNode(content3, node2, defs);
28953
29162
  });
28954
29163
  return defs;
28955
29164
  }
29165
+ function linkFromDirectNode(content3, node2) {
29166
+ const target = node2.url.trim();
29167
+ const start = node2.position?.start?.offset;
29168
+ const end = node2.position?.end?.offset;
29169
+ const span = start !== undefined && end !== undefined ? findUrlSpanInSlice({ content: content3, nodeStart: start, nodeEnd: end, url: target }) : undefined;
29170
+ return {
29171
+ target,
29172
+ line: lineFromOffset(content3, node2.position?.start?.offset),
29173
+ urlStart: span?.urlStart,
29174
+ urlEnd: span?.urlEnd
29175
+ };
29176
+ }
29177
+ function linkFromReference(id, refDefs) {
29178
+ const def = refDefs.get(id);
29179
+ if (!def)
29180
+ return null;
29181
+ return {
29182
+ target: def.url.trim(),
29183
+ line: def.line,
29184
+ urlStart: def.urlStart,
29185
+ urlEnd: def.urlEnd
29186
+ };
29187
+ }
28956
29188
  function extractLinksFromMarkdown(content3, _filePath) {
28957
29189
  const tree = processor.parse(content3);
28958
29190
  const refDefs = collectReferenceDefinitions(content3, tree);
28959
29191
  const links = [];
28960
29192
  visit2(tree, (node2) => {
28961
29193
  if (node2.type === "link" && "url" in node2 && typeof node2.url === "string") {
28962
- const target = node2.url.trim();
28963
- const start = node2.position?.start.offset;
28964
- const end = node2.position?.end.offset;
28965
- const span = start !== undefined && end !== undefined ? findUrlSpanInSlice(content3, start, end, target) : undefined;
28966
- links.push({
28967
- target,
28968
- line: lineFromOffset(content3, node2.position?.start.offset),
28969
- urlStart: span?.urlStart,
28970
- urlEnd: span?.urlEnd
28971
- });
29194
+ links.push(linkFromDirectNode(content3, node2));
28972
29195
  }
28973
29196
  if (node2.type === "linkReference" && "identifier" in node2) {
28974
- const id = String(node2.identifier).toLowerCase();
28975
- const def = refDefs.get(id);
28976
- if (def) {
28977
- links.push({
28978
- target: def.url.trim(),
28979
- line: def.line,
28980
- urlStart: def.urlStart,
28981
- urlEnd: def.urlEnd
28982
- });
28983
- }
29197
+ const refLink = linkFromReference(String(node2.identifier).toLowerCase(), refDefs);
29198
+ if (refLink)
29199
+ links.push(refLink);
28984
29200
  }
28985
29201
  });
28986
29202
  return links;
28987
29203
  }
29204
+ function textFromPhrasingNode(node2) {
29205
+ if (node2.type === "text" || node2.type === "inlineCode") {
29206
+ return "value" in node2 && node2.value !== undefined ? String(node2.value) : "";
29207
+ }
29208
+ return "";
29209
+ }
28988
29210
  function phrasingText(nodes) {
28989
29211
  if (!nodes?.length)
28990
29212
  return "";
28991
29213
  let out = "";
28992
29214
  for (const node2 of nodes) {
28993
- if (node2.type === "text" || node2.type === "inlineCode") {
28994
- out += "value" in node2 && node2.value !== undefined ? String(node2.value) : "";
28995
- continue;
28996
- }
28997
- if (node2.children?.length) {
29215
+ out += textFromPhrasingNode(node2);
29216
+ if (node2.children?.length)
28998
29217
  out += phrasingText(node2.children);
28999
- }
29000
29218
  }
29001
29219
  return out;
29002
29220
  }
@@ -29086,60 +29304,86 @@ function replaceAnchorInTarget(target, oldAnchor, newAnchor) {
29086
29304
  return target;
29087
29305
  return `${pathPart}#${newAnchor}${queryPart}`;
29088
29306
  }
29307
+ function anchorTargetReplacement(filePath, target) {
29308
+ if (isExternalLink(target) && !target.startsWith("#"))
29309
+ return null;
29310
+ if (isPlaceholderLink(target))
29311
+ return null;
29312
+ const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
29313
+ if (!anchor)
29314
+ return null;
29315
+ const resolved = resolveLink(filePath, target);
29316
+ if (!existsSync9(resolved))
29317
+ return null;
29318
+ const targetContent = readFileSync7(resolved, "utf8");
29319
+ const slugs = extractHeadingSlugs(targetContent, resolved);
29320
+ const anchorSlug = slugifyAnchor(anchor);
29321
+ if (slugs.has(anchorSlug))
29322
+ return null;
29323
+ const match = findBestAnchorMatch(anchorSlug, slugs);
29324
+ if (!match)
29325
+ return null;
29326
+ const nextTarget = replaceAnchorInTarget(target, anchor, match.slug);
29327
+ if (nextTarget === target)
29328
+ return null;
29329
+ return { nextTarget, match, anchor };
29330
+ }
29331
+ function pendingAnchorForLink(input) {
29332
+ const { filePath, content: content3, relFile, link: link2 } = input;
29333
+ const { target, line, urlStart, urlEnd } = link2;
29334
+ const replacement = anchorTargetReplacement(filePath, target);
29335
+ if (!replacement)
29336
+ return null;
29337
+ if (urlStart === undefined || urlEnd === undefined || content3.slice(urlStart, urlEnd) !== target) {
29338
+ return null;
29339
+ }
29340
+ const lineLabel = line ? `${relFile}:${line}` : relFile;
29341
+ return {
29342
+ urlStart,
29343
+ urlEnd,
29344
+ from: target,
29345
+ to: replacement.nextTarget,
29346
+ description: `${lineLabel} #${replacement.anchor} → #${replacement.match.slug} (score ${replacement.match.score.toFixed(2)})`
29347
+ };
29348
+ }
29349
+ function applyPendingEdits(content3, pending) {
29350
+ const uniqueBySpan = new Map;
29351
+ for (const edit of pending) {
29352
+ uniqueBySpan.set(`${edit.urlStart}:${edit.urlEnd}:${edit.from}`, edit);
29353
+ }
29354
+ const uniquePending = [...uniqueBySpan.values()].sort((a, b) => b.urlStart - a.urlStart);
29355
+ let updated = content3;
29356
+ const descriptions = [];
29357
+ for (const edit of uniquePending) {
29358
+ if (updated.slice(edit.urlStart, edit.urlEnd) !== edit.from)
29359
+ continue;
29360
+ updated = updated.slice(0, edit.urlStart) + edit.to + updated.slice(edit.urlEnd);
29361
+ descriptions.push(edit.description);
29362
+ }
29363
+ if (updated === content3 || descriptions.length === 0)
29364
+ return null;
29365
+ return { content: updated, descriptions };
29366
+ }
29367
+ function collectFileAnchorFixes(ctx, filePath) {
29368
+ const content3 = readFileContent(filePath);
29369
+ const links = extractLinksFromMarkdown(content3, filePath);
29370
+ const relFile = relPath(filePath, ctx.root);
29371
+ const pending = [];
29372
+ for (const link2 of links) {
29373
+ const edit = pendingAnchorForLink({ ctx, filePath, content: content3, relFile, link: link2 });
29374
+ if (edit)
29375
+ pending.push(edit);
29376
+ }
29377
+ if (pending.length === 0)
29378
+ return null;
29379
+ return applyPendingEdits(content3, pending);
29380
+ }
29089
29381
  function collectAnchorFixes(ctx) {
29090
29382
  const editsByFile = new Map;
29091
29383
  for (const filePath of ctx.files) {
29092
- const content3 = readFileContent(filePath);
29093
- const links = extractLinksFromMarkdown(content3, filePath);
29094
- const pending = [];
29095
- const relFile = relPath(filePath, ctx.root);
29096
- for (const { target, line, urlStart, urlEnd } of links) {
29097
- if (isExternalLink(target) && !target.startsWith("#"))
29098
- continue;
29099
- if (isPlaceholderLink(target))
29100
- continue;
29101
- const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
29102
- if (!anchor)
29103
- continue;
29104
- const resolved = resolveLink(filePath, target);
29105
- if (!existsSync9(resolved))
29106
- continue;
29107
- const targetContent = readFileSync7(resolved, "utf8");
29108
- const slugs = extractHeadingSlugs(targetContent, resolved);
29109
- const anchorSlug = slugifyAnchor(anchor);
29110
- if (slugs.has(anchorSlug))
29111
- continue;
29112
- const match = findBestAnchorMatch(anchorSlug, slugs);
29113
- if (!match)
29114
- continue;
29115
- const nextTarget = replaceAnchorInTarget(target, anchor, match.slug);
29116
- if (nextTarget === target)
29117
- continue;
29118
- const lineLabel = line ? `${relFile}:${line}` : relFile;
29119
- const description = `${lineLabel} #${anchor} → #${match.slug} (score ${match.score.toFixed(2)})`;
29120
- if (urlStart !== undefined && urlEnd !== undefined && content3.slice(urlStart, urlEnd) === target) {
29121
- pending.push({ urlStart, urlEnd, from: target, to: nextTarget, description });
29122
- }
29123
- }
29124
- if (pending.length === 0)
29125
- continue;
29126
- const uniqueBySpan = new Map;
29127
- for (const edit of pending) {
29128
- uniqueBySpan.set(`${edit.urlStart}:${edit.urlEnd}:${edit.from}`, edit);
29129
- }
29130
- const uniquePending = [...uniqueBySpan.values()];
29131
- uniquePending.sort((a, b) => b.urlStart - a.urlStart);
29132
- let updated = content3;
29133
- const descriptions = [];
29134
- for (const edit of uniquePending) {
29135
- if (updated.slice(edit.urlStart, edit.urlEnd) !== edit.from)
29136
- continue;
29137
- updated = updated.slice(0, edit.urlStart) + edit.to + updated.slice(edit.urlEnd);
29138
- descriptions.push(edit.description);
29139
- }
29140
- if (updated === content3 || descriptions.length === 0)
29141
- continue;
29142
- editsByFile.set(filePath, { content: updated, descriptions });
29384
+ const result = collectFileAnchorFixes(ctx, filePath);
29385
+ if (result)
29386
+ editsByFile.set(filePath, result);
29143
29387
  }
29144
29388
  const edits = [];
29145
29389
  for (const [absPath, { content: content3, descriptions }] of editsByFile) {
@@ -29182,32 +29426,37 @@ function bumpDocMetaLastReviewed(content3, gitDate) {
29182
29426
  return null;
29183
29427
  return replaceDocMetaLastReviewed(content3, gitDate);
29184
29428
  }
29429
+ function docMetaFixForPath(ctx, relPath2) {
29430
+ const abs = join9(ctx.root, relPath2);
29431
+ if (!existsSync10(abs))
29432
+ return null;
29433
+ const content3 = readFileSync8(abs, "utf8");
29434
+ if (!DOC_META_RE.test(content3))
29435
+ return null;
29436
+ const reviewedStr = docMetaLastReviewed(content3);
29437
+ if (!reviewedStr)
29438
+ return null;
29439
+ const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
29440
+ if (Number.isNaN(reviewed.getTime()))
29441
+ return null;
29442
+ const gitDate = lastGitCommitDate(relPath2, ctx.root);
29443
+ if (!gitDate)
29444
+ return null;
29445
+ const updated = bumpDocMetaLastReviewed(content3, gitDate);
29446
+ if (!updated)
29447
+ return null;
29448
+ return {
29449
+ file: relPath2,
29450
+ description: `last-reviewed ${reviewedStr} → ${gitDate}`,
29451
+ content: updated
29452
+ };
29453
+ }
29185
29454
  function collectDocMetaFixes(ctx) {
29186
29455
  const edits = [];
29187
29456
  for (const relPath2 of ctx.docMetaPaths) {
29188
- const abs = join9(ctx.root, relPath2);
29189
- if (!existsSync10(abs))
29190
- continue;
29191
- const content3 = readFileSync8(abs, "utf8");
29192
- if (!DOC_META_RE.test(content3))
29193
- continue;
29194
- const reviewedStr = docMetaLastReviewed(content3);
29195
- if (!reviewedStr)
29196
- continue;
29197
- const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
29198
- if (Number.isNaN(reviewed.getTime()))
29199
- continue;
29200
- const gitDate = lastGitCommitDate(relPath2, ctx.root);
29201
- if (!gitDate)
29202
- continue;
29203
- const updated = bumpDocMetaLastReviewed(content3, gitDate);
29204
- if (!updated)
29205
- continue;
29206
- edits.push({
29207
- file: relPath2,
29208
- description: `last-reviewed ${reviewedStr} → ${gitDate}`,
29209
- content: updated
29210
- });
29457
+ const fix = docMetaFixForPath(ctx, relPath2);
29458
+ if (fix)
29459
+ edits.push(fix);
29211
29460
  }
29212
29461
  return edits;
29213
29462
  }
@@ -29249,28 +29498,31 @@ function overlayLastReviewed(targetContent, metaContent) {
29249
29498
  function underRoot(rootAbs, candidateAbs) {
29250
29499
  return candidateAbs === rootAbs || candidateAbs.startsWith(rootAbs + sep3);
29251
29500
  }
29252
- function resolveWritePath(root2, relFile) {
29253
- const rootResolved = resolve6(root2);
29254
- const abs = resolve6(rootResolved, relFile);
29255
- if (!underRoot(rootResolved, abs)) {
29256
- throw new Error(`Refusing autofix outside repo root: ${relFile}`);
29257
- }
29501
+ function shouldStopPathWalk(rootResolved, parent, cursor) {
29502
+ return parent === cursor || !underRoot(rootResolved, parent) && parent !== rootResolved;
29503
+ }
29504
+ function resolveExistingRealPath(rootResolved, abs, relFile) {
29258
29505
  const rootReal = existsSync11(rootResolved) ? realpathSync5(rootResolved) : rootResolved;
29259
29506
  let cursor = abs;
29260
- while (true) {
29261
- if (existsSync11(cursor)) {
29262
- const real = realpathSync5(cursor);
29263
- if (!underRoot(rootReal, real)) {
29264
- throw new Error(`Refusing autofix outside repo root: ${relFile}`);
29265
- }
29266
- return abs;
29267
- }
29507
+ while (!existsSync11(cursor)) {
29268
29508
  const parent = dirname6(cursor);
29269
- if (parent === cursor || !underRoot(rootResolved, parent) && parent !== rootResolved) {
29509
+ if (shouldStopPathWalk(rootResolved, parent, cursor))
29270
29510
  return abs;
29271
- }
29272
29511
  cursor = parent;
29273
29512
  }
29513
+ const real = realpathSync5(cursor);
29514
+ if (!underRoot(rootReal, real)) {
29515
+ throw new Error(`Refusing autofix outside repo root: ${relFile}`);
29516
+ }
29517
+ return abs;
29518
+ }
29519
+ function resolveWritePath(root2, relFile) {
29520
+ const rootResolved = resolve6(root2);
29521
+ const abs = resolve6(rootResolved, relFile);
29522
+ if (!underRoot(rootResolved, abs)) {
29523
+ throw new Error(`Refusing autofix outside repo root: ${relFile}`);
29524
+ }
29525
+ return resolveExistingRealPath(rootResolved, abs, relFile);
29274
29526
  }
29275
29527
  function applyFixes(ctx, options) {
29276
29528
  const kinds = new Set(options.kinds);
@@ -29314,13 +29566,14 @@ function fixKindsForOnly(kinds, only) {
29314
29566
  }
29315
29567
 
29316
29568
  // src/audit/core/report.ts
29317
- function issue(rule, file, message, opts) {
29569
+ function issue(rule, file, details) {
29570
+ const message = typeof details === "string" ? details : details.message;
29318
29571
  return {
29319
29572
  rule,
29320
29573
  file,
29321
- link: opts?.link,
29574
+ link: typeof details === "string" ? undefined : details.link,
29322
29575
  message,
29323
- severity: opts?.severity ?? "error"
29576
+ severity: typeof details === "string" ? "error" : details.severity ?? "error"
29324
29577
  };
29325
29578
  }
29326
29579
  function finalizeIssues(issues, strict) {
@@ -29333,31 +29586,39 @@ function printReport(issues, options) {
29333
29586
  const errors2 = finalized.filter((i) => i.severity === "error");
29334
29587
  const warnings = finalized.filter((i) => i.severity === "warning");
29335
29588
  const label = options.label ?? "Audit";
29336
- if (options.json) {
29337
- console.log(JSON.stringify({
29338
- label,
29339
- fileCount: options.fileCount,
29340
- errors: errors2.length,
29341
- warnings: warnings.length,
29342
- issues: finalized
29343
- }, null, 2));
29344
- return errors2.length > 0 ? 1 : 0;
29345
- }
29346
- if (warnings.length > 0) {
29347
- console.log(`${label} warnings:
29589
+ const ctx = { label, options, errors: errors2, warnings, finalized };
29590
+ if (options.json)
29591
+ return printJsonReport(ctx);
29592
+ return printTextReport(ctx);
29593
+ }
29594
+ function printJsonReport(ctx) {
29595
+ console.log(JSON.stringify({
29596
+ label: ctx.label,
29597
+ fileCount: ctx.options.fileCount,
29598
+ errors: ctx.errors.length,
29599
+ warnings: ctx.warnings.length,
29600
+ issues: ctx.finalized
29601
+ }, null, 2));
29602
+ return ctx.errors.length > 0 ? 1 : 0;
29603
+ }
29604
+ function printWarnings(label, warnings) {
29605
+ if (warnings.length === 0)
29606
+ return;
29607
+ console.log(`${label} warnings:
29348
29608
  `);
29349
- for (const i of warnings) {
29350
- const linkPart = i.link ? ` (${i.link})` : "";
29351
- console.log(`- ${i.file}${linkPart}: ${i.message}`);
29352
- }
29353
- console.log("");
29354
- }
29355
- if (errors2.length === 0) {
29356
- const warnNote = warnings.length > 0 ? `, ${warnings.length} warning(s)` : "";
29357
- const countNote = options.successSuffix ?? (options.fileCount !== undefined ? ` (${options.fileCount} files scanned${warnNote})` : "");
29358
- console.log(`${label} passed${countNote}.`);
29359
- return 0;
29609
+ for (const i of warnings) {
29610
+ const linkPart = i.link ? ` (${i.link})` : "";
29611
+ console.log(`- ${i.file}${linkPart}: ${i.message}`);
29360
29612
  }
29613
+ console.log("");
29614
+ }
29615
+ function printSuccess(label, options, warnings) {
29616
+ const warnNote = warnings.length > 0 ? `, ${warnings.length} warning(s)` : "";
29617
+ const countNote = options.successSuffix ?? (options.fileCount !== undefined ? ` (${options.fileCount} files scanned${warnNote})` : "");
29618
+ console.log(`${label} passed${countNote}.`);
29619
+ return 0;
29620
+ }
29621
+ function printErrors(label, errors2) {
29361
29622
  console.log(`${label} failed:
29362
29623
  `);
29363
29624
  for (const i of errors2) {
@@ -29366,6 +29627,13 @@ function printReport(issues, options) {
29366
29627
  }
29367
29628
  return 1;
29368
29629
  }
29630
+ function printTextReport(ctx) {
29631
+ const { label, options, errors: errors2, warnings } = ctx;
29632
+ printWarnings(label, warnings);
29633
+ if (errors2.length === 0)
29634
+ return printSuccess(label, options, warnings);
29635
+ return printErrors(label, errors2);
29636
+ }
29369
29637
 
29370
29638
  // src/references/check.ts
29371
29639
  import { existsSync as existsSync13, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
@@ -29425,73 +29693,86 @@ function findSharedRefLinks(content3, sourceFile) {
29425
29693
  }
29426
29694
  return links;
29427
29695
  }
29696
+ function findSiblingRefLinks(root2, content3, sourceFile) {
29697
+ const links = [];
29698
+ if (!/\/references\//.test(sourceFile))
29699
+ return links;
29700
+ const refsIdx = sourceFile.lastIndexOf("/references/");
29701
+ const withinRefs = sourceFile.slice(refsIdx + "/references/".length);
29702
+ const withinDir = withinRefs.includes("/") ? withinRefs.slice(0, withinRefs.lastIndexOf("/")) : "";
29703
+ const siblingRe = /\((?!https?:|#|\.\.\/)([a-z0-9./_-]+\.md)\)/gi;
29704
+ for (const match of content3.matchAll(siblingRe)) {
29705
+ const raw = normalizeRelPath(match[1] ?? "");
29706
+ if (!raw)
29707
+ continue;
29708
+ const refPath = withinDir ? normalizeRelPath(join10(withinDir, raw)) : raw;
29709
+ if (!canonicalExists(root2, refPath))
29710
+ continue;
29711
+ links.push({ refPath, sourceFile });
29712
+ }
29713
+ return links;
29714
+ }
29428
29715
  function findLocalCanonicalLinks(root2, content3, sourceFile) {
29429
29716
  const links = [];
29430
29717
  const localRefRe = /\((?:\.\/)?references\/([^)]+)\)/g;
29431
29718
  for (const match of content3.matchAll(localRefRe)) {
29432
29719
  const refPath = normalizeRelPath(match[1] ?? "");
29433
- if (!refPath || !canonicalExists(root2, refPath))
29720
+ if (!(refPath && canonicalExists(root2, refPath)))
29434
29721
  continue;
29435
29722
  links.push({ refPath, sourceFile });
29436
29723
  }
29437
- const inReferencesDir = /\/references\//.test(sourceFile);
29438
- if (inReferencesDir) {
29439
- const refsIdx = sourceFile.lastIndexOf("/references/");
29440
- const withinRefs = sourceFile.slice(refsIdx + "/references/".length);
29441
- const withinDir = withinRefs.includes("/") ? withinRefs.slice(0, withinRefs.lastIndexOf("/")) : "";
29442
- const siblingRe = /\((?!https?:|#|\.\.\/)([a-z0-9./_-]+\.md)\)/gi;
29443
- for (const match of content3.matchAll(siblingRe)) {
29444
- const raw = normalizeRelPath(match[1] ?? "");
29445
- if (!raw)
29446
- continue;
29447
- const refPath = withinDir ? normalizeRelPath(join10(withinDir, raw)) : raw;
29448
- if (!canonicalExists(root2, refPath))
29724
+ links.push(...findSiblingRefLinks(root2, content3, sourceFile));
29725
+ return links;
29726
+ }
29727
+ function collectLinksForFile(root2, relFile) {
29728
+ const content3 = readFileSync9(join10(root2, relFile), "utf8");
29729
+ if (isGeneratedReference(content3))
29730
+ return [];
29731
+ return [
29732
+ ...findSharedRefLinks(content3, relFile),
29733
+ ...findLocalCanonicalLinks(root2, content3, relFile)
29734
+ ];
29735
+ }
29736
+ function expandTransitiveRefs(input) {
29737
+ const { root: root2, slug: slug2, refPaths, links } = input;
29738
+ const queue = [...refPaths];
29739
+ while (queue.length > 0) {
29740
+ const refPath = queue.pop();
29741
+ if (!(refPath && canonicalExists(root2, refPath)))
29742
+ continue;
29743
+ const canonicalContent = readFileSync9(join10(root2, CANONICAL_REFS_DIR, refPath), "utf8");
29744
+ const syntheticSource = generatedRefPath(slug2, refPath);
29745
+ for (const link2 of findLocalCanonicalLinks(root2, canonicalContent, syntheticSource)) {
29746
+ if (refPaths.has(link2.refPath))
29449
29747
  continue;
29450
- links.push({ refPath, sourceFile });
29748
+ refPaths.add(link2.refPath);
29749
+ links.push(link2);
29750
+ queue.push(link2.refPath);
29451
29751
  }
29452
29752
  }
29453
- return links;
29753
+ }
29754
+ function planForSkill(root2, slug2) {
29755
+ const skillDir = join10(root2, slug2);
29756
+ if (!existsSync12(join10(skillDir, "SKILL.md")))
29757
+ return null;
29758
+ const refPaths = new Set;
29759
+ const links = [];
29760
+ for (const relFile of walkMarkdownFiles(skillDir, root2)) {
29761
+ for (const link2 of collectLinksForFile(root2, relFile)) {
29762
+ refPaths.add(link2.refPath);
29763
+ links.push(link2);
29764
+ }
29765
+ }
29766
+ expandTransitiveRefs({ root: root2, slug: slug2, refPaths, links });
29767
+ return refPaths.size > 0 ? { skill: slug2, refPaths, links } : null;
29454
29768
  }
29455
29769
  function discoverSkillReferencePlans(root2, ownership) {
29456
29770
  const index2 = buildSkillIndex(root2, ownership);
29457
29771
  const plans = [];
29458
29772
  for (const slug2 of index2.ownedSlugs) {
29459
- const skillDir = join10(root2, slug2);
29460
- if (!existsSync12(join10(skillDir, "SKILL.md")))
29461
- continue;
29462
- const refPaths = new Set;
29463
- const links = [];
29464
- for (const relFile of walkMarkdownFiles(skillDir, root2)) {
29465
- const content3 = readFileSync9(join10(root2, relFile), "utf8");
29466
- if (isGeneratedReference(content3))
29467
- continue;
29468
- for (const link2 of findSharedRefLinks(content3, relFile)) {
29469
- refPaths.add(link2.refPath);
29470
- links.push(link2);
29471
- }
29472
- for (const link2 of findLocalCanonicalLinks(root2, content3, relFile)) {
29473
- refPaths.add(link2.refPath);
29474
- links.push(link2);
29475
- }
29476
- }
29477
- const queue = [...refPaths];
29478
- while (queue.length > 0) {
29479
- const refPath = queue.pop();
29480
- if (!refPath || !canonicalExists(root2, refPath))
29481
- continue;
29482
- const canonicalContent = readFileSync9(join10(root2, CANONICAL_REFS_DIR, refPath), "utf8");
29483
- const syntheticSource = generatedRefPath(slug2, refPath);
29484
- for (const link2 of findLocalCanonicalLinks(root2, canonicalContent, syntheticSource)) {
29485
- if (refPaths.has(link2.refPath))
29486
- continue;
29487
- refPaths.add(link2.refPath);
29488
- links.push(link2);
29489
- queue.push(link2.refPath);
29490
- }
29491
- }
29492
- if (refPaths.size > 0) {
29493
- plans.push({ skill: slug2, refPaths, links });
29494
- }
29773
+ const plan = planForSkill(root2, slug2);
29774
+ if (plan)
29775
+ plans.push(plan);
29495
29776
  }
29496
29777
  return plans.sort((a, b) => a.skill.localeCompare(b.skill));
29497
29778
  }
@@ -29522,30 +29803,75 @@ function rewriteSharedRefLinks(content3, sourceFile, skill) {
29522
29803
  }
29523
29804
 
29524
29805
  // src/references/check.ts
29806
+ function walkMarkdown(dir, onFile) {
29807
+ if (!existsSync13(dir))
29808
+ return;
29809
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
29810
+ if (entry.name.startsWith("."))
29811
+ continue;
29812
+ const fullPath = join11(dir, entry.name);
29813
+ if (entry.isDirectory()) {
29814
+ walkMarkdown(fullPath, onFile);
29815
+ continue;
29816
+ }
29817
+ if (!entry.name.endsWith(".md"))
29818
+ continue;
29819
+ onFile(fullPath);
29820
+ }
29821
+ }
29525
29822
  function listAllGeneratedFiles(root2) {
29526
29823
  const files = [];
29527
- const walk = (dir) => {
29528
- if (!existsSync13(dir))
29529
- return;
29530
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
29531
- if (entry.name.startsWith("."))
29532
- continue;
29533
- const fullPath = join11(dir, entry.name);
29534
- if (entry.isDirectory()) {
29535
- walk(fullPath);
29536
- continue;
29537
- }
29538
- if (!entry.name.endsWith(".md"))
29539
- continue;
29540
- const content3 = readFileSync10(fullPath, "utf8");
29541
- if (isGeneratedReference(content3)) {
29542
- files.push(normalizeRelPath(relative7(root2, fullPath)));
29543
- }
29824
+ walkMarkdown(root2, (fullPath) => {
29825
+ const content3 = readFileSync10(fullPath, "utf8");
29826
+ if (isGeneratedReference(content3)) {
29827
+ files.push(normalizeRelPath(relative7(root2, fullPath)));
29544
29828
  }
29545
- };
29546
- walk(root2);
29829
+ });
29547
29830
  return files;
29548
29831
  }
29832
+ function checkNeededCopy(root2, targetRel) {
29833
+ const targetPath = join11(root2, targetRel);
29834
+ if (!existsSync13(targetPath)) {
29835
+ return issue("generated-references", targetRel, "missing generated copy — run skeleton references sync");
29836
+ }
29837
+ const generated = readFileSync10(targetPath, "utf8");
29838
+ if (!isGeneratedReference(generated)) {
29839
+ return issue("generated-references", targetRel, "expected generated-reference provenance header");
29840
+ }
29841
+ const body = stripGeneratedHeader(generated);
29842
+ const sourceRel = normalizeRelPath(generated.match(/source: ([^\n]+)/)?.[1] ?? join11(CANONICAL_REFS_DIR, targetRel.split("/references/")[1] ?? ""));
29843
+ const canonicalPath = join11(root2, sourceRel);
29844
+ if (!existsSync13(canonicalPath)) {
29845
+ return issue("generated-references", targetRel, `canonical source missing: ${sourceRel}`);
29846
+ }
29847
+ const canonical = readFileSync10(canonicalPath, "utf8");
29848
+ if (body !== canonical) {
29849
+ return issue("generated-references", targetRel, "stale generated copy — run skeleton references sync");
29850
+ }
29851
+ return null;
29852
+ }
29853
+ function checkOrphanedCopies(root2, needed, skillIndex) {
29854
+ const issues = [];
29855
+ for (const generatedRel of listAllGeneratedFiles(root2)) {
29856
+ if (isForeignSkillPath(generatedRel, skillIndex))
29857
+ continue;
29858
+ if (!needed.has(generatedRel)) {
29859
+ issues.push(issue("generated-references", generatedRel, "orphaned generated copy — run skeleton references sync"));
29860
+ }
29861
+ }
29862
+ return issues;
29863
+ }
29864
+ function checkStaleSharedLinks(root2, skillDir) {
29865
+ const issues = [];
29866
+ walkMarkdown(skillDir, (fullPath) => {
29867
+ const relFile = normalizeRelPath(relative7(root2, fullPath));
29868
+ const content3 = readFileSync10(fullPath, "utf8");
29869
+ if (!content3.match(SHARED_REF_LINK_RE))
29870
+ return;
29871
+ issues.push(issue("generated-references", relFile, "still links to shared root references/ — run skeleton references sync"));
29872
+ });
29873
+ return issues;
29874
+ }
29549
29875
  function runGeneratedReferencesCheck(root2, ownership) {
29550
29876
  const issues = [];
29551
29877
  const canonicalDir = join11(root2, CANONICAL_REFS_DIR);
@@ -29560,58 +29886,13 @@ function runGeneratedReferencesCheck(root2, ownership) {
29560
29886
  }
29561
29887
  }
29562
29888
  for (const targetRel of needed) {
29563
- const targetPath = join11(root2, targetRel);
29564
- if (!existsSync13(targetPath)) {
29565
- issues.push(issue("generated-references", targetRel, "missing generated copy — run skeleton references sync"));
29566
- continue;
29567
- }
29568
- const generated = readFileSync10(targetPath, "utf8");
29569
- if (!isGeneratedReference(generated)) {
29570
- issues.push(issue("generated-references", targetRel, "expected generated-reference provenance header"));
29571
- continue;
29572
- }
29573
- const body = stripGeneratedHeader(generated);
29574
- const sourceRel = normalizeRelPath(generated.match(/source: ([^\n]+)/)?.[1] ?? join11(CANONICAL_REFS_DIR, targetRel.split("/references/")[1] ?? ""));
29575
- const canonicalPath = join11(root2, sourceRel);
29576
- if (!existsSync13(canonicalPath)) {
29577
- issues.push(issue("generated-references", targetRel, `canonical source missing: ${sourceRel}`));
29578
- continue;
29579
- }
29580
- const canonical = readFileSync10(canonicalPath, "utf8");
29581
- if (body !== canonical) {
29582
- issues.push(issue("generated-references", targetRel, "stale generated copy — run skeleton references sync"));
29583
- }
29584
- }
29585
- for (const generatedRel of listAllGeneratedFiles(root2)) {
29586
- if (isForeignSkillPath(generatedRel, skillIndex))
29587
- continue;
29588
- if (!needed.has(generatedRel)) {
29589
- issues.push(issue("generated-references", generatedRel, "orphaned generated copy — run skeleton references sync"));
29590
- }
29889
+ const found = checkNeededCopy(root2, targetRel);
29890
+ if (found)
29891
+ issues.push(found);
29591
29892
  }
29893
+ issues.push(...checkOrphanedCopies(root2, needed, skillIndex));
29592
29894
  for (const plan of plans) {
29593
- const skillDir = join11(root2, plan.skill);
29594
- if (!existsSync13(skillDir))
29595
- continue;
29596
- const walk = (dir) => {
29597
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
29598
- if (entry.name.startsWith("."))
29599
- continue;
29600
- const fullPath = join11(dir, entry.name);
29601
- if (entry.isDirectory()) {
29602
- walk(fullPath);
29603
- continue;
29604
- }
29605
- if (!entry.name.endsWith(".md"))
29606
- continue;
29607
- const relFile = normalizeRelPath(relative7(root2, fullPath));
29608
- const content3 = readFileSync10(fullPath, "utf8");
29609
- if (content3.match(SHARED_REF_LINK_RE)) {
29610
- issues.push(issue("generated-references", relFile, "still links to shared root references/ — run skeleton references sync"));
29611
- }
29612
- }
29613
- };
29614
- walk(skillDir);
29895
+ issues.push(...checkStaleSharedLinks(root2, join11(root2, plan.skill)));
29615
29896
  }
29616
29897
  return issues;
29617
29898
  }
@@ -29638,6 +29919,51 @@ var bannedRule = { id: "banned", run: runBannedRule };
29638
29919
  // src/audit/rules/doc-meta.ts
29639
29920
  import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
29640
29921
  import { join as join12 } from "node:path";
29922
+ function checkDocMetaBanner(relPath2, content3) {
29923
+ if (DOC_META_RE.test(content3))
29924
+ return null;
29925
+ return issue("doc-meta", relPath2, "missing doc-meta comment (owner + last-reviewed)");
29926
+ }
29927
+ function checkStaleReview(input) {
29928
+ const { relPath: relPath2, content: content3, today, staleDays } = input;
29929
+ const reviewedStr = docMetaLastReviewed(content3);
29930
+ if (!reviewedStr)
29931
+ return null;
29932
+ const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
29933
+ if (Number.isNaN(reviewed.getTime()))
29934
+ return null;
29935
+ const ageDays = (today.getTime() - reviewed.getTime()) / 86400000;
29936
+ if (ageDays <= staleDays)
29937
+ return null;
29938
+ return issue("doc-meta", relPath2, {
29939
+ message: `doc-meta last-reviewed ${reviewedStr} is stale (>${staleDays} days)`,
29940
+ severity: "warning"
29941
+ });
29942
+ }
29943
+ function checkGitFreshness(input) {
29944
+ const { relPath: relPath2, content: content3, root: root2, lockedSkillSlugs } = input;
29945
+ const reviewedStr = docMetaLastReviewed(content3);
29946
+ if (!reviewedStr)
29947
+ return null;
29948
+ const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
29949
+ if (Number.isNaN(reviewed.getTime()))
29950
+ return null;
29951
+ const slug2 = slugFromPath(relPath2, root2);
29952
+ if (slug2 !== null && lockedSkillSlugs.has(slug2))
29953
+ return null;
29954
+ const gitDate = lastGitCommitDate(relPath2, root2);
29955
+ if (!gitDate)
29956
+ return null;
29957
+ const committed = new Date(`${gitDate}T00:00:00Z`);
29958
+ if (Number.isNaN(committed.getTime()))
29959
+ return null;
29960
+ if (committed.getTime() <= reviewed.getTime())
29961
+ return null;
29962
+ return issue("doc-meta", relPath2, {
29963
+ message: `content changed after last-reviewed ${reviewedStr} (git: ${gitDate}) — bump last-reviewed or confirm review`,
29964
+ severity: "warning"
29965
+ });
29966
+ }
29641
29967
  function runDocMetaRule(ctx) {
29642
29968
  const issues = [];
29643
29969
  const today = new Date;
@@ -29646,32 +29972,27 @@ function runDocMetaRule(ctx) {
29646
29972
  if (!existsSync14(abs))
29647
29973
  continue;
29648
29974
  const content3 = readFileSync11(abs, "utf8");
29649
- if (!DOC_META_RE.test(content3)) {
29650
- issues.push(issue("doc-meta", relPath2, "missing doc-meta comment (owner + last-reviewed)"));
29651
- continue;
29652
- }
29653
- const reviewedStr = docMetaLastReviewed(content3);
29654
- if (!reviewedStr)
29655
- continue;
29656
- const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
29657
- if (Number.isNaN(reviewed.getTime()))
29975
+ const banner = checkDocMetaBanner(relPath2, content3);
29976
+ if (banner) {
29977
+ issues.push(banner);
29658
29978
  continue;
29659
- const ageDays = (today.getTime() - reviewed.getTime()) / 86400000;
29660
- if (ageDays > ctx.config.daysUntilStale) {
29661
- issues.push(issue("doc-meta", relPath2, `doc-meta last-reviewed ${reviewedStr} is stale (>${ctx.config.daysUntilStale} days)`, { severity: "warning" }));
29662
- }
29663
- const slug2 = slugFromPath(relPath2, ctx.root);
29664
- if (slug2 !== null && ctx.lockedSkillSlugs.has(slug2))
29665
- continue;
29666
- const gitDate = lastGitCommitDate(relPath2, ctx.root);
29667
- if (!gitDate)
29668
- continue;
29669
- const committed = new Date(`${gitDate}T00:00:00Z`);
29670
- if (Number.isNaN(committed.getTime()))
29671
- continue;
29672
- if (committed.getTime() > reviewed.getTime()) {
29673
- issues.push(issue("doc-meta", relPath2, `content changed after last-reviewed ${reviewedStr} (git: ${gitDate}) — bump last-reviewed or confirm review`, { severity: "warning" }));
29674
29979
  }
29980
+ const stale = checkStaleReview({
29981
+ relPath: relPath2,
29982
+ content: content3,
29983
+ today,
29984
+ staleDays: ctx.config.daysUntilStale
29985
+ });
29986
+ if (stale)
29987
+ issues.push(stale);
29988
+ const git = checkGitFreshness({
29989
+ relPath: relPath2,
29990
+ content: content3,
29991
+ root: ctx.root,
29992
+ lockedSkillSlugs: ctx.lockedSkillSlugs
29993
+ });
29994
+ if (git)
29995
+ issues.push(git);
29675
29996
  }
29676
29997
  return issues;
29677
29998
  }
@@ -29686,57 +30007,88 @@ function resolveLink2(sourceFile, target) {
29686
30007
  return sourceFile;
29687
30008
  return resolve7(dirname7(sourceFile), withoutAnchor);
29688
30009
  }
29689
- function validateTarget(ctx, sourceFile, target, linkLabel) {
29690
- const issues = [];
29691
- if (isExternalLink(target) && !target.startsWith("#"))
29692
- return issues;
29693
- if (isPlaceholderLink(target))
29694
- return issues;
29695
- const relSource = relPath(sourceFile, ctx.root);
29696
- const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
29697
- const pathPart = target.split("#")[0]?.split("?")[0] ?? "";
29698
- const resolved = resolveLink2(sourceFile, target);
29699
- const relTarget = relPath(resolved, ctx.root);
29700
- const skillMatch = SKILL_LINK_IN_TARGET_RE.exec(target);
29701
- if (skillMatch?.[1] && ctx.retiredSkills.has(skillMatch[1])) {
29702
- issues.push(issue("links", relSource, `references retired skill "${skillMatch[1]}/SKILL.md"`, {
29703
- link: linkLabel
29704
- }));
29705
- return issues;
29706
- }
29707
- if (target.includes("/SKILL.md")) {
29708
- const slug2 = skillMatch?.[1];
29709
- if (slug2 && !resolveSkillPath(ctx.skillIndex, ctx.root, slug2)) {
29710
- issues.push(issue("links", relSource, `missing skill "${slug2}/SKILL.md"`, {
29711
- link: linkLabel
29712
- }));
29713
- return issues;
29714
- }
29715
- }
29716
- if ((target.includes(".claude/agents/") || target.includes(".cursor/agents/")) && target.endsWith(".md")) {
29717
- const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
29718
- if (!existsSync15(agentPath)) {
29719
- issues.push(issue("links", relSource, "missing agent file", { link: linkLabel }));
29720
- }
29721
- return issues;
29722
- }
29723
- if (pathPart && !existsSync15(resolved)) {
29724
- issues.push(issue("links", relSource, `broken link → ${relTarget}`, {
29725
- link: linkLabel
29726
- }));
29727
- return issues;
29728
- }
29729
- if (anchor && existsSync15(resolved)) {
29730
- const targetContent = readFileSync12(resolved, "utf8");
29731
- const slugs = extractHeadingSlugs(targetContent, resolved);
29732
- const anchorSlug = slugifyAnchor(anchor);
29733
- if (!slugs.has(anchorSlug)) {
29734
- issues.push(issue("links", relSource, `broken anchor → #${anchor} in ${relTarget}`, {
29735
- link: linkLabel
29736
- }));
29737
- }
30010
+ function checkRetiredSkill(input) {
30011
+ const skillMatch = SKILL_LINK_IN_TARGET_RE.exec(input.target);
30012
+ const slug2 = skillMatch?.[1];
30013
+ if (!(slug2 && input.ctx.retiredSkills.has(slug2)))
30014
+ return null;
30015
+ return issue("links", relPath(input.sourceFile, input.ctx.root), {
30016
+ message: `references retired skill "${slug2}/SKILL.md"`,
30017
+ link: input.linkLabel
30018
+ });
30019
+ }
30020
+ function checkMissingSkill(input, relSource) {
30021
+ if (!input.target.includes("/SKILL.md"))
30022
+ return null;
30023
+ const slug2 = SKILL_LINK_IN_TARGET_RE.exec(input.target)?.[1];
30024
+ if (!(slug2 && !resolveSkillPath(input.ctx.skillIndex, input.ctx.root, slug2)))
30025
+ return null;
30026
+ return issue("links", relSource, {
30027
+ message: `missing skill "${slug2}/SKILL.md"`,
30028
+ link: input.linkLabel
30029
+ });
30030
+ }
30031
+ function checkAgentFile(input, resolved, relSource) {
30032
+ if (!((input.target.includes(".claude/agents/") || input.target.includes(".cursor/agents/")) && input.target.endsWith(".md"))) {
30033
+ return null;
29738
30034
  }
29739
- return issues;
30035
+ const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
30036
+ if (existsSync15(agentPath))
30037
+ return null;
30038
+ return issue("links", relSource, { message: "missing agent file", link: input.linkLabel });
30039
+ }
30040
+ function checkBrokenPath(ctx) {
30041
+ const { input, pathPart, resolved, relSource, relTarget } = ctx;
30042
+ if (!(pathPart && !existsSync15(resolved)))
30043
+ return null;
30044
+ return issue("links", relSource, {
30045
+ message: `broken link → ${relTarget}`,
30046
+ link: input.linkLabel
30047
+ });
30048
+ }
30049
+ function checkBrokenAnchor(ctx) {
30050
+ const { input, anchor, resolved, relSource, relTarget } = ctx;
30051
+ if (!(anchor && existsSync15(resolved)))
30052
+ return null;
30053
+ const targetContent = readFileSync12(resolved, "utf8");
30054
+ const slugs = extractHeadingSlugs(targetContent, resolved);
30055
+ const anchorSlug = slugifyAnchor(anchor);
30056
+ if (slugs.has(anchorSlug))
30057
+ return null;
30058
+ return issue("links", relSource, {
30059
+ message: `broken anchor → #${anchor} in ${relTarget}`,
30060
+ link: input.linkLabel
30061
+ });
30062
+ }
30063
+ function resolveTargetParts(sourceFile, target, root2) {
30064
+ const relSource = relPath(sourceFile, root2);
30065
+ const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
30066
+ const pathPart = target.split("#")[0]?.split("?")[0] ?? "";
30067
+ const resolved = resolveLink2(sourceFile, target);
30068
+ const relTarget = relPath(resolved, root2);
30069
+ return { relSource, anchor, pathPart, resolved, relTarget };
30070
+ }
30071
+ function validateTarget(input) {
30072
+ const { ctx, sourceFile, target } = input;
30073
+ if (isExternalLink(target) && !target.startsWith("#"))
30074
+ return [];
30075
+ if (isPlaceholderLink(target))
30076
+ return [];
30077
+ const parts = resolveTargetParts(sourceFile, target, ctx.root);
30078
+ const retired = checkRetiredSkill(input);
30079
+ if (retired)
30080
+ return [retired];
30081
+ const missingSkill = checkMissingSkill(input, parts.relSource);
30082
+ if (missingSkill)
30083
+ return [missingSkill];
30084
+ const agent = checkAgentFile(input, parts.resolved, parts.relSource);
30085
+ if (agent)
30086
+ return [agent];
30087
+ const brokenPath = checkBrokenPath({ input, ...parts });
30088
+ if (brokenPath)
30089
+ return [brokenPath];
30090
+ const brokenAnchor = checkBrokenAnchor({ input, ...parts });
30091
+ return brokenAnchor ? [brokenAnchor] : [];
29740
30092
  }
29741
30093
  function runLinksRule(ctx) {
29742
30094
  const issues = [];
@@ -29745,7 +30097,7 @@ function runLinksRule(ctx) {
29745
30097
  const links = extractLinksFromMarkdown(content3, filePath);
29746
30098
  for (const { target, line } of links) {
29747
30099
  const linkLabel = line ? `line ${line}` : target;
29748
- issues.push(...validateTarget(ctx, filePath, target, linkLabel));
30100
+ issues.push(...validateTarget({ ctx, sourceFile: filePath, target, linkLabel }));
29749
30101
  }
29750
30102
  }
29751
30103
  return issues;
@@ -29753,6 +30105,44 @@ function runLinksRule(ctx) {
29753
30105
  var linksRule = { id: "links", run: runLinksRule };
29754
30106
 
29755
30107
  // src/audit/rules/prose-policy.ts
30108
+ function checkDraftEntry(input) {
30109
+ const { rel, lines, entry, draftPrefixes } = input;
30110
+ const issues = [];
30111
+ for (let i = 0;i < lines.length; i++) {
30112
+ if (entry.regex?.test(lines[i] ?? "") && !isDraftPlacementAllowed(rel, draftPrefixes)) {
30113
+ issues.push(issue("prose-policy", rel, {
30114
+ message: entry.message,
30115
+ link: `line ${i + 1}`,
30116
+ severity: entry.severity
30117
+ }));
30118
+ }
30119
+ }
30120
+ return issues;
30121
+ }
30122
+ function checkLineEntry(rel, lines, entry) {
30123
+ const issues = [];
30124
+ for (let i = 0;i < lines.length; i++) {
30125
+ if (entry.regex?.test(lines[i] ?? "")) {
30126
+ issues.push(issue("prose-policy", rel, {
30127
+ message: entry.message,
30128
+ link: `line ${i + 1}`,
30129
+ severity: entry.severity
30130
+ }));
30131
+ }
30132
+ }
30133
+ return issues;
30134
+ }
30135
+ function checkPolicyEntry(input) {
30136
+ const { rel, content: content3, lines, entry, draftPrefixes } = input;
30137
+ if (entry.id === "draft-marker") {
30138
+ return checkDraftEntry({ rel, lines, entry, draftPrefixes });
30139
+ }
30140
+ const isMultiline = entry.pattern?.includes("[\\s\\S]");
30141
+ if (isMultiline) {
30142
+ return entry.regex?.test(content3) ? [issue("prose-policy", rel, { message: entry.message, severity: entry.severity })] : [];
30143
+ }
30144
+ return checkLineEntry(rel, lines, entry);
30145
+ }
29756
30146
  function runProsePolicyRule(ctx) {
29757
30147
  if (ctx.policies.length === 0)
29758
30148
  return [];
@@ -29765,36 +30155,9 @@ function runProsePolicyRule(ctx) {
29765
30155
  `);
29766
30156
  const policies = policiesForFile(ctx.policies, rel);
29767
30157
  for (const entry of policies) {
29768
- if (entry.mode === "fingerprint")
30158
+ if (entry.mode === "fingerprint" || !entry.regex)
29769
30159
  continue;
29770
- if (!entry.regex)
29771
- continue;
29772
- if (entry.id === "draft-marker") {
29773
- for (let i = 0;i < lines.length; i++) {
29774
- if (entry.regex.test(lines[i] ?? "") && !isDraftPlacementAllowed(rel, draftPrefixes)) {
29775
- issues.push(issue("prose-policy", rel, entry.message, {
29776
- link: `line ${i + 1}`,
29777
- severity: entry.severity
29778
- }));
29779
- }
29780
- }
29781
- continue;
29782
- }
29783
- const isMultiline = entry.pattern?.includes("[\\s\\S]");
29784
- if (isMultiline) {
29785
- if (entry.regex.test(content3)) {
29786
- issues.push(issue("prose-policy", rel, entry.message, { severity: entry.severity }));
29787
- }
29788
- continue;
29789
- }
29790
- for (let i = 0;i < lines.length; i++) {
29791
- if (entry.regex.test(lines[i] ?? "")) {
29792
- issues.push(issue("prose-policy", rel, entry.message, {
29793
- link: `line ${i + 1}`,
29794
- severity: entry.severity
29795
- }));
29796
- }
29797
- }
30160
+ issues.push(...checkPolicyEntry({ rel, content: content3, lines, entry, draftPrefixes }));
29798
30161
  }
29799
30162
  }
29800
30163
  return issues;
@@ -29804,12 +30167,8 @@ var prosePolicyRule = { id: "prose-policy", run: runProsePolicyRule };
29804
30167
  // src/audit/rules/registry.ts
29805
30168
  import { existsSync as existsSync16, readFileSync as readFileSync13 } from "node:fs";
29806
30169
  import { join as join13 } from "node:path";
29807
- function runRegistryRule(ctx) {
30170
+ function checkRegistryEntries(ctx) {
29808
30171
  const issues = [];
29809
- const registry = new Set(ctx.registryPaths);
29810
- if (ctx.registryHasTableHeader && ctx.registryPaths.length === 0) {
29811
- issues.push(issue("registry", REGISTRY_REL_PATH, "registry table header found but 0 rows parsed — check | Topic | Canonical file | format and link syntax"));
29812
- }
29813
30172
  for (const rel of ctx.registryPaths) {
29814
30173
  const abs = join13(ctx.root, rel);
29815
30174
  if (!existsSync16(abs)) {
@@ -29821,11 +30180,15 @@ function runRegistryRule(ctx) {
29821
30180
  issues.push(issue("registry", rel, "missing **Source of truth for** banner (required for registry entry)"));
29822
30181
  }
29823
30182
  }
30183
+ return issues;
30184
+ }
30185
+ function checkUnregisteredBanners(ctx, registry) {
30186
+ const issues = [];
29824
30187
  for (const filePath of ctx.files) {
29825
30188
  const rel = relPath(filePath, ctx.root);
29826
30189
  if (rel === REGISTRY_REL_PATH)
29827
30190
  continue;
29828
- if (!rel.endsWith(".md") && !rel.endsWith(".mdc"))
30191
+ if (!(rel.endsWith(".md") || rel.endsWith(".mdc")))
29829
30192
  continue;
29830
30193
  const content3 = readFileSync13(filePath, "utf8");
29831
30194
  if (!SOURCE_OF_TRUTH_BANNER_LINE_RE.test(content3))
@@ -29836,6 +30199,16 @@ function runRegistryRule(ctx) {
29836
30199
  }
29837
30200
  return issues;
29838
30201
  }
30202
+ function runRegistryRule(ctx) {
30203
+ const issues = [];
30204
+ const registry = new Set(ctx.registryPaths);
30205
+ if (ctx.registryHasTableHeader && ctx.registryPaths.length === 0) {
30206
+ issues.push(issue("registry", REGISTRY_REL_PATH, "registry table header found but 0 rows parsed — check | Topic | Canonical file | format and link syntax"));
30207
+ }
30208
+ issues.push(...checkRegistryEntries(ctx));
30209
+ issues.push(...checkUnregisteredBanners(ctx, registry));
30210
+ return issues;
30211
+ }
29839
30212
  var registryRule = { id: "registry", run: runRegistryRule };
29840
30213
 
29841
30214
  // src/audit/rules/scan-gaps.ts
@@ -29847,7 +30220,10 @@ function runCoverageGapsRule(ctx) {
29847
30220
  for (const rel of candidates) {
29848
30221
  if (scanned.has(rel))
29849
30222
  continue;
29850
- issues.push(issue("coverage-gaps", rel, "markdown outside audit scan perimeter — extend .skeleton/config.yaml scan.include", { severity: "warning" }));
30223
+ issues.push(issue("coverage-gaps", rel, {
30224
+ message: "markdown outside audit scan perimeter — extend .skeleton/config.yaml scan.include",
30225
+ severity: "warning"
30226
+ }));
29851
30227
  }
29852
30228
  return issues;
29853
30229
  }
@@ -29917,70 +30293,79 @@ function scanFileForSkillLinks(ctx, filePath, index2) {
29917
30293
  }
29918
30294
  return issues;
29919
30295
  }
30296
+ function taxonomyIssuesForReadme(input) {
30297
+ const { ctx, index: index2, skillRoot, diskSlugs, nonPublic } = input;
30298
+ const readmePath = join14(ctx.root, skillRoot.relPath, "README.md");
30299
+ if (!existsSync17(readmePath))
30300
+ return [];
30301
+ const readme = readFileSync14(readmePath, "utf8");
30302
+ if (!readme.includes("## Taxonomy"))
30303
+ return [];
30304
+ const taxonomySlugs = parseReadmeTaxonomySlugs(readme);
30305
+ const nestedSlugs = diskSlugs.filter((slug2) => existsSync17(join14(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
30306
+ const foreign = new Set(index2.foreignSlugs);
30307
+ const publicSlugs = nestedSlugs.filter((slug2) => !(nonPublic.has(slug2) || foreign.has(slug2)));
30308
+ const relReadme = `${skillRoot.relPath}/README.md`;
30309
+ const issues = [];
30310
+ for (const slug2 of publicSlugs) {
30311
+ if (!taxonomySlugs.includes(slug2)) {
30312
+ issues.push(issue("skill-index", relReadme, `taxonomy missing public skill "${slug2}"`));
30313
+ }
30314
+ }
30315
+ for (const slug2 of taxonomySlugs) {
30316
+ if (!nestedSlugs.includes(slug2)) {
30317
+ issues.push(issue("skill-index", relReadme, `taxonomy lists skill "${slug2}" with no SKILL.md on disk`));
30318
+ }
30319
+ }
30320
+ return issues;
30321
+ }
29920
30322
  function validateReadmeTaxonomy(ctx, index2, diskSlugs) {
29921
30323
  const issues = [];
29922
30324
  const nonPublic = new Set(nonPublicSkills(ctx.config));
29923
30325
  for (const skillRoot of index2.roots) {
29924
30326
  if (skillRoot.kind !== "nested")
29925
30327
  continue;
29926
- const readmePath = join14(ctx.root, skillRoot.relPath, "README.md");
29927
- if (!existsSync17(readmePath))
29928
- continue;
29929
- const readme = readFileSync14(readmePath, "utf8");
29930
- if (!readme.includes("## Taxonomy"))
30328
+ issues.push(...taxonomyIssuesForReadme({ ctx, index: index2, skillRoot, diskSlugs, nonPublic }));
30329
+ }
30330
+ return issues;
30331
+ }
30332
+ function slugsForRoot(skillRoot, index2, owned) {
30333
+ return skillRoot.kind === "nested" ? index2.ownedSlugs : index2.flatSlugs.filter((slug2) => owned.has(slug2));
30334
+ }
30335
+ function auditSkillRoot(input) {
30336
+ const { ctx, index: index2, skillRoot, owned } = input;
30337
+ const issues = [];
30338
+ const base = skillRoot.kind === "nested" ? join14(ctx.root, skillRoot.relPath) : ctx.root;
30339
+ for (const slug2 of slugsForRoot(skillRoot, index2, owned)) {
30340
+ const skillDir = join14(base, slug2);
30341
+ if (!existsSync17(skillDir))
29931
30342
  continue;
29932
- const taxonomySlugs = parseReadmeTaxonomySlugs(readme);
29933
- const nestedSlugs = diskSlugs.filter((slug2) => existsSync17(join14(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
29934
- const foreign = new Set(index2.foreignSlugs);
29935
- const publicSlugs = nestedSlugs.filter((slug2) => !nonPublic.has(slug2) && !foreign.has(slug2));
29936
- const relReadme = `${skillRoot.relPath}/README.md`;
29937
- for (const slug2 of publicSlugs) {
29938
- if (!taxonomySlugs.includes(slug2)) {
29939
- issues.push(issue("skill-index", relReadme, `taxonomy missing public skill "${slug2}"`));
29940
- }
29941
- }
29942
- for (const slug2 of taxonomySlugs) {
29943
- if (!nestedSlugs.includes(slug2)) {
29944
- issues.push(issue("skill-index", relReadme, `taxonomy lists skill "${slug2}" with no SKILL.md on disk`));
29945
- }
30343
+ for (const skillMd of walkSkillMarkdown(skillDir)) {
30344
+ issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
29946
30345
  }
29947
30346
  }
29948
30347
  return issues;
29949
30348
  }
30349
+ function auditOwnedSkillFiles(ctx, index2) {
30350
+ const owned = new Set(index2.ownedSlugs);
30351
+ const issues = [];
30352
+ for (const skillRoot of index2.roots) {
30353
+ issues.push(...auditSkillRoot({ ctx, index: index2, skillRoot, owned }));
30354
+ }
30355
+ return issues;
30356
+ }
29950
30357
  function runSkillIndexRule(ctx) {
29951
30358
  const issues = [];
29952
30359
  const index2 = ctx.skillIndex;
29953
30360
  const diskSlugs = listSkillSlugs(index2);
29954
30361
  for (const warning of index2.provenance.warnings) {
29955
- issues.push(issue("skill-index", index2.provenance.lockfile ?? "skills-lock.json", `skill provenance: ${warning}`, {
30362
+ issues.push(issue("skill-index", index2.provenance.lockfile ?? "skills-lock.json", {
30363
+ message: `skill provenance: ${warning}`,
29956
30364
  severity: "warning"
29957
30365
  }));
29958
30366
  }
29959
30367
  issues.push(...validateReadmeTaxonomy(ctx, index2, diskSlugs));
29960
- const owned = new Set(index2.ownedSlugs);
29961
- for (const skillRoot of index2.roots) {
29962
- if (skillRoot.kind === "nested") {
29963
- for (const slug2 of index2.ownedSlugs) {
29964
- const skillDir = join14(ctx.root, skillRoot.relPath, slug2);
29965
- if (!existsSync17(skillDir))
29966
- continue;
29967
- for (const skillMd of walkSkillMarkdown(skillDir)) {
29968
- issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
29969
- }
29970
- }
29971
- continue;
29972
- }
29973
- for (const slug2 of index2.flatSlugs) {
29974
- if (!owned.has(slug2))
29975
- continue;
29976
- const skillDir = join14(ctx.root, slug2);
29977
- if (!existsSync17(skillDir))
29978
- continue;
29979
- for (const skillMd of walkSkillMarkdown(skillDir)) {
29980
- issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
29981
- }
29982
- }
29983
- }
30368
+ issues.push(...auditOwnedSkillFiles(ctx, index2));
29984
30369
  return issues;
29985
30370
  }
29986
30371
  var skillIndexRule = { id: "skill-index", run: runSkillIndexRule };
@@ -30008,6 +30393,19 @@ var skillsRules = [
30008
30393
  prosePolicyRule
30009
30394
  ];
30010
30395
  var allRules = [...docsRules, ...skillsRules];
30396
+ function attachPluginRule(docs, skills, rule) {
30397
+ const suites = rule.suites ?? ["docs"];
30398
+ const inDocs = suites.includes("docs");
30399
+ const inSkills = suites.includes("skills");
30400
+ if (!(inDocs || inSkills)) {
30401
+ const listed = suites.length === 0 ? "(empty)" : suites.join(", ");
30402
+ 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.`);
30403
+ }
30404
+ if (inDocs)
30405
+ docs.push(rule);
30406
+ if (inSkills)
30407
+ skills.push(rule);
30408
+ }
30011
30409
  function assembleRules(pluginRules = []) {
30012
30410
  const coreIds = new Set(allRules.map((rule) => rule.id));
30013
30411
  const seenPlugin = new Set;
@@ -30020,17 +30418,7 @@ function assembleRules(pluginRules = []) {
30020
30418
  const docs = [...docsRules];
30021
30419
  const skills = [...skillsRules];
30022
30420
  for (const rule of pluginRules) {
30023
- const suites = rule.suites ?? ["docs"];
30024
- const inDocs = suites.includes("docs");
30025
- const inSkills = suites.includes("skills");
30026
- if (!inDocs && !inSkills) {
30027
- const listed = suites.length === 0 ? "(empty)" : suites.join(", ");
30028
- 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.`);
30029
- }
30030
- if (inDocs)
30031
- docs.push(rule);
30032
- if (inSkills)
30033
- skills.push(rule);
30421
+ attachPluginRule(docs, skills, rule);
30034
30422
  }
30035
30423
  const selfById = new Map;
30036
30424
  for (const rule of [...docs, ...skills]) {
@@ -30053,46 +30441,70 @@ function rulesForSuite(suite, pluginRules = []) {
30053
30441
  }
30054
30442
 
30055
30443
  // src/audit/run.ts
30444
+ function parseFixArg(argv, index2) {
30445
+ const next = argv[index2 + 1];
30446
+ if (next && !next.startsWith("-")) {
30447
+ if (next !== "doc-meta" && next !== "anchors") {
30448
+ throw new Error(`Unknown --fix kind: ${next}. Use --fix, --fix=doc-meta, or --fix=anchors.`);
30449
+ }
30450
+ return { fix: next, nextIndex: index2 + 1 };
30451
+ }
30452
+ return { fix: true, nextIndex: index2 };
30453
+ }
30454
+ function applyAuditFlag(input) {
30455
+ const { state, arg, argv, index: index2 } = input;
30456
+ if (arg.startsWith("--suite=")) {
30457
+ state.suite = arg.slice("--suite=".length);
30458
+ return index2;
30459
+ }
30460
+ if (arg === "--strict") {
30461
+ state.strict = true;
30462
+ return index2;
30463
+ }
30464
+ if (arg === "--json") {
30465
+ state.json = true;
30466
+ return index2;
30467
+ }
30468
+ if (arg === "--dry-run") {
30469
+ state.dryRun = true;
30470
+ return index2;
30471
+ }
30472
+ if (arg.startsWith("--dry-run=")) {
30473
+ throw new Error("audit: use --dry-run (boolean flag), not --dry-run=<value>");
30474
+ }
30475
+ if (arg === "--fix") {
30476
+ const parsed = parseFixArg(argv, index2);
30477
+ state.fix = parsed.fix;
30478
+ return parsed.nextIndex;
30479
+ }
30480
+ if (arg.startsWith("--fix=")) {
30481
+ state.fix = arg.slice("--fix=".length);
30482
+ return index2;
30483
+ }
30484
+ if (arg.startsWith("--paths=")) {
30485
+ state.paths = arg.slice("--paths=".length).split(",").map((path2) => path2.trim()).filter(Boolean);
30486
+ return index2;
30487
+ }
30488
+ if (arg.startsWith("--only=")) {
30489
+ state.only = new Set(arg.slice("--only=".length).split(",").filter(Boolean));
30490
+ return index2;
30491
+ }
30492
+ return index2;
30493
+ }
30056
30494
  function parseAuditArgs(argv) {
30057
- let suite = "docs";
30058
- let strict = false;
30059
- let json = false;
30060
- let dryRun = false;
30061
- let paths = [];
30062
- let only = null;
30063
- let fix = null;
30495
+ const state = {
30496
+ suite: "docs",
30497
+ strict: false,
30498
+ json: false,
30499
+ dryRun: false,
30500
+ paths: [],
30501
+ only: null,
30502
+ fix: null
30503
+ };
30064
30504
  for (let i = 0;i < argv.length; i++) {
30065
- const arg = argv[i] ?? "";
30066
- if (arg.startsWith("--suite=")) {
30067
- suite = arg.slice("--suite=".length);
30068
- } else if (arg === "--strict") {
30069
- strict = true;
30070
- } else if (arg === "--json") {
30071
- json = true;
30072
- } else if (arg === "--dry-run") {
30073
- dryRun = true;
30074
- } else if (arg.startsWith("--dry-run=")) {
30075
- throw new Error("audit: use --dry-run (boolean flag), not --dry-run=<value>");
30076
- } else if (arg === "--fix") {
30077
- const next = argv[i + 1];
30078
- if (next && !next.startsWith("-")) {
30079
- if (next !== "doc-meta" && next !== "anchors") {
30080
- throw new Error(`Unknown --fix kind: ${next}. Use --fix, --fix=doc-meta, or --fix=anchors.`);
30081
- }
30082
- fix = next;
30083
- i++;
30084
- } else {
30085
- fix = true;
30086
- }
30087
- } else if (arg.startsWith("--fix=")) {
30088
- fix = arg.slice("--fix=".length);
30089
- } else if (arg.startsWith("--paths=")) {
30090
- paths = arg.slice("--paths=".length).split(",").map((path2) => path2.trim()).filter(Boolean);
30091
- } else if (arg.startsWith("--only=")) {
30092
- only = new Set(arg.slice("--only=".length).split(",").filter(Boolean));
30093
- }
30505
+ i = applyAuditFlag({ state, arg: argv[i] ?? "", argv, index: i });
30094
30506
  }
30095
- return { suite, strict, json, paths, only, fix, dryRun };
30507
+ return state;
30096
30508
  }
30097
30509
  function labelForSuite(suite) {
30098
30510
  switch (suite) {
@@ -30115,6 +30527,29 @@ function shouldRunRule(rule, options, pathScoped) {
30115
30527
  return false;
30116
30528
  return true;
30117
30529
  }
30530
+ async function runAuditFixes(options, ctx, loaded) {
30531
+ if (options.fix === null || options.fix === undefined)
30532
+ return null;
30533
+ if (options.suite !== "docs") {
30534
+ console.error("--fix is supported only for audit docs");
30535
+ return 1;
30536
+ }
30537
+ const kinds = fixKindsForOnly(parseFixKinds(options.fix), options.only);
30538
+ if (kinds.length === 0) {
30539
+ console.error("--fix has no overlapping rules with --only (doc-meta → doc-meta, anchors → links).");
30540
+ return 1;
30541
+ }
30542
+ applyFixes(ctx, { kinds, dryRun: options.dryRun });
30543
+ if (!options.dryRun) {
30544
+ const refreshed = createContext({
30545
+ root: options.root,
30546
+ paths: options.paths.length > 0 ? options.paths : undefined,
30547
+ policies: loaded.policies
30548
+ });
30549
+ Object.assign(ctx, refreshed);
30550
+ }
30551
+ return null;
30552
+ }
30118
30553
  async function runAudit(options) {
30119
30554
  const pathScoped = options.paths.length > 0;
30120
30555
  const base = createContext({
@@ -30124,26 +30559,9 @@ async function runAudit(options) {
30124
30559
  });
30125
30560
  const loaded = await loadPlugins(base.root, base.config);
30126
30561
  const ctx = { ...base, policies: loaded.policies };
30127
- if (options.fix !== null && options.fix !== undefined) {
30128
- if (options.suite !== "docs") {
30129
- console.error("--fix is supported only for audit docs");
30130
- return 1;
30131
- }
30132
- const kinds = fixKindsForOnly(parseFixKinds(options.fix), options.only);
30133
- if (kinds.length === 0) {
30134
- console.error("--fix has no overlapping rules with --only (doc-meta → doc-meta, anchors → links).");
30135
- return 1;
30136
- }
30137
- applyFixes(ctx, { kinds, dryRun: options.dryRun });
30138
- if (!options.dryRun) {
30139
- const refreshed = createContext({
30140
- root: options.root,
30141
- paths: options.paths.length > 0 ? options.paths : undefined,
30142
- policies: loaded.policies
30143
- });
30144
- Object.assign(ctx, refreshed);
30145
- }
30146
- }
30562
+ const fixExit = await runAuditFixes(options, ctx, loaded);
30563
+ if (fixExit !== null)
30564
+ return fixExit;
30147
30565
  const rules = rulesForSuite(options.suite, loaded.rules).filter((r) => !options.only || options.only.has(r.id));
30148
30566
  const skipGlobalsForPaths = pathScoped && !options.globalOnly;
30149
30567
  const issues = [];
@@ -30173,7 +30591,7 @@ function customizePathForSlug(root2, slug2) {
30173
30591
  return join15(customizeDir(root2), `${slug2}.md`);
30174
30592
  }
30175
30593
  function findCustomizeViaRegistry(root2, slug2) {
30176
- for (const rel of parseRegistryPaths(root2)) {
30594
+ for (const rel of parseRegistry(root2).paths) {
30177
30595
  const expected = `${REGISTRY_DIR_REL}/customize/${slug2}.md`;
30178
30596
  if (normalizeRelPath(rel) === expected && existsSync18(join15(root2, rel))) {
30179
30597
  return rel;
@@ -30226,11 +30644,11 @@ function readAlwaysInclude(root2, basenames, skipBasename) {
30226
30644
  function resolveCustomize(root2, slug2) {
30227
30645
  const slugFile = resolveSlugFile(root2, slug2);
30228
30646
  const alwaysNames = alwaysIncludeBasenames(root2);
30229
- const skip = slugFile.path != null ? basename3(slugFile.path) : null;
30647
+ const skip = slugFile.path !== null && slugFile.path !== undefined ? basename3(slugFile.path) : null;
30230
30648
  const always = readAlwaysInclude(root2, alwaysNames, skip);
30231
30649
  const parts = [];
30232
30650
  const included = [];
30233
- if (slugFile.content != null && slugFile.content.trim().length > 0) {
30651
+ if (slugFile.content !== null && slugFile.content !== undefined && slugFile.content.trim().length > 0) {
30234
30652
  parts.push(slugFile.content.trimEnd());
30235
30653
  if (slugFile.path)
30236
30654
  included.push(slugFile.path);
@@ -30258,6 +30676,7 @@ function resolveCustomizeFromRoot(slug2, startDir) {
30258
30676
  }
30259
30677
 
30260
30678
  // src/hooks/run.ts
30679
+ import process3 from "node:process";
30261
30680
  function parsePayload(raw) {
30262
30681
  if (!raw.trim())
30263
30682
  return {};
@@ -30288,7 +30707,7 @@ function extractSkillSlug(payload) {
30288
30707
  }
30289
30708
  const path2 = extractPath(payload);
30290
30709
  if (path2)
30291
- return slugFromPath(path2, process.cwd());
30710
+ return slugFromPath(path2, process3.cwd());
30292
30711
  return null;
30293
30712
  }
30294
30713
  function cursorResponse(content3) {
@@ -30343,6 +30762,7 @@ Customize override for /${slug2} (from ${from}):
30343
30762
  import { spawnSync as spawnSync2 } from "node:child_process";
30344
30763
  import { copyFileSync, existsSync as existsSync22, mkdirSync as mkdirSync2, readFileSync as readFileSync17 } from "node:fs";
30345
30764
  import { join as join19 } from "node:path";
30765
+ import process4 from "node:process";
30346
30766
 
30347
30767
  // src/init/merge-hooks.ts
30348
30768
  import { existsSync as existsSync21, mkdirSync, readFileSync as readFileSync16, writeFileSync as writeFileSync2 } from "node:fs";
@@ -30412,7 +30832,7 @@ function walkNodeModulesCli(cwd) {
30412
30832
  }
30413
30833
  function isInsidePackageRoot(cwd) {
30414
30834
  const rel = relative10(PACKAGE_ROOT, resolve8(cwd)).replace(/\\/g, "/");
30415
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
30835
+ return rel === "" || !(rel.startsWith("..") || rel.startsWith("/"));
30416
30836
  }
30417
30837
  function nodeCliHookCommand(cliPath) {
30418
30838
  return `node ${cliPath} hook customize`;
@@ -30462,6 +30882,36 @@ function writeJson(path2, value) {
30462
30882
  function deepEqual(a, b) {
30463
30883
  return JSON.stringify(a) === JSON.stringify(b);
30464
30884
  }
30885
+ function mergeExistingCursorHook(input) {
30886
+ const { postToolUse, skeletonIdx, canonical, opts } = input;
30887
+ const current = postToolUse[skeletonIdx];
30888
+ const userEdited = current && !opts.forceHooks && (current.matcher !== canonical.matcher || !isSkeletonHookCommand(current.command));
30889
+ if (userEdited) {
30890
+ return {
30891
+ platform: "cursor",
30892
+ action: "conflict",
30893
+ message: identityKey("cursor", "postToolUse", String(current.matcher ?? "Read"))
30894
+ };
30895
+ }
30896
+ const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["command", "matcher"].includes(key)));
30897
+ const merged = { ...extras, ...canonical };
30898
+ if (deepEqual(current, merged))
30899
+ return { platform: "cursor", action: "skipped" };
30900
+ postToolUse[skeletonIdx] = merged;
30901
+ return null;
30902
+ }
30903
+ function persistCursorHooks(input) {
30904
+ const { targetPath, existing, hooks, postToolUse, skeletonIdx } = input;
30905
+ const next = {
30906
+ ...existing,
30907
+ version: existing.version ?? 1,
30908
+ hooks: { ...hooks, postToolUse }
30909
+ };
30910
+ if (deepEqual(existing, next))
30911
+ return { platform: "cursor", action: "skipped" };
30912
+ writeJson(targetPath, next);
30913
+ return { platform: "cursor", action: skeletonIdx >= 0 ? "updated" : "added" };
30914
+ }
30465
30915
  function mergeCursorHooks(targetPath, fragment, opts) {
30466
30916
  const existing = readJson(targetPath) ?? {};
30467
30917
  const hooks = existing.hooks ?? {};
@@ -30472,79 +30922,103 @@ function mergeCursorHooks(targetPath, fragment, opts) {
30472
30922
  const skeletonIdx = postToolUse.findIndex((entry) => isSkeletonHookCommand(entry.command));
30473
30923
  const canonical = { ...incoming, matcher: incoming.matcher ?? "Read" };
30474
30924
  if (skeletonIdx >= 0) {
30475
- const current = postToolUse[skeletonIdx];
30476
- const userEdited = current && !opts.forceHooks && (current.matcher !== canonical.matcher || !isSkeletonHookCommand(current.command));
30477
- if (userEdited) {
30478
- return {
30479
- platform: "cursor",
30480
- action: "conflict",
30481
- message: identityKey("cursor", "postToolUse", String(current.matcher ?? "Read"))
30482
- };
30483
- }
30484
- const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["command", "matcher"].includes(key)));
30485
- const merged = { ...extras, ...canonical };
30486
- if (deepEqual(current, merged))
30487
- return { platform: "cursor", action: "skipped" };
30488
- postToolUse[skeletonIdx] = merged;
30925
+ const conflict = mergeExistingCursorHook({ postToolUse, skeletonIdx, canonical, opts });
30926
+ if (conflict)
30927
+ return conflict;
30489
30928
  } else {
30490
30929
  postToolUse.push(canonical);
30491
30930
  }
30492
- const next = {
30493
- ...existing,
30494
- version: existing.version ?? 1,
30495
- hooks: { ...hooks, postToolUse }
30496
- };
30497
- if (deepEqual(existing, next))
30498
- return { platform: "cursor", action: "skipped" };
30499
- writeJson(targetPath, next);
30500
- return { platform: "cursor", action: skeletonIdx >= 0 ? "updated" : "added" };
30931
+ return persistCursorHooks({ targetPath, existing, hooks, postToolUse, skeletonIdx });
30501
30932
  }
30502
- function mergeNestedHooks(platform, targetPath, fragment, eventName, opts) {
30503
- const existing = readJson(targetPath) ?? {};
30504
- const rootHooks = existing.hooks ?? {};
30505
- const eventHooks = [...rootHooks[eventName] ?? []];
30506
- const incomingGroups = fragment.hooks?.[eventName] ?? [];
30933
+ function mergeExistingNestedSkeletonHook(input) {
30934
+ const { hooks, skeletonIdx, incomingHook, opts, platform, eventName, matcher } = input;
30935
+ const current = hooks[skeletonIdx];
30936
+ const userEdited = current && !opts.forceHooks && (current.type !== incomingHook.type || !isSkeletonHookCommand(current.command));
30937
+ if (userEdited) {
30938
+ return {
30939
+ changed: false,
30940
+ conflict: {
30941
+ platform,
30942
+ action: "conflict",
30943
+ message: identityKey(platform, eventName, matcher)
30944
+ }
30945
+ };
30946
+ }
30947
+ const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["type", "command"].includes(key)));
30948
+ const merged = { ...extras, ...incomingHook };
30949
+ if (!deepEqual(current, merged)) {
30950
+ hooks[skeletonIdx] = merged;
30951
+ return { changed: true };
30952
+ }
30953
+ return { changed: false };
30954
+ }
30955
+ function mergeIncomingHookGroup(args) {
30956
+ const { group, incomingHook, eventHooks, opts, platform, eventName } = args;
30957
+ const matcher = group.matcher ?? "";
30958
+ const groupIdx = eventHooks.findIndex((g) => g.matcher === matcher);
30959
+ if (groupIdx < 0) {
30960
+ eventHooks.push({ ...group, hooks: [{ ...incomingHook }] });
30961
+ return { changed: true };
30962
+ }
30963
+ const existingGroup = eventHooks[groupIdx];
30964
+ const hooks = [...existingGroup?.hooks ?? []];
30965
+ const skeletonIdx = hooks.findIndex((entry) => isSkeletonHookCommand(entry.command));
30966
+ if (skeletonIdx >= 0) {
30967
+ const result = mergeExistingNestedSkeletonHook({
30968
+ hooks,
30969
+ skeletonIdx,
30970
+ incomingHook,
30971
+ opts,
30972
+ platform,
30973
+ eventName,
30974
+ matcher
30975
+ });
30976
+ if (result.conflict)
30977
+ return { changed: false, conflict: result.conflict };
30978
+ eventHooks[groupIdx] = { ...existingGroup, matcher, hooks };
30979
+ return { changed: result.changed };
30980
+ }
30981
+ hooks.push({ ...incomingHook });
30982
+ eventHooks[groupIdx] = { ...existingGroup, matcher, hooks };
30983
+ return { changed: true };
30984
+ }
30985
+ function mergeIncomingGroups(args) {
30507
30986
  let changed = false;
30508
- for (const incomingGroup of incomingGroups) {
30509
- const matcher = incomingGroup.matcher ?? "";
30987
+ for (const incomingGroup of args.incomingGroups) {
30510
30988
  const incomingHook = incomingGroup.hooks?.[0];
30511
30989
  if (!incomingHook)
30512
30990
  continue;
30513
- const groupIdx = eventHooks.findIndex((group2) => group2.matcher === matcher);
30514
- if (groupIdx < 0) {
30515
- eventHooks.push({
30516
- ...incomingGroup,
30517
- hooks: [{ ...incomingHook }]
30518
- });
30519
- changed = true;
30520
- continue;
30521
- }
30522
- const group = eventHooks[groupIdx];
30523
- const hooks = [...group?.hooks ?? []];
30524
- const skeletonIdx = hooks.findIndex((entry) => isSkeletonHookCommand(entry.command));
30525
- if (skeletonIdx >= 0) {
30526
- const current = hooks[skeletonIdx];
30527
- const userEdited = current && !opts.forceHooks && (current.type !== incomingHook.type || !isSkeletonHookCommand(current.command));
30528
- if (userEdited) {
30529
- return {
30530
- platform,
30531
- action: "conflict",
30532
- message: identityKey(platform, eventName, matcher)
30533
- };
30534
- }
30535
- const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["type", "command"].includes(key)));
30536
- const merged = { ...extras, ...incomingHook };
30537
- if (!deepEqual(current, merged)) {
30538
- hooks[skeletonIdx] = merged;
30539
- changed = true;
30540
- }
30541
- } else {
30542
- hooks.push({ ...incomingHook });
30991
+ const result = mergeIncomingHookGroup({
30992
+ group: incomingGroup,
30993
+ incomingHook,
30994
+ eventHooks: args.eventHooks,
30995
+ opts: args.opts,
30996
+ platform: args.platform,
30997
+ eventName: args.eventName
30998
+ });
30999
+ if (result.conflict)
31000
+ return { changed: false, conflict: result.conflict };
31001
+ if (result.changed)
30543
31002
  changed = true;
30544
- }
30545
- eventHooks[groupIdx] = { ...group, matcher, hooks };
30546
31003
  }
30547
- if (!changed)
31004
+ return { changed };
31005
+ }
31006
+ function mergeNestedHooks(args) {
31007
+ const { platform, targetPath, eventName } = args;
31008
+ const existing = readJson(targetPath) ?? {};
31009
+ const rootHooks = existing.hooks ?? {};
31010
+ const eventHooks = [...rootHooks[eventName] ?? []];
31011
+ const incomingGroups = args.fragment.hooks?.[eventName] ?? [];
31012
+ const result = mergeIncomingGroups({
31013
+ incomingGroups,
31014
+ eventHooks,
31015
+ opts: args.opts,
31016
+ platform,
31017
+ eventName
31018
+ });
31019
+ if (result.conflict)
31020
+ return result.conflict;
31021
+ if (!result.changed)
30548
31022
  return { platform, action: "skipped" };
30549
31023
  const next = {
30550
31024
  ...existing,
@@ -30560,11 +31034,23 @@ function mergeHookConfigs(opts) {
30560
31034
  results.push(mergeCursorHooks(cursorPath, cursorFragment, opts));
30561
31035
  const claudePath = join18(opts.cwd, ".claude/settings.json");
30562
31036
  const claudeFragment = loadFragment("claude-settings.fragment.json", opts.hookCommand);
30563
- results.push(mergeNestedHooks("claude", claudePath, claudeFragment, "PostToolUse", opts));
31037
+ results.push(mergeNestedHooks({
31038
+ platform: "claude",
31039
+ targetPath: claudePath,
31040
+ fragment: claudeFragment,
31041
+ eventName: "PostToolUse",
31042
+ opts
31043
+ }));
30564
31044
  const codexPath = join18(opts.cwd, ".codex/hooks.json");
30565
31045
  if (existsSync21(join18(opts.cwd, ".codex"))) {
30566
31046
  const codexFragment = loadFragment("codex-hooks.fragment.json", opts.hookCommand);
30567
- results.push(mergeNestedHooks("codex", codexPath, codexFragment, "PostToolUse", opts));
31047
+ results.push(mergeNestedHooks({
31048
+ platform: "codex",
31049
+ targetPath: codexPath,
31050
+ fragment: codexFragment,
31051
+ eventName: "PostToolUse",
31052
+ opts
31053
+ }));
30568
31054
  } else {
30569
31055
  results.push({ platform: "codex", action: "skipped", message: "missing .codex directory" });
30570
31056
  }
@@ -30669,26 +31155,36 @@ function runSkillsAdd(args, cwd) {
30669
31155
  });
30670
31156
  return result.status ?? 1;
30671
31157
  }
31158
+ function logHookMergeResult(result) {
31159
+ if (result.action === "conflict") {
31160
+ console.error(`init: skipped ${result.platform} hook (user-edited) — re-run with --force-hooks to restore`);
31161
+ return;
31162
+ }
31163
+ if (result.action === "added")
31164
+ console.log(`init: added ${result.platform} customize hook`);
31165
+ if (result.action === "updated")
31166
+ console.log(`init: updated ${result.platform} customize hook`);
31167
+ }
31168
+ function installSkillsIfRequested(options, cwd) {
31169
+ if (!(options.skills && !options.noSkills))
31170
+ return "skipped";
31171
+ const args = skillsAddArgs({ skillsFlags: options.skillsFlags });
31172
+ const run = options.runSkillsCommand ?? runSkillsAdd;
31173
+ const exitCode = run(args, cwd);
31174
+ if (exitCode !== 0)
31175
+ throw new Error(`skills install failed: npx ${args.join(" ")}`);
31176
+ console.log("init: installed /skeleton skill");
31177
+ return "installed";
31178
+ }
30672
31179
  function runInit(options = {}) {
30673
- const cwd = options.cwd ?? process.cwd();
31180
+ const cwd = options.cwd ?? process4.cwd();
30674
31181
  assertPackageResolvable(cwd);
30675
31182
  const scaffold = writeScaffold(cwd);
30676
31183
  const hookCommand = resolveHookCommand(cwd);
30677
- const hooks = mergeHookConfigs({
30678
- cwd,
30679
- hookCommand,
30680
- forceHooks: options.forceHooks
30681
- });
31184
+ const hooks = mergeHookConfigs({ cwd, hookCommand, forceHooks: options.forceHooks });
30682
31185
  const scripts = mergePackageJsonScripts(cwd);
30683
- for (const result of hooks) {
30684
- if (result.action === "conflict") {
30685
- console.error(`init: skipped ${result.platform} hook (user-edited) — re-run with --force-hooks to restore`);
30686
- } else if (result.action === "added") {
30687
- console.log(`init: added ${result.platform} customize hook`);
30688
- } else if (result.action === "updated") {
30689
- console.log(`init: updated ${result.platform} customize hook`);
30690
- }
30691
- }
31186
+ for (const result of hooks)
31187
+ logHookMergeResult(result);
30692
31188
  if (scaffold === "created") {
30693
31189
  console.log("init: wrote .skeleton/config.yaml and registry.md");
30694
31190
  } else {
@@ -30697,17 +31193,7 @@ function runInit(options = {}) {
30697
31193
  if (scripts === "updated") {
30698
31194
  console.log("init: merged validate/audit scripts into package.json");
30699
31195
  }
30700
- let skills = "skipped";
30701
- if (options.skills && !options.noSkills) {
30702
- const args = skillsAddArgs({ skillsFlags: options.skillsFlags });
30703
- const run = options.runSkillsCommand ?? runSkillsAdd;
30704
- const exitCode = run(args, cwd);
30705
- if (exitCode !== 0) {
30706
- throw new Error(`skills install failed: npx ${args.join(" ")}`);
30707
- }
30708
- skills = "installed";
30709
- console.log("init: installed /skeleton skill");
30710
- }
31196
+ const skills = installSkillsIfRequested(options, cwd);
30711
31197
  return { scaffold, hooks, scripts, skills };
30712
31198
  }
30713
31199
 
@@ -30798,7 +31284,7 @@ function sourceFingerprint(tsAbs, seen = new Set) {
30798
31284
  hash.update("\x00");
30799
31285
  hash.update(content3);
30800
31286
  hash.update("\x00");
30801
- for (const dep of localImportPaths(abs, content3).sort()) {
31287
+ for (const dep of localImportPaths(abs, content3).sort((a, b) => a.localeCompare(b))) {
30802
31288
  walk(dep);
30803
31289
  }
30804
31290
  }
@@ -30869,6 +31355,9 @@ async function runBuildPlugin(options = {}) {
30869
31355
  return { built, checked };
30870
31356
  }
30871
31357
 
31358
+ // src/references/run.ts
31359
+ import process6 from "node:process";
31360
+
30872
31361
  // src/references/sync.ts
30873
31362
  import {
30874
31363
  existsSync as existsSync24,
@@ -30879,14 +31368,13 @@ import {
30879
31368
  writeFileSync as writeFileSync4
30880
31369
  } from "node:fs";
30881
31370
  import { dirname as dirname12, join as join20, relative as relative11 } from "node:path";
31371
+ import process5 from "node:process";
30882
31372
  function resolveOwnership(root2, override) {
30883
31373
  if (override !== undefined)
30884
31374
  return override;
30885
31375
  try {
30886
31376
  return loadConfig(root2).skillOwnership;
30887
- } catch {
30888
- return;
30889
- }
31377
+ } catch {}
30890
31378
  }
30891
31379
  function walkMarkdownFiles2(dir, root2) {
30892
31380
  const files = [];
@@ -30906,89 +31394,97 @@ function walkMarkdownFiles2(dir, root2) {
30906
31394
  }
30907
31395
  return files;
30908
31396
  }
31397
+ function collectGeneratedInDir(input) {
31398
+ const { dir, refsDir, skill, files } = input;
31399
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
31400
+ const fullPath = join20(dir, entry.name);
31401
+ if (entry.isDirectory()) {
31402
+ collectGeneratedInDir({ dir: fullPath, refsDir, skill, files });
31403
+ continue;
31404
+ }
31405
+ if (!entry.name.endsWith(".md"))
31406
+ continue;
31407
+ const content3 = readFileSync19(fullPath, "utf8");
31408
+ if (!isGeneratedReference(content3))
31409
+ continue;
31410
+ const refPath = normalizeRelPath(relative11(refsDir, fullPath));
31411
+ files.push(generatedRefPath(skill, refPath));
31412
+ }
31413
+ }
30909
31414
  function listGeneratedReferenceFiles(skillDir, skill) {
30910
31415
  const refsDir = join20(skillDir, "references");
30911
31416
  if (!existsSync24(refsDir))
30912
31417
  return [];
30913
31418
  const files = [];
30914
- const walk = (dir) => {
30915
- for (const entry of readdirSync6(dir, { withFileTypes: true })) {
30916
- const fullPath = join20(dir, entry.name);
30917
- if (entry.isDirectory()) {
30918
- walk(fullPath);
30919
- continue;
30920
- }
30921
- if (!entry.name.endsWith(".md"))
30922
- continue;
30923
- const content3 = readFileSync19(fullPath, "utf8");
30924
- if (isGeneratedReference(content3)) {
30925
- const refPath = normalizeRelPath(relative11(refsDir, fullPath));
30926
- files.push(generatedRefPath(skill, refPath));
30927
- }
30928
- }
30929
- };
30930
- walk(refsDir);
31419
+ collectGeneratedInDir({ dir: refsDir, refsDir, skill, files });
30931
31420
  return files;
30932
31421
  }
31422
+ function syncGeneratedCopy(ctx, refPath) {
31423
+ const { root: root2, plan, options, result } = ctx;
31424
+ const sourceRel = normalizeRelPath(join20(CANONICAL_REFS_DIR, refPath));
31425
+ const canonicalPath = join20(root2, sourceRel);
31426
+ if (!existsSync24(canonicalPath)) {
31427
+ throw new Error(`canonical reference missing: ${sourceRel}`);
31428
+ }
31429
+ const targetRel = generatedRefPath(plan.skill, refPath);
31430
+ const targetPath = join20(root2, targetRel);
31431
+ const canonicalContent = readFileSync19(canonicalPath, "utf8");
31432
+ const nextContent = formatGeneratedHeader(sourceRel) + canonicalContent;
31433
+ if (!options.dryRun)
31434
+ mkdirSync3(dirname12(targetPath), { recursive: true });
31435
+ const existing = existsSync24(targetPath) ? readFileSync19(targetPath, "utf8") : null;
31436
+ if (existing !== nextContent) {
31437
+ if (!options.dryRun)
31438
+ writeFileSync4(targetPath, nextContent, "utf8");
31439
+ result.written.push(targetRel);
31440
+ } else {
31441
+ result.skipped.push(targetRel);
31442
+ }
31443
+ }
31444
+ function rewritePlanLinks(ctx, skillDir) {
31445
+ const { root: root2, plan, options, result } = ctx;
31446
+ if (options.rewriteLinks === false)
31447
+ return;
31448
+ for (const relFile of walkMarkdownFiles2(skillDir, root2)) {
31449
+ const filePath = join20(root2, relFile);
31450
+ const content3 = readFileSync19(filePath, "utf8");
31451
+ const next = rewriteSharedRefLinks(content3, relFile, plan.skill);
31452
+ if (next === content3)
31453
+ continue;
31454
+ if (!options.dryRun)
31455
+ writeFileSync4(filePath, next, "utf8");
31456
+ result.rewritten.push(relFile);
31457
+ }
31458
+ }
31459
+ function removeStaleGenerated(ctx, skillDir) {
31460
+ const { root: root2, plan, options, result } = ctx;
31461
+ for (const generatedRel of listGeneratedReferenceFiles(skillDir, plan.skill)) {
31462
+ const refPath = generatedRel.slice(`${plan.skill}/references/`.length);
31463
+ if (plan.refPaths.has(refPath))
31464
+ continue;
31465
+ if (!options.dryRun)
31466
+ unlinkSync(join20(root2, generatedRel));
31467
+ result.removed.push(generatedRel);
31468
+ }
31469
+ }
31470
+ function syncPlan(ctx) {
31471
+ const skillDir = join20(ctx.root, ctx.plan.skill);
31472
+ for (const refPath of ctx.plan.refPaths) {
31473
+ syncGeneratedCopy(ctx, refPath);
31474
+ }
31475
+ rewritePlanLinks(ctx, skillDir);
31476
+ removeStaleGenerated(ctx, skillDir);
31477
+ }
30933
31478
  function syncReferences(options = {}) {
30934
- const root2 = options.root ?? process.cwd();
31479
+ const root2 = options.root ?? process5.cwd();
30935
31480
  const canonicalDir = join20(root2, CANONICAL_REFS_DIR);
30936
31481
  if (!existsSync24(canonicalDir)) {
30937
31482
  throw new Error(`canonical references dir not found: ${CANONICAL_REFS_DIR}`);
30938
31483
  }
30939
- const result = {
30940
- written: [],
30941
- rewritten: [],
30942
- removed: [],
30943
- skipped: []
30944
- };
31484
+ const result = { written: [], rewritten: [], removed: [], skipped: [] };
30945
31485
  const plans = discoverSkillReferencePlans(root2, resolveOwnership(root2, options.ownership));
30946
- for (const plan of plans) {
30947
- const skillDir = join20(root2, plan.skill);
30948
- for (const refPath of plan.refPaths) {
30949
- const sourceRel = normalizeRelPath(join20(CANONICAL_REFS_DIR, refPath));
30950
- const canonicalPath = join20(root2, sourceRel);
30951
- if (!existsSync24(canonicalPath)) {
30952
- throw new Error(`canonical reference missing: ${sourceRel}`);
30953
- }
30954
- const targetRel = generatedRefPath(plan.skill, refPath);
30955
- const targetPath = join20(root2, targetRel);
30956
- const canonicalContent = readFileSync19(canonicalPath, "utf8");
30957
- const nextContent = formatGeneratedHeader(sourceRel) + canonicalContent;
30958
- if (!options.dryRun) {
30959
- mkdirSync3(dirname12(targetPath), { recursive: true });
30960
- }
30961
- const existing = existsSync24(targetPath) ? readFileSync19(targetPath, "utf8") : null;
30962
- if (existing !== nextContent) {
30963
- if (!options.dryRun)
30964
- writeFileSync4(targetPath, nextContent, "utf8");
30965
- result.written.push(targetRel);
30966
- } else {
30967
- result.skipped.push(targetRel);
30968
- }
30969
- }
30970
- if (options.rewriteLinks !== false) {
30971
- for (const relFile of walkMarkdownFiles2(skillDir, root2)) {
30972
- const filePath = join20(root2, relFile);
30973
- const content3 = readFileSync19(filePath, "utf8");
30974
- const next = rewriteSharedRefLinks(content3, relFile, plan.skill);
30975
- if (next !== content3) {
30976
- if (!options.dryRun)
30977
- writeFileSync4(filePath, next, "utf8");
30978
- result.rewritten.push(relFile);
30979
- }
30980
- }
30981
- }
30982
- for (const generatedRel of listGeneratedReferenceFiles(skillDir, plan.skill)) {
30983
- const refPath = generatedRel.slice(`${plan.skill}/references/`.length);
30984
- if (!plan.refPaths.has(refPath)) {
30985
- const fullPath = join20(root2, generatedRel);
30986
- if (!options.dryRun)
30987
- unlinkSync(fullPath);
30988
- result.removed.push(generatedRel);
30989
- }
30990
- }
30991
- }
31486
+ for (const plan of plans)
31487
+ syncPlan({ root: root2, plan, options, result });
30992
31488
  return result;
30993
31489
  }
30994
31490
 
@@ -30997,7 +31493,7 @@ function runReferencesSync(options = {}) {
30997
31493
  return syncReferences(options);
30998
31494
  }
30999
31495
  function runReferencesCheck(options = {}) {
31000
- const root2 = options.root ?? process.cwd();
31496
+ const root2 = options.root ?? process6.cwd();
31001
31497
  let ownership;
31002
31498
  try {
31003
31499
  ownership = loadConfig(root2).skillOwnership;
@@ -31011,22 +31507,17 @@ function runReferencesCheck(options = {}) {
31011
31507
  label: "References check"
31012
31508
  });
31013
31509
  }
31510
+ function printSyncSection(label, files, prefix) {
31511
+ if (files.length === 0)
31512
+ return;
31513
+ console.log(label);
31514
+ for (const file of files)
31515
+ console.log(` ${prefix} ${file}`);
31516
+ }
31014
31517
  function printSyncResult(result) {
31015
- if (result.written.length > 0) {
31016
- console.log(`references sync: wrote ${result.written.length} file(s)`);
31017
- for (const file of result.written)
31018
- console.log(` + ${file}`);
31019
- }
31020
- if (result.rewritten.length > 0) {
31021
- console.log(`references sync: rewrote links in ${result.rewritten.length} file(s)`);
31022
- for (const file of result.rewritten)
31023
- console.log(` ~ ${file}`);
31024
- }
31025
- if (result.removed.length > 0) {
31026
- console.log(`references sync: removed ${result.removed.length} stale file(s)`);
31027
- for (const file of result.removed)
31028
- console.log(` - ${file}`);
31029
- }
31518
+ printSyncSection(`references sync: wrote ${result.written.length} file(s)`, result.written, "+");
31519
+ printSyncSection(`references sync: rewrote links in ${result.rewritten.length} file(s)`, result.rewritten, "~");
31520
+ printSyncSection(`references sync: removed ${result.removed.length} stale file(s)`, result.removed, "-");
31030
31521
  if (result.written.length === 0 && result.rewritten.length === 0 && result.removed.length === 0) {
31031
31522
  console.log(`references sync: up to date (${result.skipped.length} file(s) checked)`);
31032
31523
  }
@@ -31060,7 +31551,7 @@ function parseRegistryRows(content3) {
31060
31551
  for (const line of content3.split(`
31061
31552
  `)) {
31062
31553
  const match = REGISTRY_TABLE_ROW_RE2.exec(line);
31063
- if (!match?.[1] || !match[2])
31554
+ if (!(match?.[1] && match[2]))
31064
31555
  continue;
31065
31556
  rows.push({ topic: match[1].trim(), link: match[2].trim(), line });
31066
31557
  }
@@ -31091,7 +31582,8 @@ ${REGISTRY_TABLE_HEADER}
31091
31582
 
31092
31583
  `;
31093
31584
  }
31094
- function upsertRow(content3, topic, link2, section, root2) {
31585
+ function upsertRow(opts) {
31586
+ const { content: content3, topic, link: link2, section, root: root2 } = opts;
31095
31587
  const rows = parseRegistryRows(content3);
31096
31588
  const targetPath = link2;
31097
31589
  const existingByLink = rows.find((row) => row.link === targetPath);
@@ -31131,13 +31623,19 @@ ${newLine}
31131
31623
  `;
31132
31624
  return { content: appended, action: "added" };
31133
31625
  }
31134
- function registerPath(options) {
31135
- const root2 = options.root ?? findRepoRoot();
31136
- const relPath2 = normalizeRelPath(options.path);
31137
- const absPath = join21(root2, relPath2);
31138
- if (!existsSync25(absPath)) {
31139
- throw new Error(`File not found: ${relPath2}`);
31626
+ function printRegisterResult(options, result, relPath2) {
31627
+ if (options.json) {
31628
+ console.log(JSON.stringify(result, null, 2));
31629
+ } else if (options.dryRun) {
31630
+ console.log(`dry-run: would ${result.action} registry row for ${relPath2} → ${result.topic}`);
31631
+ } else if (result.action === "noop") {
31632
+ console.log(`register: ${relPath2} already registered (${result.topic})`);
31633
+ } else {
31634
+ console.log(`register: ${result.action} ${relPath2} → ${result.topic}`);
31140
31635
  }
31636
+ }
31637
+ function resolveRegistrationTopic(input) {
31638
+ const { options, root: root2, relPath: relPath2, absPath } = input;
31141
31639
  const content3 = readFileSync20(absPath, "utf8");
31142
31640
  let topic = options.topic ?? extractTopic(content3);
31143
31641
  if (!topic) {
@@ -31145,13 +31643,36 @@ function registerPath(options) {
31145
31643
  }
31146
31644
  const registryLink = toRegistryLink(root2, absPath);
31147
31645
  topic = ensureCustomizeTopic(topic, registryLink);
31148
- const section = inferSection(registryLink);
31646
+ return { topic, registryLink, section: inferSection(registryLink) };
31647
+ }
31648
+ function loadRegistryContent(root2) {
31149
31649
  const registryAbs = join21(root2, REGISTRY_REL_PATH);
31150
- let registryContent = existsSync25(registryAbs) ? readFileSync20(registryAbs, "utf8") : defaultRegistryContent();
31151
- if (!existsSync25(registryAbs) && !existsSync25(join21(root2, ".skeleton/config.yaml"))) {
31650
+ if (!(existsSync25(registryAbs) || existsSync25(join21(root2, ".skeleton/config.yaml")))) {
31152
31651
  throw new Error("Missing .skeleton/config.yaml — run skeleton init first");
31153
31652
  }
31154
- const { content: updated, action } = upsertRow(registryContent, topic, registryLink, section, root2);
31653
+ return existsSync25(registryAbs) ? readFileSync20(registryAbs, "utf8") : defaultRegistryContent();
31654
+ }
31655
+ function registerPath(options) {
31656
+ const root2 = options.root ?? findRepoRoot();
31657
+ const relPath2 = normalizeRelPath(options.path);
31658
+ const absPath = join21(root2, relPath2);
31659
+ if (!existsSync25(absPath)) {
31660
+ throw new Error(`File not found: ${relPath2}`);
31661
+ }
31662
+ const { topic, registryLink, section } = resolveRegistrationTopic({
31663
+ options,
31664
+ root: root2,
31665
+ relPath: relPath2,
31666
+ absPath
31667
+ });
31668
+ let registryContent = loadRegistryContent(root2);
31669
+ const { content: updated, action } = upsertRow({
31670
+ content: registryContent,
31671
+ topic,
31672
+ link: registryLink,
31673
+ section,
31674
+ root: root2
31675
+ });
31155
31676
  registryContent = updated;
31156
31677
  const result = {
31157
31678
  topic,
@@ -31160,6 +31681,7 @@ function registerPath(options) {
31160
31681
  action,
31161
31682
  warnOutsideScan: isOutsideScan(root2, relPath2)
31162
31683
  };
31684
+ const registryAbs = join21(root2, REGISTRY_REL_PATH);
31163
31685
  if (!options.dryRun && action !== "noop") {
31164
31686
  const dir = dirname13(registryAbs);
31165
31687
  if (!existsSync25(dir)) {
@@ -31170,15 +31692,7 @@ function registerPath(options) {
31170
31692
  if (result.warnOutsideScan) {
31171
31693
  console.error(`warning: ${relPath2} is outside scan.include — register succeeded but audit will not scan it`);
31172
31694
  }
31173
- if (options.json) {
31174
- console.log(JSON.stringify(result, null, 2));
31175
- } else if (options.dryRun) {
31176
- console.log(`dry-run: would ${action} registry row for ${relPath2} → ${topic}`);
31177
- } else if (action === "noop") {
31178
- console.log(`register: ${relPath2} already registered (${topic})`);
31179
- } else {
31180
- console.log(`register: ${action} ${relPath2} → ${topic}`);
31181
- }
31695
+ printRegisterResult(options, result, relPath2);
31182
31696
  return result;
31183
31697
  }
31184
31698
 
@@ -31224,41 +31738,49 @@ function isSkeletonYamlCandidate(normalized, ext) {
31224
31738
  return false;
31225
31739
  return true;
31226
31740
  }
31227
- function bucketFor(relPath2, root2, wiredPolicies, skillIndex) {
31741
+ function bucketForSkillPath(normalized, skillIndex) {
31742
+ if (!isSkillPath(normalized, skillIndex))
31743
+ return "skip";
31744
+ if (isForeignSkillPath(normalized, skillIndex))
31745
+ return "foreign-skill";
31746
+ return "skills";
31747
+ }
31748
+ function bucketForDocPath(normalized, ctx) {
31749
+ const ext = extname2(normalized).toLowerCase();
31750
+ if (!DOC_EXTENSIONS.has(ext))
31751
+ return "skip";
31752
+ const config = loadConfig(ctx.root);
31753
+ if (isInScanPerimeter(normalized, { config, root: ctx.root, skillIndex: ctx.skillIndex })) {
31754
+ return "docs";
31755
+ }
31756
+ return "skip";
31757
+ }
31758
+ function bucketFor(relPath2, ctx) {
31228
31759
  const normalized = normalizeRelPath(relPath2);
31229
31760
  const ext = extname2(normalized).toLowerCase();
31230
31761
  const name = basename5(normalized);
31231
- if (SKIP_EXTENSIONS.has(ext))
31232
- return "skip";
31233
- if (COMMAND_CONFIG_NAMES.has(name))
31762
+ if (SKIP_EXTENSIONS.has(ext) || COMMAND_CONFIG_NAMES.has(name))
31234
31763
  return "skip";
31235
31764
  if (isSkeletonYamlCandidate(normalized, ext)) {
31236
- if (wiredPolicies.has(normalized))
31237
- return "policy";
31238
- return "skip";
31239
- }
31240
- if (isSkillPath(normalized, skillIndex)) {
31241
- if (isForeignSkillPath(normalized, skillIndex))
31242
- return "foreign-skill";
31243
- return "skills";
31244
- }
31245
- if (DOC_EXTENSIONS.has(ext)) {
31246
- const config = loadConfig(root2);
31247
- if (isInScanPerimeter(normalized, config, root2, skillIndex))
31248
- return "docs";
31249
- return "skip";
31250
- }
31765
+ return ctx.wiredPolicies.has(normalized) ? "policy" : "skip";
31766
+ }
31767
+ const skillBucket = bucketForSkillPath(normalized, ctx.skillIndex);
31768
+ if (skillBucket !== "skip")
31769
+ return skillBucket;
31770
+ const docBucket = bucketForDocPath(normalized, ctx);
31771
+ if (docBucket !== "skip")
31772
+ return docBucket;
31251
31773
  if (SHELL_EXTENSIONS.has(ext))
31252
31774
  return "shell";
31253
31775
  if (ext === ".json")
31254
31776
  return "json";
31255
31777
  return "skip";
31256
31778
  }
31257
- function isInScanPerimeter(relPath2, config, root2, skillIndex) {
31258
- const scanned = new Set(collectScanFiles(config, root2, skillIndex).map((abs) => relPath(abs, root2)));
31779
+ function isInScanPerimeter(relPath2, ctx) {
31780
+ const scanned = new Set(collectScanFiles(ctx.config, ctx.root, ctx.skillIndex).map((abs) => relPath(abs, ctx.root)));
31259
31781
  if (scanned.has(relPath2))
31260
31782
  return true;
31261
- return config.scan.include.some((pattern) => matchesGlobScope(relPath2, pattern));
31783
+ return ctx.config.scan.include.some((pattern) => matchesGlobScope(relPath2, pattern));
31262
31784
  }
31263
31785
  function parseJsonContent(content3) {
31264
31786
  try {
@@ -31310,27 +31832,30 @@ function resolvePaths(options) {
31310
31832
  root: options.root
31311
31833
  });
31312
31834
  }
31313
- function codeValidationHint(root2) {
31314
- let pm2 = null;
31835
+ function packageManagerFromPackageJson(root2) {
31315
31836
  const pkgPath = join22(root2, "package.json");
31316
- if (existsSync26(pkgPath)) {
31317
- try {
31318
- const pkg = JSON.parse(readFileSync21(pkgPath, "utf8"));
31319
- const raw = pkg.packageManager?.split("@")[0];
31320
- if (raw === "bun" || raw === "npm" || raw === "pnpm" || raw === "yarn")
31321
- pm2 = raw;
31322
- } catch {}
31323
- }
31324
- if (!pm2) {
31325
- if (existsSync26(join22(root2, "bun.lock")) || existsSync26(join22(root2, "bun.lockb")))
31326
- pm2 = "bun";
31327
- else if (existsSync26(join22(root2, "pnpm-lock.yaml")))
31328
- pm2 = "pnpm";
31329
- else if (existsSync26(join22(root2, "yarn.lock")))
31330
- pm2 = "yarn";
31331
- else if (existsSync26(join22(root2, "package-lock.json")))
31332
- pm2 = "npm";
31333
- }
31837
+ if (!existsSync26(pkgPath))
31838
+ return null;
31839
+ try {
31840
+ const pkg = JSON.parse(readFileSync21(pkgPath, "utf8"));
31841
+ const raw = pkg.packageManager?.split("@")[0];
31842
+ if (raw === "bun" || raw === "npm" || raw === "pnpm" || raw === "yarn")
31843
+ return raw;
31844
+ } catch {}
31845
+ return null;
31846
+ }
31847
+ function packageManagerFromLockfiles(root2) {
31848
+ if (existsSync26(join22(root2, "bun.lock")) || existsSync26(join22(root2, "bun.lockb")))
31849
+ return "bun";
31850
+ if (existsSync26(join22(root2, "pnpm-lock.yaml")))
31851
+ return "pnpm";
31852
+ if (existsSync26(join22(root2, "yarn.lock")))
31853
+ return "yarn";
31854
+ if (existsSync26(join22(root2, "package-lock.json")))
31855
+ return "npm";
31856
+ return null;
31857
+ }
31858
+ function hintForPackageManager(pm2) {
31334
31859
  switch (pm2) {
31335
31860
  case "bun":
31336
31861
  return " Run: bun test && bun run typecheck && bun run build";
@@ -31340,126 +31865,113 @@ function codeValidationHint(root2) {
31340
31865
  return " Run: pnpm test && pnpm run typecheck";
31341
31866
  case "yarn":
31342
31867
  return " Run: yarn test && yarn typecheck";
31343
- default:
31344
- return " Run your local code validation gates (test + typecheck + build).";
31345
31868
  }
31346
31869
  }
31347
- async function runValidateChanged(options = {}) {
31348
- const root2 = options.root ?? findRepoRoot();
31349
- const relPaths = resolvePaths(options);
31350
- if (relPaths.length === 0) {
31351
- console.log("validate changed: no changed files.");
31352
- return 0;
31353
- }
31354
- const config = loadConfig(root2);
31355
- const skillIndex = buildSkillIndex(root2, config.skillOwnership);
31356
- let wiredPolicies;
31357
- try {
31358
- wiredPolicies = await collectWiredPolicyRelPaths(root2, config);
31359
- } catch (error) {
31360
- console.error(`validate changed: ${error instanceof Error ? error.message : error}`);
31361
- return 1;
31362
- }
31363
- const buckets = {
31364
- docs: [],
31365
- skills: [],
31366
- shell: [],
31367
- json: [],
31368
- policy: []
31369
- };
31370
- let missing = 0;
31371
- let skipped = 0;
31372
- let foreignSkipped = 0;
31373
- const orphans = [];
31374
- for (const relPath2 of relPaths) {
31375
- const normalized = normalizeRelPath(relPath2);
31376
- const abs = join22(root2, normalized);
31377
- if (!existsSync26(abs)) {
31378
- missing++;
31379
- console.error(`validate changed: path not found: ${relPath2}`);
31380
- continue;
31381
- }
31382
- const ext = extname2(normalized).toLowerCase();
31383
- if (isSkeletonYamlCandidate(normalized, ext) && !wiredPolicies.has(normalized)) {
31384
- orphans.push(normalized);
31385
- continue;
31386
- }
31387
- const bucket = bucketFor(normalized, root2, wiredPolicies, skillIndex);
31388
- if (bucket === "skip") {
31389
- skipped++;
31390
- continue;
31391
- }
31392
- if (bucket === "foreign-skill") {
31393
- foreignSkipped++;
31394
- console.log(`validate changed: skipping foreign skill ${normalized} (owned upstream; see skills-lock.json / skillOwnership)`);
31395
- continue;
31396
- }
31397
- buckets[bucket].push(normalized);
31870
+ function codeValidationHint(root2) {
31871
+ const pm2 = packageManagerFromPackageJson(root2) ?? packageManagerFromLockfiles(root2);
31872
+ return pm2 ? hintForPackageManager(pm2) : " Run your local code validation gates (test + typecheck + build).";
31873
+ }
31874
+ function emptyBuckets() {
31875
+ return { docs: [], skills: [], shell: [], json: [], policy: [] };
31876
+ }
31877
+ function classifySinglePath(input) {
31878
+ const { relPath: relPath2, ctx, state, bucketCtx } = input;
31879
+ const normalized = normalizeRelPath(relPath2);
31880
+ const abs = join22(ctx.root, normalized);
31881
+ if (!existsSync26(abs)) {
31882
+ state.missing++;
31883
+ console.error(`validate changed: path not found: ${relPath2}`);
31884
+ return;
31398
31885
  }
31399
- if (orphans.length > 0) {
31400
- for (const orphan of orphans) {
31401
- console.error(`validate changed: ${orphan} is under .skeleton/ but not referenced by any plugin policies glob.
31402
- ` + " Export it from a plugin `policies` array (see docs/developer/plugins.md), or remove the file.");
31403
- }
31404
- return 1;
31886
+ const ext = extname2(normalized).toLowerCase();
31887
+ if (isSkeletonYamlCandidate(normalized, ext) && !ctx.wiredPolicies.has(normalized)) {
31888
+ state.orphans.push(normalized);
31889
+ return;
31405
31890
  }
31406
- const audited = buckets.docs.length + buckets.skills.length + buckets.shell.length + buckets.json.length + buckets.policy.length;
31407
- let exitCode = 0;
31408
- if (missing > 0 && audited === 0 && skipped === 0) {
31409
- console.error("validate changed: no paths existed on disk. Pass real paths or use --staged / --base.");
31410
- return 1;
31891
+ const bucket = bucketFor(normalized, bucketCtx);
31892
+ if (bucket === "skip") {
31893
+ state.skipped++;
31894
+ return;
31411
31895
  }
31412
- if (options.base) {
31413
- const globalExit = await runAudit({
31414
- suite: "self",
31415
- strict: false,
31416
- json: false,
31417
- paths: [],
31418
- only: null,
31419
- root: root2,
31420
- globalOnly: true
31421
- });
31422
- if (globalExit !== 0)
31423
- exitCode = 1;
31896
+ if (bucket === "foreign-skill") {
31897
+ state.foreignSkipped++;
31898
+ console.log(`validate changed: skipping foreign skill ${normalized} (owned upstream; see skills-lock.json / skillOwnership)`);
31899
+ return;
31424
31900
  }
31425
- if (skipped > 0 && audited === 0) {
31426
- console.error(`validate changed: all paths were skipped (code/config). This does not verify TypeScript or app code.
31427
- ` + codeValidationHint(root2));
31428
- return 1;
31901
+ state.buckets[bucket].push(normalized);
31902
+ }
31903
+ function classifyPaths(ctx) {
31904
+ const state = {
31905
+ buckets: emptyBuckets(),
31906
+ missing: 0,
31907
+ skipped: 0,
31908
+ foreignSkipped: 0,
31909
+ orphans: []
31910
+ };
31911
+ const bucketCtx = {
31912
+ root: ctx.root,
31913
+ wiredPolicies: ctx.wiredPolicies,
31914
+ skillIndex: ctx.skillIndex
31915
+ };
31916
+ for (const relPath2 of ctx.relPaths) {
31917
+ classifySinglePath({ relPath: relPath2, ctx, state, bucketCtx });
31429
31918
  }
31430
- if (buckets.docs.length > 0) {
31431
- const docExit = await runAudit({
31432
- suite: "docs",
31433
- strict: false,
31434
- json: false,
31435
- paths: buckets.docs,
31436
- only: null,
31437
- root: root2,
31438
- pathScopedOnly: true
31439
- });
31440
- if (docExit !== 0)
31441
- exitCode = 1;
31919
+ return state;
31920
+ }
31921
+ function reportOrphans(orphans) {
31922
+ for (const orphan of orphans) {
31923
+ console.error(`validate changed: ${orphan} is under .skeleton/ but not referenced by any plugin policies glob.
31924
+ ` + " Export it from a plugin `policies` array (see docs/developer/plugins.md), or remove the file.");
31442
31925
  }
31443
- if (buckets.skills.length > 0) {
31444
- if (!options.base) {
31445
- console.error(`validate changed: skill paths need the full skills suite (path-scoped skill rules are empty).
31926
+ return 1;
31927
+ }
31928
+ async function runGlobalAuditIfBase(base, root2) {
31929
+ if (!base)
31930
+ return 0;
31931
+ return runAudit({
31932
+ suite: "self",
31933
+ strict: false,
31934
+ json: false,
31935
+ paths: [],
31936
+ only: null,
31937
+ root: root2,
31938
+ globalOnly: true
31939
+ });
31940
+ }
31941
+ async function auditDocsBucket(paths, root2) {
31942
+ if (paths.length === 0)
31943
+ return 0;
31944
+ return runAudit({
31945
+ suite: "docs",
31946
+ strict: false,
31947
+ json: false,
31948
+ paths,
31949
+ only: null,
31950
+ root: root2,
31951
+ pathScopedOnly: true
31952
+ });
31953
+ }
31954
+ async function auditSkillsBucket(paths, root2, base) {
31955
+ if (paths.length === 0)
31956
+ return 0;
31957
+ if (!base) {
31958
+ console.error(`validate changed: skill paths need the full skills suite (path-scoped skill rules are empty).
31446
31959
  ` + ` Run: skeleton audit skills
31447
31960
  ` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
31448
- exitCode = 1;
31449
- } else {
31450
- const skillExit = await runAudit({
31451
- suite: "skills",
31452
- strict: false,
31453
- json: false,
31454
- paths: buckets.skills,
31455
- only: null,
31456
- root: root2,
31457
- pathScopedOnly: true
31458
- });
31459
- if (skillExit !== 0)
31460
- exitCode = 1;
31461
- }
31961
+ return 1;
31462
31962
  }
31963
+ return runAudit({
31964
+ suite: "skills",
31965
+ strict: false,
31966
+ json: false,
31967
+ paths,
31968
+ only: null,
31969
+ root: root2,
31970
+ pathScopedOnly: true
31971
+ });
31972
+ }
31973
+ function validateLocalBuckets(buckets, root2) {
31974
+ let exitCode = 0;
31463
31975
  for (const relPath2 of buckets.shell) {
31464
31976
  if (validateShell(relPath2, root2) !== 0)
31465
31977
  exitCode = 1;
@@ -31472,49 +31984,124 @@ async function runValidateChanged(options = {}) {
31472
31984
  if (validatePolicy(relPath2, root2) !== 0)
31473
31985
  exitCode = 1;
31474
31986
  }
31475
- if (buckets.policy.length > 0) {
31476
- if (options.base) {
31477
- const proseExit = await runAudit({
31478
- suite: "docs",
31479
- strict: false,
31480
- json: false,
31481
- paths: [],
31482
- only: null,
31483
- root: root2
31484
- });
31485
- if (proseExit !== 0)
31486
- exitCode = 1;
31487
- const skillPaths = listSkillMarkdownPaths(root2, skillIndex);
31488
- if (skillPaths.length > 0) {
31489
- const skillProseExit = await runAudit({
31490
- suite: "skills",
31491
- strict: false,
31492
- json: false,
31493
- paths: skillPaths,
31494
- only: null,
31495
- root: root2,
31496
- pathScopedOnly: true
31497
- });
31498
- if (skillProseExit !== 0)
31499
- exitCode = 1;
31500
- }
31501
- } else {
31502
- console.error(`validate changed: policy YAML changes need a full prose-policy pass (path-scoped docs are not enough).
31987
+ return exitCode;
31988
+ }
31989
+ async function provePolicyProse(root2, skillIndex) {
31990
+ const proseExit = await runAudit({
31991
+ suite: "docs",
31992
+ strict: false,
31993
+ json: false,
31994
+ paths: [],
31995
+ only: null,
31996
+ root: root2
31997
+ });
31998
+ if (proseExit !== 0)
31999
+ return 1;
32000
+ const skillPaths = listSkillMarkdownPaths(root2, skillIndex);
32001
+ if (skillPaths.length === 0)
32002
+ return 0;
32003
+ const skillProseExit = await runAudit({
32004
+ suite: "skills",
32005
+ strict: false,
32006
+ json: false,
32007
+ paths: skillPaths,
32008
+ only: null,
32009
+ root: root2,
32010
+ pathScopedOnly: true
32011
+ });
32012
+ return skillProseExit !== 0 ? 1 : 0;
32013
+ }
32014
+ async function auditPolicyBucket(ctx) {
32015
+ if (ctx.policyPaths.length === 0)
32016
+ return 0;
32017
+ if (!ctx.base) {
32018
+ console.error(`validate changed: policy YAML changes need a full prose-policy pass (path-scoped docs are not enough).
31503
32019
  ` + ` Run: skeleton audit docs
31504
32020
  ` + ` And: skeleton audit skills
31505
32021
  ` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
31506
- return 1;
31507
- }
32022
+ return 1;
31508
32023
  }
31509
- if (exitCode === 0) {
31510
- const parts = [];
31511
- if (skipped > 0)
31512
- parts.push(`${skipped} path(s) skipped`);
31513
- if (foreignSkipped > 0)
31514
- parts.push(`${foreignSkipped} foreign skill(s) ignored`);
31515
- const note = parts.length > 0 ? ` (${parts.join(", ")})` : "";
31516
- console.log(`validate changed passed${note}.`);
32024
+ return provePolicyProse(ctx.root, ctx.skillIndex);
32025
+ }
32026
+ function printSuccess2(skipped, foreignSkipped) {
32027
+ const parts = [];
32028
+ if (skipped > 0)
32029
+ parts.push(`${skipped} path(s) skipped`);
32030
+ if (foreignSkipped > 0)
32031
+ parts.push(`${foreignSkipped} foreign skill(s) ignored`);
32032
+ const note = parts.length > 0 ? ` (${parts.join(", ")})` : "";
32033
+ console.log(`validate changed passed${note}.`);
32034
+ }
32035
+ function auditedPathCount(buckets) {
32036
+ return buckets.docs.length + buckets.skills.length + buckets.shell.length + buckets.json.length + buckets.policy.length;
32037
+ }
32038
+ function earlyExitForClassification(classification, root2, base) {
32039
+ const { buckets, missing, skipped, orphans } = classification;
32040
+ if (orphans.length > 0)
32041
+ return reportOrphans(orphans);
32042
+ const audited = auditedPathCount(buckets);
32043
+ if (missing > 0 && audited === 0 && skipped === 0) {
32044
+ console.error("validate changed: no paths existed on disk. Pass real paths or use --staged / --base.");
32045
+ return 1;
31517
32046
  }
32047
+ if (skipped > 0 && audited === 0 && !base) {
32048
+ console.error(`validate changed: all paths were skipped (code/config). This does not verify TypeScript or app code.
32049
+ ` + codeValidationHint(root2));
32050
+ return 1;
32051
+ }
32052
+ return null;
32053
+ }
32054
+ async function runBucketAudits(ctx) {
32055
+ let exitCode = 0;
32056
+ const mergeExit = (code3) => {
32057
+ if (code3 !== 0)
32058
+ exitCode = 1;
32059
+ };
32060
+ mergeExit(await runGlobalAuditIfBase(ctx.base, ctx.root));
32061
+ mergeExit(await auditDocsBucket(ctx.buckets.docs, ctx.root));
32062
+ mergeExit(await auditSkillsBucket(ctx.buckets.skills, ctx.root, ctx.base));
32063
+ mergeExit(validateLocalBuckets(ctx.buckets, ctx.root));
32064
+ mergeExit(await auditPolicyBucket({
32065
+ policyPaths: ctx.buckets.policy,
32066
+ root: ctx.root,
32067
+ skillIndex: ctx.skillIndex,
32068
+ base: ctx.base
32069
+ }));
32070
+ return exitCode;
32071
+ }
32072
+ async function runValidateChanged(options = {}) {
32073
+ const root2 = options.root ?? findRepoRoot();
32074
+ const relPaths = resolvePaths(options);
32075
+ if (relPaths.length === 0) {
32076
+ console.log("validate changed: no changed files.");
32077
+ return 0;
32078
+ }
32079
+ const config = loadConfig(root2);
32080
+ const skillIndex = buildSkillIndex(root2, config.skillOwnership);
32081
+ let wiredPolicies;
32082
+ try {
32083
+ wiredPolicies = await collectWiredPolicyRelPaths(root2, config);
32084
+ } catch (error) {
32085
+ console.error(`validate changed: ${error instanceof Error ? error.message : error}`);
32086
+ return 1;
32087
+ }
32088
+ const classification = classifyPaths({
32089
+ relPaths,
32090
+ root: root2,
32091
+ wiredPolicies,
32092
+ skillIndex
32093
+ });
32094
+ const earlyExit = earlyExitForClassification(classification, root2, options.base);
32095
+ if (earlyExit !== null)
32096
+ return earlyExit;
32097
+ const exitCode = await runBucketAudits({
32098
+ buckets: classification.buckets,
32099
+ root: root2,
32100
+ skillIndex,
32101
+ base: options.base
32102
+ });
32103
+ if (exitCode === 0)
32104
+ printSuccess2(classification.skipped, classification.foreignSkipped);
31518
32105
  return exitCode;
31519
32106
  }
31520
32107
 
@@ -31546,123 +32133,153 @@ function parseRegisterArgs(argv) {
31546
32133
  json = true;
31547
32134
  else if (arg.startsWith("--topic="))
31548
32135
  topic = arg.slice("--topic=".length);
31549
- else if (!arg.startsWith("-") && !path2)
32136
+ else if (!(arg.startsWith("-") || path2))
31550
32137
  path2 = arg;
31551
32138
  }
31552
32139
  return { path: path2, topic, dryRun, json };
31553
32140
  }
32141
+ function parseValidateChangedArgs(rest) {
32142
+ const paths = [];
32143
+ let staged = false;
32144
+ let base;
32145
+ for (let i = 0;i < rest.length; i++) {
32146
+ const arg = rest[i];
32147
+ if (arg === "--staged")
32148
+ staged = true;
32149
+ else if (arg === "--base")
32150
+ base = rest[++i];
32151
+ else if (arg?.startsWith("--base="))
32152
+ base = arg.slice("--base=".length);
32153
+ else if (arg && !arg.startsWith("-"))
32154
+ paths.push(arg);
32155
+ }
32156
+ return { paths, staged, base };
32157
+ }
32158
+ async function handleAudit(argv) {
32159
+ const sub = argv[0];
32160
+ if (sub !== "docs" && sub !== "self" && sub !== "skills") {
32161
+ usage();
32162
+ return 1;
32163
+ }
32164
+ const options = parseAuditArgs(argv.slice(1));
32165
+ options.suite = sub;
32166
+ return runAudit(options);
32167
+ }
32168
+ async function handleBuildPlugin(argv) {
32169
+ const { entry, check } = parseBuildPluginArgs(argv);
32170
+ const root2 = findRepoRoot();
32171
+ const result = await runBuildPlugin({ root: root2, entry, check });
32172
+ if (check) {
32173
+ console.log(result.checked.length === 0 ? "build-plugin --check: no plugins configured." : `build-plugin --check: ${result.checked.length} plugin(s) up to date.`);
32174
+ } else {
32175
+ console.log(result.built.length === 0 ? "build-plugin: no plugins configured." : `build-plugin: built ${result.built.length} plugin(s).`);
32176
+ }
32177
+ return 0;
32178
+ }
32179
+ async function handleValidateChanged(argv) {
32180
+ const { paths, staged, base } = parseValidateChangedArgs(argv);
32181
+ return runValidateChanged({ paths, staged, base });
32182
+ }
32183
+ function handleRegister(argv) {
32184
+ const opts = parseRegisterArgs(argv);
32185
+ if (!opts.path) {
32186
+ console.error("register: path required");
32187
+ return 1;
32188
+ }
32189
+ registerPath({
32190
+ path: opts.path,
32191
+ topic: opts.topic,
32192
+ dryRun: opts.dryRun,
32193
+ json: opts.json
32194
+ });
32195
+ return 0;
32196
+ }
32197
+ function handleCustomizeResolve(argv) {
32198
+ const slug2 = argv[0];
32199
+ const json = argv.includes("--json");
32200
+ if (!slug2) {
32201
+ console.error("customize resolve: slug required");
32202
+ return 1;
32203
+ }
32204
+ const result = resolveCustomizeFromRoot(slug2);
32205
+ if (json) {
32206
+ console.log(JSON.stringify(result, null, 2));
32207
+ } else if (result.content) {
32208
+ process7.stdout.write(result.content);
32209
+ }
32210
+ return 0;
32211
+ }
32212
+ function handleHook(argv) {
32213
+ if (argv[0] !== "customize") {
32214
+ usage();
32215
+ return 1;
32216
+ }
32217
+ process7.stdout.write(runCustomizeHook(readFileSync22(0, "utf8")));
32218
+ return 0;
32219
+ }
32220
+ function handleInit(argv) {
32221
+ runInit(parseInitArgs(argv));
32222
+ return 0;
32223
+ }
32224
+ function handleReferences(argv) {
32225
+ const sub = argv[0];
32226
+ if (sub === "sync") {
32227
+ const dryRun = argv.includes("--dry-run");
32228
+ const rewriteLinks = !argv.includes("--no-rewrite-links");
32229
+ const result = runReferencesSync({ dryRun, rewriteLinks });
32230
+ printSyncResult(result);
32231
+ return 0;
32232
+ }
32233
+ if (sub === "check") {
32234
+ return runReferencesCheck({
32235
+ json: argv.includes("--json"),
32236
+ strict: argv.includes("--strict")
32237
+ });
32238
+ }
32239
+ usage();
32240
+ return 1;
32241
+ }
32242
+ async function dispatchCommand(argv) {
32243
+ const command = argv[0];
32244
+ const rest = argv.slice(1);
32245
+ switch (command) {
32246
+ case "audit":
32247
+ return handleAudit(rest);
32248
+ case "build-plugin":
32249
+ return handleBuildPlugin(rest);
32250
+ case "validate":
32251
+ return rest[0] === "changed" ? handleValidateChanged(rest.slice(1)) : null;
32252
+ case "register":
32253
+ return handleRegister(rest);
32254
+ case "customize":
32255
+ return rest[0] === "resolve" ? handleCustomizeResolve(rest.slice(1)) : null;
32256
+ case "hook":
32257
+ return handleHook(rest);
32258
+ case "init":
32259
+ return handleInit(rest);
32260
+ case "references":
32261
+ return handleReferences(rest);
32262
+ default:
32263
+ return null;
32264
+ }
32265
+ }
31554
32266
  async function main() {
31555
- const argv = process.argv.slice(2);
32267
+ const argv = process7.argv.slice(2);
31556
32268
  const command = argv[0];
31557
32269
  if (!command || command === "--help" || command === "-h") {
31558
32270
  usage();
31559
- process.exit(command ? 0 : 1);
32271
+ process7.exit(command ? 0 : 1);
31560
32272
  }
31561
32273
  try {
31562
- if (command === "audit") {
31563
- const sub = argv[1];
31564
- if (sub !== "docs" && sub !== "self" && sub !== "skills") {
31565
- usage();
31566
- process.exit(1);
31567
- }
31568
- const options = parseAuditArgs(argv.slice(2));
31569
- options.suite = sub;
31570
- process.exit(await runAudit(options));
31571
- }
31572
- if (command === "build-plugin") {
31573
- const { entry, check } = parseBuildPluginArgs(argv.slice(1));
31574
- const root2 = findRepoRoot();
31575
- const result = await runBuildPlugin({ root: root2, entry, check });
31576
- if (check) {
31577
- console.log(result.checked.length === 0 ? "build-plugin --check: no plugins configured." : `build-plugin --check: ${result.checked.length} plugin(s) up to date.`);
31578
- } else {
31579
- console.log(result.built.length === 0 ? "build-plugin: no plugins configured." : `build-plugin: built ${result.built.length} plugin(s).`);
31580
- }
31581
- process.exit(0);
31582
- }
31583
- if (command === "validate" && argv[1] === "changed") {
31584
- const rest = argv.slice(2);
31585
- const paths = [];
31586
- let staged = false;
31587
- let base;
31588
- for (let i = 0;i < rest.length; i++) {
31589
- const arg = rest[i];
31590
- if (arg === "--staged")
31591
- staged = true;
31592
- else if (arg === "--base")
31593
- base = rest[++i];
31594
- else if (arg?.startsWith("--base="))
31595
- base = arg.slice("--base=".length);
31596
- else if (arg && !arg.startsWith("-"))
31597
- paths.push(arg);
31598
- }
31599
- process.exit(await runValidateChanged({ paths, staged, base }));
31600
- }
31601
- if (command === "register") {
31602
- const opts = parseRegisterArgs(argv.slice(1));
31603
- if (!opts.path) {
31604
- console.error("register: path required");
31605
- process.exit(1);
31606
- }
31607
- registerPath({
31608
- path: opts.path,
31609
- topic: opts.topic,
31610
- dryRun: opts.dryRun,
31611
- json: opts.json
31612
- });
31613
- process.exit(0);
31614
- }
31615
- if (command === "customize" && argv[1] === "resolve") {
31616
- const slug2 = argv[2];
31617
- const json = argv.includes("--json");
31618
- if (!slug2) {
31619
- console.error("customize resolve: slug required");
31620
- process.exit(1);
31621
- }
31622
- const result = resolveCustomizeFromRoot(slug2);
31623
- if (json) {
31624
- console.log(JSON.stringify(result, null, 2));
31625
- } else if (result.content) {
31626
- process.stdout.write(result.content);
31627
- }
31628
- process.exit(0);
31629
- }
31630
- if (command === "hook") {
31631
- if (argv[1] !== "customize") {
31632
- usage();
31633
- process.exit(1);
31634
- }
31635
- process.stdout.write(runCustomizeHook(readFileSync22(0, "utf8")));
31636
- process.exit(0);
31637
- }
31638
- if (command === "init") {
31639
- const parsed = parseInitArgs(argv.slice(1));
31640
- runInit(parsed);
31641
- process.exit(0);
31642
- }
31643
- if (command === "references") {
31644
- const sub = argv[1];
31645
- if (sub === "sync") {
31646
- const dryRun = argv.includes("--dry-run");
31647
- const rewriteLinks = !argv.includes("--no-rewrite-links");
31648
- const result = runReferencesSync({ dryRun, rewriteLinks });
31649
- printSyncResult(result);
31650
- process.exit(0);
31651
- }
31652
- if (sub === "check") {
31653
- process.exit(runReferencesCheck({
31654
- json: argv.includes("--json"),
31655
- strict: argv.includes("--strict")
31656
- }));
31657
- }
32274
+ const exitCode = await dispatchCommand(argv);
32275
+ if (exitCode === null) {
31658
32276
  usage();
31659
- process.exit(1);
32277
+ process7.exit(1);
31660
32278
  }
31661
- usage();
31662
- process.exit(1);
32279
+ process7.exit(exitCode);
31663
32280
  } catch (error) {
31664
32281
  console.error(String(error));
31665
- process.exit(1);
32282
+ process7.exit(1);
31666
32283
  }
31667
32284
  }
31668
32285
  main();