@csark0812/skeleton 1.5.5 → 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) {
@@ -17313,23 +17500,10 @@ function expandPatterns(root, patterns, exclude) {
17313
17500
  cwd: root,
17314
17501
  absolute: true,
17315
17502
  onlyFiles: true,
17316
- dot: true
17503
+ dot: true,
17504
+ ignore: exclude
17317
17505
  })) {
17318
- if (!isMarkdownFile(abs))
17319
- continue;
17320
- const rel = normalizeRelPath(relative4(root, abs));
17321
- if (shouldExclude(rel, exclude))
17322
- continue;
17323
- let real;
17324
- try {
17325
- real = realpathSync4(abs);
17326
- } catch {
17327
- real = abs;
17328
- }
17329
- const existing = byReal.get(real);
17330
- if (existing === undefined || abs === real && existing !== real) {
17331
- byReal.set(real, abs);
17332
- }
17506
+ rememberMarkdownFile({ byReal, root, abs, exclude });
17333
17507
  }
17334
17508
  }
17335
17509
  return [...byReal.values()];
@@ -17358,7 +17532,8 @@ function collectBannedFiles(config, root) {
17358
17532
  cwd: root,
17359
17533
  absolute: true,
17360
17534
  onlyFiles: true,
17361
- dot: false
17535
+ dot: false,
17536
+ ignore: exclude
17362
17537
  })) {
17363
17538
  const rel = normalizeRelPath(relative4(root, abs));
17364
17539
  if (shouldExclude(rel, exclude))
@@ -17375,7 +17550,8 @@ function collectCoverageCandidateFiles(root, exclude) {
17375
17550
  cwd: root,
17376
17551
  absolute: true,
17377
17552
  onlyFiles: true,
17378
- dot: false
17553
+ dot: false,
17554
+ ignore: exclude
17379
17555
  })) {
17380
17556
  const rel = normalizeRelPath(relative4(root, abs));
17381
17557
  if (shouldExclude(rel, exclude))
@@ -17390,31 +17566,41 @@ function excludeForeignSkillDocMetaPaths(docMetaPaths, skillIndex) {
17390
17566
  return docMetaPaths;
17391
17567
  return docMetaPaths.filter((rel) => !isForeignSkillPath(rel, skillIndex));
17392
17568
  }
17393
- function collectDocMetaPaths(config, root, registryPaths, skillIndex) {
17569
+ function collectRegistryDocMeta(ctx) {
17394
17570
  const paths = [];
17395
- for (const abs of expandPatterns(root, ["docs/*/README.md"], mergedExcludes(config))) {
17396
- paths.push(normalizeRelPath(relative4(root, abs)));
17397
- }
17398
- const extras = ["docs/README.md", ".skeleton/registry.md"];
17399
- for (const file of extras) {
17400
- const abs = join6(root, file);
17401
- if (existsSync7(abs))
17402
- paths.push(normalizeRelPath(file));
17403
- }
17404
- for (const rel of registryPaths) {
17405
- if (!rel.endsWith(".md") && !rel.endsWith(".mdc"))
17571
+ for (const rel of ctx.registryPaths) {
17572
+ if (!(rel.endsWith(".md") || rel.endsWith(".mdc")))
17406
17573
  continue;
17407
- const abs = join6(root, rel);
17574
+ const abs = join6(ctx.root, rel);
17408
17575
  if (existsSync7(abs))
17409
17576
  paths.push(normalizeRelPath(rel));
17410
17577
  }
17411
- 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)) {
17412
17583
  const content = readFileSync5(abs, "utf8");
17413
17584
  if (/<!--\s*doc-meta:/.test(content)) {
17414
- paths.push(normalizeRelPath(relative4(root, abs)));
17585
+ paths.push(normalizeRelPath(relative4(ctx.root, abs)));
17415
17586
  }
17416
17587
  }
17417
- 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);
17418
17604
  }
17419
17605
  function validateScanRoots(config, root) {
17420
17606
  const missing = [];
@@ -17437,31 +17623,34 @@ function filterToPaths(files, paths, root) {
17437
17623
  return normalizedPaths.some((path) => rel === path || rel.startsWith(`${path}/`));
17438
17624
  });
17439
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
+ }
17440
17650
  function includeExplicitMarkdownPaths(files, paths, root) {
17441
17651
  const out = new Set(files);
17442
17652
  for (const raw of paths) {
17443
- const rel = normalizeRelPath(raw);
17444
- const abs = join6(root, rel);
17445
- if (!existsSync7(abs))
17446
- continue;
17447
- if (isMarkdownFile(rel)) {
17448
- out.add(abs);
17449
- continue;
17450
- }
17451
- try {
17452
- if (!statSync2(abs).isDirectory())
17453
- continue;
17454
- } catch {
17455
- continue;
17456
- }
17457
- for (const md of globSync("**/*.{md,mdc}", {
17458
- cwd: abs,
17459
- absolute: true,
17460
- onlyFiles: true,
17461
- dot: true
17462
- })) {
17463
- out.add(md);
17464
- }
17653
+ addExplicitPath(out, root, raw);
17465
17654
  }
17466
17655
  return [...out];
17467
17656
  }
@@ -17498,9 +17687,6 @@ function parseRegistry(root) {
17498
17687
  }
17499
17688
  return { paths: [...new Set(paths)], hasTableHeader };
17500
17689
  }
17501
- function parseRegistryPaths(root) {
17502
- return parseRegistry(root).paths;
17503
- }
17504
17690
 
17505
17691
  // src/audit/core/context.ts
17506
17692
  function createContext(options = {}) {
@@ -17516,7 +17702,12 @@ function createContext(options = {}) {
17516
17702
  files = filterToPaths(files, options.paths, root);
17517
17703
  }
17518
17704
  const registry = parseRegistry(root);
17519
- const allDocMetaPaths = collectDocMetaPaths(config, root, registry.paths, skillIndex);
17705
+ const allDocMetaPaths = collectDocMetaPaths({
17706
+ config,
17707
+ root,
17708
+ registryPaths: registry.paths,
17709
+ skillIndex
17710
+ });
17520
17711
  return {
17521
17712
  root,
17522
17713
  config,
@@ -21788,9 +21979,9 @@ function factoryTitle(effects, ok2, nok, type, markerType, stringType) {
21788
21979
  return atBreak(code);
21789
21980
  }
21790
21981
  effects.consume(code);
21791
- return code === codes.backslash ? escape2 : inside;
21982
+ return code === codes.backslash ? escape : inside;
21792
21983
  }
21793
- function escape2(code) {
21984
+ function escape(code) {
21794
21985
  if (code === marker || code === codes.backslash) {
21795
21986
  effects.consume(code);
21796
21987
  return inside;
@@ -28817,174 +29008,213 @@ function stripYamlFrontmatter(content3) {
28817
29008
  return "";
28818
29009
  return content3;
28819
29010
  }
28820
- function destinationConsumesToSliceEnd(slice, afterDest) {
28821
- let i = afterDest;
28822
- if (i >= slice.length)
28823
- return false;
28824
- if (slice[i] === ")") {
28825
- return i === slice.length - 1;
28826
- }
28827
- if (!/\s/.test(slice[i]))
28828
- return false;
28829
- while (i < slice.length && /\s/.test(slice[i]))
29011
+ function skipWhitespace(slice, start) {
29012
+ let i = start;
29013
+ while (i < slice.length) {
29014
+ const ch = slice[i];
29015
+ if (ch === undefined || !/\s/.test(ch))
29016
+ break;
28830
29017
  i++;
28831
- if (i >= slice.length)
28832
- return false;
28833
- if (slice[i] === ")") {
28834
- return i === slice.length - 1;
28835
29018
  }
28836
- 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];
28837
29025
  if (open !== '"' && open !== "'" && open !== "(")
28838
- return false;
29026
+ return start;
28839
29027
  const close = open === "(" ? ")" : open;
28840
- i++;
29028
+ let i = start + 1;
28841
29029
  while (i < slice.length && slice[i] !== close)
28842
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);
28843
29042
  if (i >= slice.length)
28844
29043
  return false;
28845
- i++;
28846
- while (i < slice.length && /\s/.test(slice[i]))
28847
- i++;
29044
+ if (slice[i] === ")")
29045
+ return i === slice.length - 1;
29046
+ i = skipTitle(slice, i);
29047
+ i = skipWhitespace(slice, i);
28848
29048
  return i === slice.length - 1 && slice[i] === ")";
28849
29049
  }
28850
- function findUrlSpanInSlice(content3, nodeStart, nodeEnd, url) {
28851
- 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);
28852
29076
  let searchFrom = 0;
28853
- while (searchFrom < slice.length) {
28854
- const openParen = slice.indexOf("](", searchFrom);
29077
+ while (searchFrom < nodeSlice.length) {
29078
+ const openParen = nodeSlice.indexOf("](", searchFrom);
28855
29079
  if (openParen === -1)
28856
29080
  break;
28857
- const after = openParen + 2;
28858
- if (slice.startsWith(`<${url}>`, after)) {
28859
- const afterDest = after + 2 + url.length;
28860
- if (destinationConsumesToSliceEnd(slice, afterDest)) {
28861
- const urlStart = nodeStart + after + 1;
28862
- return { urlStart, urlEnd: urlStart + url.length };
28863
- }
28864
- } else if (slice.startsWith(url, after)) {
28865
- const next = slice[after + url.length];
28866
- if (next === ")" || next !== undefined && /\s/.test(next)) {
28867
- if (destinationConsumesToSliceEnd(slice, after + url.length)) {
28868
- const urlStart = nodeStart + after;
28869
- return { urlStart, urlEnd: urlStart + url.length };
28870
- }
28871
- }
28872
- }
29081
+ const span = tryLinkDestination({ content: nodeSlice, nodeStart: slice.nodeStart, nodeEnd: slice.nodeEnd, url: slice.url }, openParen);
29082
+ if (span)
29083
+ return span;
28873
29084
  searchFrom = openParen + 1;
28874
29085
  }
28875
- const auto = `<${url}>`;
28876
- if (slice === auto) {
28877
- 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 };
28878
29092
  }
28879
- if (slice === url) {
28880
- return { urlStart: nodeStart, urlEnd: nodeEnd };
29093
+ if (nodeSlice === slice.url) {
29094
+ return { urlStart: slice.nodeStart, urlEnd: slice.nodeEnd };
28881
29095
  }
28882
- const trimmed = slice.trim();
29096
+ const trimmed = nodeSlice.trim();
28883
29097
  if (trimmed === auto) {
28884
- const lead = slice.indexOf(auto);
29098
+ const lead = nodeSlice.indexOf(auto);
28885
29099
  if (lead !== -1) {
28886
- 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
+ };
28887
29104
  }
28888
29105
  }
28889
- if (trimmed === url) {
28890
- const lead = slice.indexOf(url);
29106
+ if (trimmed === slice.url) {
29107
+ const lead = nodeSlice.indexOf(slice.url);
28891
29108
  if (lead !== -1) {
28892
- 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
+ };
28893
29113
  }
28894
29114
  }
28895
- return;
28896
29115
  }
28897
- function findUrlInDefinitionSlice(content3, nodeStart, nodeEnd, url) {
28898
- const slice = content3.slice(nodeStart, nodeEnd);
28899
- const labelEnd2 = slice.indexOf("]:");
29116
+ function findUrlInDefinitionSlice(slice) {
29117
+ const nodeSlice = slice.content.slice(slice.nodeStart, slice.nodeEnd);
29118
+ const labelEnd2 = nodeSlice.indexOf("]:");
28900
29119
  if (labelEnd2 === -1)
28901
29120
  return;
28902
29121
  let i = labelEnd2 + 2;
28903
- while (i < slice.length && /\s/.test(slice[i] ?? ""))
29122
+ while (i < nodeSlice.length && /\s/.test(nodeSlice[i] ?? ""))
28904
29123
  i++;
28905
- if (slice.startsWith(`<${url}>`, i)) {
28906
- const urlStart = nodeStart + i + 1;
28907
- 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 };
28908
29127
  }
28909
- if (slice.startsWith(url, i)) {
28910
- const urlStart = nodeStart + i;
28911
- 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 };
28912
29131
  }
28913
- return;
28914
29132
  }
28915
- function collectReferenceDefinitions(content3, tree) {
28916
- const defs = new Map;
28917
- visit2(tree, (node2) => {
28918
- if (node2.type !== "definition")
28919
- return;
28920
- if (!("identifier" in node2) || !("url" in node2))
28921
- return;
28922
- const id = String(node2.identifier).toLowerCase();
28923
- if (defs.has(id))
28924
- return;
28925
- const url = typeof node2.url === "string" ? node2.url : "";
28926
- if (!url)
28927
- return;
28928
- const start = node2.position?.start.offset;
28929
- const end = node2.position?.end.offset;
28930
- if (start === undefined || end === undefined)
28931
- return;
28932
- const span = findUrlInDefinitionSlice(content3, start, end, url);
28933
- if (!span)
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
+ });
29155
+ }
29156
+ function collectReferenceDefinitions(content3, tree) {
29157
+ const defs = new Map;
29158
+ visit2(tree, (node2) => {
29159
+ if (node2.type !== "definition")
28934
29160
  return;
28935
- defs.set(id, {
28936
- url,
28937
- urlStart: span.urlStart,
28938
- urlEnd: span.urlEnd,
28939
- line: lineFromOffset(content3, start) ?? 1
28940
- });
29161
+ definitionFromNode(content3, node2, defs);
28941
29162
  });
28942
29163
  return defs;
28943
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
+ }
28944
29188
  function extractLinksFromMarkdown(content3, _filePath) {
28945
29189
  const tree = processor.parse(content3);
28946
29190
  const refDefs = collectReferenceDefinitions(content3, tree);
28947
29191
  const links = [];
28948
29192
  visit2(tree, (node2) => {
28949
29193
  if (node2.type === "link" && "url" in node2 && typeof node2.url === "string") {
28950
- const target = node2.url.trim();
28951
- const start = node2.position?.start.offset;
28952
- const end = node2.position?.end.offset;
28953
- const span = start !== undefined && end !== undefined ? findUrlSpanInSlice(content3, start, end, target) : undefined;
28954
- links.push({
28955
- target,
28956
- line: lineFromOffset(content3, node2.position?.start.offset),
28957
- urlStart: span?.urlStart,
28958
- urlEnd: span?.urlEnd
28959
- });
29194
+ links.push(linkFromDirectNode(content3, node2));
28960
29195
  }
28961
29196
  if (node2.type === "linkReference" && "identifier" in node2) {
28962
- const id = String(node2.identifier).toLowerCase();
28963
- const def = refDefs.get(id);
28964
- if (def) {
28965
- links.push({
28966
- target: def.url.trim(),
28967
- line: def.line,
28968
- urlStart: def.urlStart,
28969
- urlEnd: def.urlEnd
28970
- });
28971
- }
29197
+ const refLink = linkFromReference(String(node2.identifier).toLowerCase(), refDefs);
29198
+ if (refLink)
29199
+ links.push(refLink);
28972
29200
  }
28973
29201
  });
28974
29202
  return links;
28975
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
+ }
28976
29210
  function phrasingText(nodes) {
28977
29211
  if (!nodes?.length)
28978
29212
  return "";
28979
29213
  let out = "";
28980
29214
  for (const node2 of nodes) {
28981
- if (node2.type === "text" || node2.type === "inlineCode") {
28982
- out += "value" in node2 && node2.value !== undefined ? String(node2.value) : "";
28983
- continue;
28984
- }
28985
- if (node2.children?.length) {
29215
+ out += textFromPhrasingNode(node2);
29216
+ if (node2.children?.length)
28986
29217
  out += phrasingText(node2.children);
28987
- }
28988
29218
  }
28989
29219
  return out;
28990
29220
  }
@@ -29074,60 +29304,86 @@ function replaceAnchorInTarget(target, oldAnchor, newAnchor) {
29074
29304
  return target;
29075
29305
  return `${pathPart}#${newAnchor}${queryPart}`;
29076
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
+ }
29077
29381
  function collectAnchorFixes(ctx) {
29078
29382
  const editsByFile = new Map;
29079
29383
  for (const filePath of ctx.files) {
29080
- const content3 = readFileContent(filePath);
29081
- const links = extractLinksFromMarkdown(content3, filePath);
29082
- const pending = [];
29083
- const relFile = relPath(filePath, ctx.root);
29084
- for (const { target, line, urlStart, urlEnd } of links) {
29085
- if (isExternalLink(target) && !target.startsWith("#"))
29086
- continue;
29087
- if (isPlaceholderLink(target))
29088
- continue;
29089
- const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
29090
- if (!anchor)
29091
- continue;
29092
- const resolved = resolveLink(filePath, target);
29093
- if (!existsSync9(resolved))
29094
- continue;
29095
- const targetContent = readFileSync7(resolved, "utf8");
29096
- const slugs = extractHeadingSlugs(targetContent, resolved);
29097
- const anchorSlug = slugifyAnchor(anchor);
29098
- if (slugs.has(anchorSlug))
29099
- continue;
29100
- const match = findBestAnchorMatch(anchorSlug, slugs);
29101
- if (!match)
29102
- continue;
29103
- const nextTarget = replaceAnchorInTarget(target, anchor, match.slug);
29104
- if (nextTarget === target)
29105
- continue;
29106
- const lineLabel = line ? `${relFile}:${line}` : relFile;
29107
- const description = `${lineLabel} #${anchor} → #${match.slug} (score ${match.score.toFixed(2)})`;
29108
- if (urlStart !== undefined && urlEnd !== undefined && content3.slice(urlStart, urlEnd) === target) {
29109
- pending.push({ urlStart, urlEnd, from: target, to: nextTarget, description });
29110
- }
29111
- }
29112
- if (pending.length === 0)
29113
- continue;
29114
- const uniqueBySpan = new Map;
29115
- for (const edit of pending) {
29116
- uniqueBySpan.set(`${edit.urlStart}:${edit.urlEnd}:${edit.from}`, edit);
29117
- }
29118
- const uniquePending = [...uniqueBySpan.values()];
29119
- uniquePending.sort((a, b) => b.urlStart - a.urlStart);
29120
- let updated = content3;
29121
- const descriptions = [];
29122
- for (const edit of uniquePending) {
29123
- if (updated.slice(edit.urlStart, edit.urlEnd) !== edit.from)
29124
- continue;
29125
- updated = updated.slice(0, edit.urlStart) + edit.to + updated.slice(edit.urlEnd);
29126
- descriptions.push(edit.description);
29127
- }
29128
- if (updated === content3 || descriptions.length === 0)
29129
- continue;
29130
- editsByFile.set(filePath, { content: updated, descriptions });
29384
+ const result = collectFileAnchorFixes(ctx, filePath);
29385
+ if (result)
29386
+ editsByFile.set(filePath, result);
29131
29387
  }
29132
29388
  const edits = [];
29133
29389
  for (const [absPath, { content: content3, descriptions }] of editsByFile) {
@@ -29170,32 +29426,37 @@ function bumpDocMetaLastReviewed(content3, gitDate) {
29170
29426
  return null;
29171
29427
  return replaceDocMetaLastReviewed(content3, gitDate);
29172
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
+ }
29173
29454
  function collectDocMetaFixes(ctx) {
29174
29455
  const edits = [];
29175
29456
  for (const relPath2 of ctx.docMetaPaths) {
29176
- const abs = join9(ctx.root, relPath2);
29177
- if (!existsSync10(abs))
29178
- continue;
29179
- const content3 = readFileSync8(abs, "utf8");
29180
- if (!DOC_META_RE.test(content3))
29181
- continue;
29182
- const reviewedStr = docMetaLastReviewed(content3);
29183
- if (!reviewedStr)
29184
- continue;
29185
- const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
29186
- if (Number.isNaN(reviewed.getTime()))
29187
- continue;
29188
- const gitDate = lastGitCommitDate(relPath2, ctx.root);
29189
- if (!gitDate)
29190
- continue;
29191
- const updated = bumpDocMetaLastReviewed(content3, gitDate);
29192
- if (!updated)
29193
- continue;
29194
- edits.push({
29195
- file: relPath2,
29196
- description: `last-reviewed ${reviewedStr} → ${gitDate}`,
29197
- content: updated
29198
- });
29457
+ const fix = docMetaFixForPath(ctx, relPath2);
29458
+ if (fix)
29459
+ edits.push(fix);
29199
29460
  }
29200
29461
  return edits;
29201
29462
  }
@@ -29237,28 +29498,31 @@ function overlayLastReviewed(targetContent, metaContent) {
29237
29498
  function underRoot(rootAbs, candidateAbs) {
29238
29499
  return candidateAbs === rootAbs || candidateAbs.startsWith(rootAbs + sep3);
29239
29500
  }
29240
- function resolveWritePath(root2, relFile) {
29241
- const rootResolved = resolve6(root2);
29242
- const abs = resolve6(rootResolved, relFile);
29243
- if (!underRoot(rootResolved, abs)) {
29244
- throw new Error(`Refusing autofix outside repo root: ${relFile}`);
29245
- }
29501
+ function shouldStopPathWalk(rootResolved, parent, cursor) {
29502
+ return parent === cursor || !underRoot(rootResolved, parent) && parent !== rootResolved;
29503
+ }
29504
+ function resolveExistingRealPath(rootResolved, abs, relFile) {
29246
29505
  const rootReal = existsSync11(rootResolved) ? realpathSync5(rootResolved) : rootResolved;
29247
29506
  let cursor = abs;
29248
- while (true) {
29249
- if (existsSync11(cursor)) {
29250
- const real = realpathSync5(cursor);
29251
- if (!underRoot(rootReal, real)) {
29252
- throw new Error(`Refusing autofix outside repo root: ${relFile}`);
29253
- }
29254
- return abs;
29255
- }
29507
+ while (!existsSync11(cursor)) {
29256
29508
  const parent = dirname6(cursor);
29257
- if (parent === cursor || !underRoot(rootResolved, parent) && parent !== rootResolved) {
29509
+ if (shouldStopPathWalk(rootResolved, parent, cursor))
29258
29510
  return abs;
29259
- }
29260
29511
  cursor = parent;
29261
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);
29262
29526
  }
29263
29527
  function applyFixes(ctx, options) {
29264
29528
  const kinds = new Set(options.kinds);
@@ -29302,13 +29566,14 @@ function fixKindsForOnly(kinds, only) {
29302
29566
  }
29303
29567
 
29304
29568
  // src/audit/core/report.ts
29305
- function issue(rule, file, message, opts) {
29569
+ function issue(rule, file, details) {
29570
+ const message = typeof details === "string" ? details : details.message;
29306
29571
  return {
29307
29572
  rule,
29308
29573
  file,
29309
- link: opts?.link,
29574
+ link: typeof details === "string" ? undefined : details.link,
29310
29575
  message,
29311
- severity: opts?.severity ?? "error"
29576
+ severity: typeof details === "string" ? "error" : details.severity ?? "error"
29312
29577
  };
29313
29578
  }
29314
29579
  function finalizeIssues(issues, strict) {
@@ -29321,31 +29586,39 @@ function printReport(issues, options) {
29321
29586
  const errors2 = finalized.filter((i) => i.severity === "error");
29322
29587
  const warnings = finalized.filter((i) => i.severity === "warning");
29323
29588
  const label = options.label ?? "Audit";
29324
- if (options.json) {
29325
- console.log(JSON.stringify({
29326
- label,
29327
- fileCount: options.fileCount,
29328
- errors: errors2.length,
29329
- warnings: warnings.length,
29330
- issues: finalized
29331
- }, null, 2));
29332
- return errors2.length > 0 ? 1 : 0;
29333
- }
29334
- if (warnings.length > 0) {
29335
- 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:
29336
29608
  `);
29337
- for (const i of warnings) {
29338
- const linkPart = i.link ? ` (${i.link})` : "";
29339
- console.log(`- ${i.file}${linkPart}: ${i.message}`);
29340
- }
29341
- console.log("");
29342
- }
29343
- if (errors2.length === 0) {
29344
- const warnNote = warnings.length > 0 ? `, ${warnings.length} warning(s)` : "";
29345
- const countNote = options.successSuffix ?? (options.fileCount !== undefined ? ` (${options.fileCount} files scanned${warnNote})` : "");
29346
- console.log(`${label} passed${countNote}.`);
29347
- return 0;
29609
+ for (const i of warnings) {
29610
+ const linkPart = i.link ? ` (${i.link})` : "";
29611
+ console.log(`- ${i.file}${linkPart}: ${i.message}`);
29348
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) {
29349
29622
  console.log(`${label} failed:
29350
29623
  `);
29351
29624
  for (const i of errors2) {
@@ -29354,6 +29627,13 @@ function printReport(issues, options) {
29354
29627
  }
29355
29628
  return 1;
29356
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
+ }
29357
29637
 
29358
29638
  // src/references/check.ts
29359
29639
  import { existsSync as existsSync13, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
@@ -29413,73 +29693,86 @@ function findSharedRefLinks(content3, sourceFile) {
29413
29693
  }
29414
29694
  return links;
29415
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
+ }
29416
29715
  function findLocalCanonicalLinks(root2, content3, sourceFile) {
29417
29716
  const links = [];
29418
29717
  const localRefRe = /\((?:\.\/)?references\/([^)]+)\)/g;
29419
29718
  for (const match of content3.matchAll(localRefRe)) {
29420
29719
  const refPath = normalizeRelPath(match[1] ?? "");
29421
- if (!refPath || !canonicalExists(root2, refPath))
29720
+ if (!(refPath && canonicalExists(root2, refPath)))
29422
29721
  continue;
29423
29722
  links.push({ refPath, sourceFile });
29424
29723
  }
29425
- const inReferencesDir = /\/references\//.test(sourceFile);
29426
- if (inReferencesDir) {
29427
- const refsIdx = sourceFile.lastIndexOf("/references/");
29428
- const withinRefs = sourceFile.slice(refsIdx + "/references/".length);
29429
- const withinDir = withinRefs.includes("/") ? withinRefs.slice(0, withinRefs.lastIndexOf("/")) : "";
29430
- const siblingRe = /\((?!https?:|#|\.\.\/)([a-z0-9./_-]+\.md)\)/gi;
29431
- for (const match of content3.matchAll(siblingRe)) {
29432
- const raw = normalizeRelPath(match[1] ?? "");
29433
- if (!raw)
29434
- continue;
29435
- const refPath = withinDir ? normalizeRelPath(join10(withinDir, raw)) : raw;
29436
- 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))
29437
29747
  continue;
29438
- links.push({ refPath, sourceFile });
29748
+ refPaths.add(link2.refPath);
29749
+ links.push(link2);
29750
+ queue.push(link2.refPath);
29439
29751
  }
29440
29752
  }
29441
- 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;
29442
29768
  }
29443
29769
  function discoverSkillReferencePlans(root2, ownership) {
29444
29770
  const index2 = buildSkillIndex(root2, ownership);
29445
29771
  const plans = [];
29446
29772
  for (const slug2 of index2.ownedSlugs) {
29447
- const skillDir = join10(root2, slug2);
29448
- if (!existsSync12(join10(skillDir, "SKILL.md")))
29449
- continue;
29450
- const refPaths = new Set;
29451
- const links = [];
29452
- for (const relFile of walkMarkdownFiles(skillDir, root2)) {
29453
- const content3 = readFileSync9(join10(root2, relFile), "utf8");
29454
- if (isGeneratedReference(content3))
29455
- continue;
29456
- for (const link2 of findSharedRefLinks(content3, relFile)) {
29457
- refPaths.add(link2.refPath);
29458
- links.push(link2);
29459
- }
29460
- for (const link2 of findLocalCanonicalLinks(root2, content3, relFile)) {
29461
- refPaths.add(link2.refPath);
29462
- links.push(link2);
29463
- }
29464
- }
29465
- const queue = [...refPaths];
29466
- while (queue.length > 0) {
29467
- const refPath = queue.pop();
29468
- if (!refPath || !canonicalExists(root2, refPath))
29469
- continue;
29470
- const canonicalContent = readFileSync9(join10(root2, CANONICAL_REFS_DIR, refPath), "utf8");
29471
- const syntheticSource = generatedRefPath(slug2, refPath);
29472
- for (const link2 of findLocalCanonicalLinks(root2, canonicalContent, syntheticSource)) {
29473
- if (refPaths.has(link2.refPath))
29474
- continue;
29475
- refPaths.add(link2.refPath);
29476
- links.push(link2);
29477
- queue.push(link2.refPath);
29478
- }
29479
- }
29480
- if (refPaths.size > 0) {
29481
- plans.push({ skill: slug2, refPaths, links });
29482
- }
29773
+ const plan = planForSkill(root2, slug2);
29774
+ if (plan)
29775
+ plans.push(plan);
29483
29776
  }
29484
29777
  return plans.sort((a, b) => a.skill.localeCompare(b.skill));
29485
29778
  }
@@ -29510,30 +29803,75 @@ function rewriteSharedRefLinks(content3, sourceFile, skill) {
29510
29803
  }
29511
29804
 
29512
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
+ }
29513
29822
  function listAllGeneratedFiles(root2) {
29514
29823
  const files = [];
29515
- const walk = (dir) => {
29516
- if (!existsSync13(dir))
29517
- return;
29518
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
29519
- if (entry.name.startsWith("."))
29520
- continue;
29521
- const fullPath = join11(dir, entry.name);
29522
- if (entry.isDirectory()) {
29523
- walk(fullPath);
29524
- continue;
29525
- }
29526
- if (!entry.name.endsWith(".md"))
29527
- continue;
29528
- const content3 = readFileSync10(fullPath, "utf8");
29529
- if (isGeneratedReference(content3)) {
29530
- files.push(normalizeRelPath(relative7(root2, fullPath)));
29531
- }
29824
+ walkMarkdown(root2, (fullPath) => {
29825
+ const content3 = readFileSync10(fullPath, "utf8");
29826
+ if (isGeneratedReference(content3)) {
29827
+ files.push(normalizeRelPath(relative7(root2, fullPath)));
29532
29828
  }
29533
- };
29534
- walk(root2);
29829
+ });
29535
29830
  return files;
29536
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
+ }
29537
29875
  function runGeneratedReferencesCheck(root2, ownership) {
29538
29876
  const issues = [];
29539
29877
  const canonicalDir = join11(root2, CANONICAL_REFS_DIR);
@@ -29548,58 +29886,13 @@ function runGeneratedReferencesCheck(root2, ownership) {
29548
29886
  }
29549
29887
  }
29550
29888
  for (const targetRel of needed) {
29551
- const targetPath = join11(root2, targetRel);
29552
- if (!existsSync13(targetPath)) {
29553
- issues.push(issue("generated-references", targetRel, "missing generated copy — run skeleton references sync"));
29554
- continue;
29555
- }
29556
- const generated = readFileSync10(targetPath, "utf8");
29557
- if (!isGeneratedReference(generated)) {
29558
- issues.push(issue("generated-references", targetRel, "expected generated-reference provenance header"));
29559
- continue;
29560
- }
29561
- const body = stripGeneratedHeader(generated);
29562
- const sourceRel = normalizeRelPath(generated.match(/source: ([^\n]+)/)?.[1] ?? join11(CANONICAL_REFS_DIR, targetRel.split("/references/")[1] ?? ""));
29563
- const canonicalPath = join11(root2, sourceRel);
29564
- if (!existsSync13(canonicalPath)) {
29565
- issues.push(issue("generated-references", targetRel, `canonical source missing: ${sourceRel}`));
29566
- continue;
29567
- }
29568
- const canonical = readFileSync10(canonicalPath, "utf8");
29569
- if (body !== canonical) {
29570
- issues.push(issue("generated-references", targetRel, "stale generated copy — run skeleton references sync"));
29571
- }
29572
- }
29573
- for (const generatedRel of listAllGeneratedFiles(root2)) {
29574
- if (isForeignSkillPath(generatedRel, skillIndex))
29575
- continue;
29576
- if (!needed.has(generatedRel)) {
29577
- issues.push(issue("generated-references", generatedRel, "orphaned generated copy — run skeleton references sync"));
29578
- }
29889
+ const found = checkNeededCopy(root2, targetRel);
29890
+ if (found)
29891
+ issues.push(found);
29579
29892
  }
29893
+ issues.push(...checkOrphanedCopies(root2, needed, skillIndex));
29580
29894
  for (const plan of plans) {
29581
- const skillDir = join11(root2, plan.skill);
29582
- if (!existsSync13(skillDir))
29583
- continue;
29584
- const walk = (dir) => {
29585
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
29586
- if (entry.name.startsWith("."))
29587
- continue;
29588
- const fullPath = join11(dir, entry.name);
29589
- if (entry.isDirectory()) {
29590
- walk(fullPath);
29591
- continue;
29592
- }
29593
- if (!entry.name.endsWith(".md"))
29594
- continue;
29595
- const relFile = normalizeRelPath(relative7(root2, fullPath));
29596
- const content3 = readFileSync10(fullPath, "utf8");
29597
- if (content3.match(SHARED_REF_LINK_RE)) {
29598
- issues.push(issue("generated-references", relFile, "still links to shared root references/ — run skeleton references sync"));
29599
- }
29600
- }
29601
- };
29602
- walk(skillDir);
29895
+ issues.push(...checkStaleSharedLinks(root2, join11(root2, plan.skill)));
29603
29896
  }
29604
29897
  return issues;
29605
29898
  }
@@ -29626,6 +29919,51 @@ var bannedRule = { id: "banned", run: runBannedRule };
29626
29919
  // src/audit/rules/doc-meta.ts
29627
29920
  import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
29628
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
+ }
29629
29967
  function runDocMetaRule(ctx) {
29630
29968
  const issues = [];
29631
29969
  const today = new Date;
@@ -29634,32 +29972,27 @@ function runDocMetaRule(ctx) {
29634
29972
  if (!existsSync14(abs))
29635
29973
  continue;
29636
29974
  const content3 = readFileSync11(abs, "utf8");
29637
- if (!DOC_META_RE.test(content3)) {
29638
- issues.push(issue("doc-meta", relPath2, "missing doc-meta comment (owner + last-reviewed)"));
29639
- continue;
29640
- }
29641
- const reviewedStr = docMetaLastReviewed(content3);
29642
- if (!reviewedStr)
29975
+ const banner = checkDocMetaBanner(relPath2, content3);
29976
+ if (banner) {
29977
+ issues.push(banner);
29643
29978
  continue;
29644
- const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
29645
- if (Number.isNaN(reviewed.getTime()))
29646
- continue;
29647
- const ageDays = (today.getTime() - reviewed.getTime()) / 86400000;
29648
- if (ageDays > ctx.config.daysUntilStale) {
29649
- issues.push(issue("doc-meta", relPath2, `doc-meta last-reviewed ${reviewedStr} is stale (>${ctx.config.daysUntilStale} days)`, { severity: "warning" }));
29650
- }
29651
- const slug2 = slugFromPath(relPath2, ctx.root);
29652
- if (slug2 !== null && ctx.lockedSkillSlugs.has(slug2))
29653
- continue;
29654
- const gitDate = lastGitCommitDate(relPath2, ctx.root);
29655
- if (!gitDate)
29656
- continue;
29657
- const committed = new Date(`${gitDate}T00:00:00Z`);
29658
- if (Number.isNaN(committed.getTime()))
29659
- continue;
29660
- if (committed.getTime() > reviewed.getTime()) {
29661
- issues.push(issue("doc-meta", relPath2, `content changed after last-reviewed ${reviewedStr} (git: ${gitDate}) — bump last-reviewed or confirm review`, { severity: "warning" }));
29662
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);
29663
29996
  }
29664
29997
  return issues;
29665
29998
  }
@@ -29674,57 +30007,88 @@ function resolveLink2(sourceFile, target) {
29674
30007
  return sourceFile;
29675
30008
  return resolve7(dirname7(sourceFile), withoutAnchor);
29676
30009
  }
29677
- function validateTarget(ctx, sourceFile, target, linkLabel) {
29678
- const issues = [];
29679
- if (isExternalLink(target) && !target.startsWith("#"))
29680
- return issues;
29681
- if (isPlaceholderLink(target))
29682
- return issues;
29683
- const relSource = relPath(sourceFile, ctx.root);
29684
- const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
29685
- const pathPart = target.split("#")[0]?.split("?")[0] ?? "";
29686
- const resolved = resolveLink2(sourceFile, target);
29687
- const relTarget = relPath(resolved, ctx.root);
29688
- const skillMatch = SKILL_LINK_IN_TARGET_RE.exec(target);
29689
- if (skillMatch?.[1] && ctx.retiredSkills.has(skillMatch[1])) {
29690
- issues.push(issue("links", relSource, `references retired skill "${skillMatch[1]}/SKILL.md"`, {
29691
- link: linkLabel
29692
- }));
29693
- return issues;
29694
- }
29695
- if (target.includes("/SKILL.md")) {
29696
- const slug2 = skillMatch?.[1];
29697
- if (slug2 && !resolveSkillPath(ctx.skillIndex, ctx.root, slug2)) {
29698
- issues.push(issue("links", relSource, `missing skill "${slug2}/SKILL.md"`, {
29699
- link: linkLabel
29700
- }));
29701
- return issues;
29702
- }
29703
- }
29704
- if ((target.includes(".claude/agents/") || target.includes(".cursor/agents/")) && target.endsWith(".md")) {
29705
- const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
29706
- if (!existsSync15(agentPath)) {
29707
- issues.push(issue("links", relSource, "missing agent file", { link: linkLabel }));
29708
- }
29709
- return issues;
29710
- }
29711
- if (pathPart && !existsSync15(resolved)) {
29712
- issues.push(issue("links", relSource, `broken link → ${relTarget}`, {
29713
- link: linkLabel
29714
- }));
29715
- return issues;
29716
- }
29717
- if (anchor && existsSync15(resolved)) {
29718
- const targetContent = readFileSync12(resolved, "utf8");
29719
- const slugs = extractHeadingSlugs(targetContent, resolved);
29720
- const anchorSlug = slugifyAnchor(anchor);
29721
- if (!slugs.has(anchorSlug)) {
29722
- issues.push(issue("links", relSource, `broken anchor → #${anchor} in ${relTarget}`, {
29723
- link: linkLabel
29724
- }));
29725
- }
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;
29726
30034
  }
29727
- 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] : [];
29728
30092
  }
29729
30093
  function runLinksRule(ctx) {
29730
30094
  const issues = [];
@@ -29733,7 +30097,7 @@ function runLinksRule(ctx) {
29733
30097
  const links = extractLinksFromMarkdown(content3, filePath);
29734
30098
  for (const { target, line } of links) {
29735
30099
  const linkLabel = line ? `line ${line}` : target;
29736
- issues.push(...validateTarget(ctx, filePath, target, linkLabel));
30100
+ issues.push(...validateTarget({ ctx, sourceFile: filePath, target, linkLabel }));
29737
30101
  }
29738
30102
  }
29739
30103
  return issues;
@@ -29741,6 +30105,44 @@ function runLinksRule(ctx) {
29741
30105
  var linksRule = { id: "links", run: runLinksRule };
29742
30106
 
29743
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
+ }
29744
30146
  function runProsePolicyRule(ctx) {
29745
30147
  if (ctx.policies.length === 0)
29746
30148
  return [];
@@ -29753,36 +30155,9 @@ function runProsePolicyRule(ctx) {
29753
30155
  `);
29754
30156
  const policies = policiesForFile(ctx.policies, rel);
29755
30157
  for (const entry of policies) {
29756
- if (entry.mode === "fingerprint")
29757
- continue;
29758
- if (!entry.regex)
30158
+ if (entry.mode === "fingerprint" || !entry.regex)
29759
30159
  continue;
29760
- if (entry.id === "draft-marker") {
29761
- for (let i = 0;i < lines.length; i++) {
29762
- if (entry.regex.test(lines[i] ?? "") && !isDraftPlacementAllowed(rel, draftPrefixes)) {
29763
- issues.push(issue("prose-policy", rel, entry.message, {
29764
- link: `line ${i + 1}`,
29765
- severity: entry.severity
29766
- }));
29767
- }
29768
- }
29769
- continue;
29770
- }
29771
- const isMultiline = entry.pattern?.includes("[\\s\\S]");
29772
- if (isMultiline) {
29773
- if (entry.regex.test(content3)) {
29774
- issues.push(issue("prose-policy", rel, entry.message, { severity: entry.severity }));
29775
- }
29776
- continue;
29777
- }
29778
- for (let i = 0;i < lines.length; i++) {
29779
- if (entry.regex.test(lines[i] ?? "")) {
29780
- issues.push(issue("prose-policy", rel, entry.message, {
29781
- link: `line ${i + 1}`,
29782
- severity: entry.severity
29783
- }));
29784
- }
29785
- }
30160
+ issues.push(...checkPolicyEntry({ rel, content: content3, lines, entry, draftPrefixes }));
29786
30161
  }
29787
30162
  }
29788
30163
  return issues;
@@ -29792,12 +30167,8 @@ var prosePolicyRule = { id: "prose-policy", run: runProsePolicyRule };
29792
30167
  // src/audit/rules/registry.ts
29793
30168
  import { existsSync as existsSync16, readFileSync as readFileSync13 } from "node:fs";
29794
30169
  import { join as join13 } from "node:path";
29795
- function runRegistryRule(ctx) {
30170
+ function checkRegistryEntries(ctx) {
29796
30171
  const issues = [];
29797
- const registry = new Set(ctx.registryPaths);
29798
- if (ctx.registryHasTableHeader && ctx.registryPaths.length === 0) {
29799
- issues.push(issue("registry", REGISTRY_REL_PATH, "registry table header found but 0 rows parsed — check | Topic | Canonical file | format and link syntax"));
29800
- }
29801
30172
  for (const rel of ctx.registryPaths) {
29802
30173
  const abs = join13(ctx.root, rel);
29803
30174
  if (!existsSync16(abs)) {
@@ -29809,11 +30180,15 @@ function runRegistryRule(ctx) {
29809
30180
  issues.push(issue("registry", rel, "missing **Source of truth for** banner (required for registry entry)"));
29810
30181
  }
29811
30182
  }
30183
+ return issues;
30184
+ }
30185
+ function checkUnregisteredBanners(ctx, registry) {
30186
+ const issues = [];
29812
30187
  for (const filePath of ctx.files) {
29813
30188
  const rel = relPath(filePath, ctx.root);
29814
30189
  if (rel === REGISTRY_REL_PATH)
29815
30190
  continue;
29816
- if (!rel.endsWith(".md") && !rel.endsWith(".mdc"))
30191
+ if (!(rel.endsWith(".md") || rel.endsWith(".mdc")))
29817
30192
  continue;
29818
30193
  const content3 = readFileSync13(filePath, "utf8");
29819
30194
  if (!SOURCE_OF_TRUTH_BANNER_LINE_RE.test(content3))
@@ -29824,6 +30199,16 @@ function runRegistryRule(ctx) {
29824
30199
  }
29825
30200
  return issues;
29826
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
+ }
29827
30212
  var registryRule = { id: "registry", run: runRegistryRule };
29828
30213
 
29829
30214
  // src/audit/rules/scan-gaps.ts
@@ -29835,7 +30220,10 @@ function runCoverageGapsRule(ctx) {
29835
30220
  for (const rel of candidates) {
29836
30221
  if (scanned.has(rel))
29837
30222
  continue;
29838
- 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
+ }));
29839
30227
  }
29840
30228
  return issues;
29841
30229
  }
@@ -29905,70 +30293,79 @@ function scanFileForSkillLinks(ctx, filePath, index2) {
29905
30293
  }
29906
30294
  return issues;
29907
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
+ }
29908
30322
  function validateReadmeTaxonomy(ctx, index2, diskSlugs) {
29909
30323
  const issues = [];
29910
30324
  const nonPublic = new Set(nonPublicSkills(ctx.config));
29911
30325
  for (const skillRoot of index2.roots) {
29912
30326
  if (skillRoot.kind !== "nested")
29913
30327
  continue;
29914
- const readmePath = join14(ctx.root, skillRoot.relPath, "README.md");
29915
- if (!existsSync17(readmePath))
29916
- continue;
29917
- const readme = readFileSync14(readmePath, "utf8");
29918
- 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))
29919
30342
  continue;
29920
- const taxonomySlugs = parseReadmeTaxonomySlugs(readme);
29921
- const nestedSlugs = diskSlugs.filter((slug2) => existsSync17(join14(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
29922
- const foreign = new Set(index2.foreignSlugs);
29923
- const publicSlugs = nestedSlugs.filter((slug2) => !nonPublic.has(slug2) && !foreign.has(slug2));
29924
- const relReadme = `${skillRoot.relPath}/README.md`;
29925
- for (const slug2 of publicSlugs) {
29926
- if (!taxonomySlugs.includes(slug2)) {
29927
- issues.push(issue("skill-index", relReadme, `taxonomy missing public skill "${slug2}"`));
29928
- }
29929
- }
29930
- for (const slug2 of taxonomySlugs) {
29931
- if (!nestedSlugs.includes(slug2)) {
29932
- issues.push(issue("skill-index", relReadme, `taxonomy lists skill "${slug2}" with no SKILL.md on disk`));
29933
- }
30343
+ for (const skillMd of walkSkillMarkdown(skillDir)) {
30344
+ issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
29934
30345
  }
29935
30346
  }
29936
30347
  return issues;
29937
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
+ }
29938
30357
  function runSkillIndexRule(ctx) {
29939
30358
  const issues = [];
29940
30359
  const index2 = ctx.skillIndex;
29941
30360
  const diskSlugs = listSkillSlugs(index2);
29942
30361
  for (const warning of index2.provenance.warnings) {
29943
- 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}`,
29944
30364
  severity: "warning"
29945
30365
  }));
29946
30366
  }
29947
30367
  issues.push(...validateReadmeTaxonomy(ctx, index2, diskSlugs));
29948
- const owned = new Set(index2.ownedSlugs);
29949
- for (const skillRoot of index2.roots) {
29950
- if (skillRoot.kind === "nested") {
29951
- for (const slug2 of index2.ownedSlugs) {
29952
- const skillDir = join14(ctx.root, skillRoot.relPath, slug2);
29953
- if (!existsSync17(skillDir))
29954
- continue;
29955
- for (const skillMd of walkSkillMarkdown(skillDir)) {
29956
- issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
29957
- }
29958
- }
29959
- continue;
29960
- }
29961
- for (const slug2 of index2.flatSlugs) {
29962
- if (!owned.has(slug2))
29963
- continue;
29964
- const skillDir = join14(ctx.root, slug2);
29965
- if (!existsSync17(skillDir))
29966
- continue;
29967
- for (const skillMd of walkSkillMarkdown(skillDir)) {
29968
- issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
29969
- }
29970
- }
29971
- }
30368
+ issues.push(...auditOwnedSkillFiles(ctx, index2));
29972
30369
  return issues;
29973
30370
  }
29974
30371
  var skillIndexRule = { id: "skill-index", run: runSkillIndexRule };
@@ -29996,6 +30393,19 @@ var skillsRules = [
29996
30393
  prosePolicyRule
29997
30394
  ];
29998
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
+ }
29999
30409
  function assembleRules(pluginRules = []) {
30000
30410
  const coreIds = new Set(allRules.map((rule) => rule.id));
30001
30411
  const seenPlugin = new Set;
@@ -30008,17 +30418,7 @@ function assembleRules(pluginRules = []) {
30008
30418
  const docs = [...docsRules];
30009
30419
  const skills = [...skillsRules];
30010
30420
  for (const rule of pluginRules) {
30011
- const suites = rule.suites ?? ["docs"];
30012
- const inDocs = suites.includes("docs");
30013
- const inSkills = suites.includes("skills");
30014
- if (!inDocs && !inSkills) {
30015
- const listed = suites.length === 0 ? "(empty)" : suites.join(", ");
30016
- 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.`);
30017
- }
30018
- if (inDocs)
30019
- docs.push(rule);
30020
- if (inSkills)
30021
- skills.push(rule);
30421
+ attachPluginRule(docs, skills, rule);
30022
30422
  }
30023
30423
  const selfById = new Map;
30024
30424
  for (const rule of [...docs, ...skills]) {
@@ -30041,46 +30441,70 @@ function rulesForSuite(suite, pluginRules = []) {
30041
30441
  }
30042
30442
 
30043
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
+ }
30044
30494
  function parseAuditArgs(argv) {
30045
- let suite = "docs";
30046
- let strict = false;
30047
- let json = false;
30048
- let dryRun = false;
30049
- let paths = [];
30050
- let only = null;
30051
- 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
+ };
30052
30504
  for (let i = 0;i < argv.length; i++) {
30053
- const arg = argv[i] ?? "";
30054
- if (arg.startsWith("--suite=")) {
30055
- suite = arg.slice("--suite=".length);
30056
- } else if (arg === "--strict") {
30057
- strict = true;
30058
- } else if (arg === "--json") {
30059
- json = true;
30060
- } else if (arg === "--dry-run") {
30061
- dryRun = true;
30062
- } else if (arg.startsWith("--dry-run=")) {
30063
- throw new Error("audit: use --dry-run (boolean flag), not --dry-run=<value>");
30064
- } else if (arg === "--fix") {
30065
- const next = argv[i + 1];
30066
- if (next && !next.startsWith("-")) {
30067
- if (next !== "doc-meta" && next !== "anchors") {
30068
- throw new Error(`Unknown --fix kind: ${next}. Use --fix, --fix=doc-meta, or --fix=anchors.`);
30069
- }
30070
- fix = next;
30071
- i++;
30072
- } else {
30073
- fix = true;
30074
- }
30075
- } else if (arg.startsWith("--fix=")) {
30076
- fix = arg.slice("--fix=".length);
30077
- } else if (arg.startsWith("--paths=")) {
30078
- paths = arg.slice("--paths=".length).split(",").map((path2) => path2.trim()).filter(Boolean);
30079
- } else if (arg.startsWith("--only=")) {
30080
- only = new Set(arg.slice("--only=".length).split(",").filter(Boolean));
30081
- }
30505
+ i = applyAuditFlag({ state, arg: argv[i] ?? "", argv, index: i });
30082
30506
  }
30083
- return { suite, strict, json, paths, only, fix, dryRun };
30507
+ return state;
30084
30508
  }
30085
30509
  function labelForSuite(suite) {
30086
30510
  switch (suite) {
@@ -30103,6 +30527,29 @@ function shouldRunRule(rule, options, pathScoped) {
30103
30527
  return false;
30104
30528
  return true;
30105
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
+ }
30106
30553
  async function runAudit(options) {
30107
30554
  const pathScoped = options.paths.length > 0;
30108
30555
  const base = createContext({
@@ -30112,26 +30559,9 @@ async function runAudit(options) {
30112
30559
  });
30113
30560
  const loaded = await loadPlugins(base.root, base.config);
30114
30561
  const ctx = { ...base, policies: loaded.policies };
30115
- if (options.fix !== null && options.fix !== undefined) {
30116
- if (options.suite !== "docs") {
30117
- console.error("--fix is supported only for audit docs");
30118
- return 1;
30119
- }
30120
- const kinds = fixKindsForOnly(parseFixKinds(options.fix), options.only);
30121
- if (kinds.length === 0) {
30122
- console.error("--fix has no overlapping rules with --only (doc-meta → doc-meta, anchors → links).");
30123
- return 1;
30124
- }
30125
- applyFixes(ctx, { kinds, dryRun: options.dryRun });
30126
- if (!options.dryRun) {
30127
- const refreshed = createContext({
30128
- root: options.root,
30129
- paths: options.paths.length > 0 ? options.paths : undefined,
30130
- policies: loaded.policies
30131
- });
30132
- Object.assign(ctx, refreshed);
30133
- }
30134
- }
30562
+ const fixExit = await runAuditFixes(options, ctx, loaded);
30563
+ if (fixExit !== null)
30564
+ return fixExit;
30135
30565
  const rules = rulesForSuite(options.suite, loaded.rules).filter((r) => !options.only || options.only.has(r.id));
30136
30566
  const skipGlobalsForPaths = pathScoped && !options.globalOnly;
30137
30567
  const issues = [];
@@ -30161,7 +30591,7 @@ function customizePathForSlug(root2, slug2) {
30161
30591
  return join15(customizeDir(root2), `${slug2}.md`);
30162
30592
  }
30163
30593
  function findCustomizeViaRegistry(root2, slug2) {
30164
- for (const rel of parseRegistryPaths(root2)) {
30594
+ for (const rel of parseRegistry(root2).paths) {
30165
30595
  const expected = `${REGISTRY_DIR_REL}/customize/${slug2}.md`;
30166
30596
  if (normalizeRelPath(rel) === expected && existsSync18(join15(root2, rel))) {
30167
30597
  return rel;
@@ -30214,11 +30644,11 @@ function readAlwaysInclude(root2, basenames, skipBasename) {
30214
30644
  function resolveCustomize(root2, slug2) {
30215
30645
  const slugFile = resolveSlugFile(root2, slug2);
30216
30646
  const alwaysNames = alwaysIncludeBasenames(root2);
30217
- const skip = slugFile.path != null ? basename3(slugFile.path) : null;
30647
+ const skip = slugFile.path !== null && slugFile.path !== undefined ? basename3(slugFile.path) : null;
30218
30648
  const always = readAlwaysInclude(root2, alwaysNames, skip);
30219
30649
  const parts = [];
30220
30650
  const included = [];
30221
- if (slugFile.content != null && slugFile.content.trim().length > 0) {
30651
+ if (slugFile.content !== null && slugFile.content !== undefined && slugFile.content.trim().length > 0) {
30222
30652
  parts.push(slugFile.content.trimEnd());
30223
30653
  if (slugFile.path)
30224
30654
  included.push(slugFile.path);
@@ -30246,6 +30676,7 @@ function resolveCustomizeFromRoot(slug2, startDir) {
30246
30676
  }
30247
30677
 
30248
30678
  // src/hooks/run.ts
30679
+ import process3 from "node:process";
30249
30680
  function parsePayload(raw) {
30250
30681
  if (!raw.trim())
30251
30682
  return {};
@@ -30276,7 +30707,7 @@ function extractSkillSlug(payload) {
30276
30707
  }
30277
30708
  const path2 = extractPath(payload);
30278
30709
  if (path2)
30279
- return slugFromPath(path2, process.cwd());
30710
+ return slugFromPath(path2, process3.cwd());
30280
30711
  return null;
30281
30712
  }
30282
30713
  function cursorResponse(content3) {
@@ -30331,6 +30762,7 @@ Customize override for /${slug2} (from ${from}):
30331
30762
  import { spawnSync as spawnSync2 } from "node:child_process";
30332
30763
  import { copyFileSync, existsSync as existsSync22, mkdirSync as mkdirSync2, readFileSync as readFileSync17 } from "node:fs";
30333
30764
  import { join as join19 } from "node:path";
30765
+ import process4 from "node:process";
30334
30766
 
30335
30767
  // src/init/merge-hooks.ts
30336
30768
  import { existsSync as existsSync21, mkdirSync, readFileSync as readFileSync16, writeFileSync as writeFileSync2 } from "node:fs";
@@ -30400,7 +30832,7 @@ function walkNodeModulesCli(cwd) {
30400
30832
  }
30401
30833
  function isInsidePackageRoot(cwd) {
30402
30834
  const rel = relative10(PACKAGE_ROOT, resolve8(cwd)).replace(/\\/g, "/");
30403
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
30835
+ return rel === "" || !(rel.startsWith("..") || rel.startsWith("/"));
30404
30836
  }
30405
30837
  function nodeCliHookCommand(cliPath) {
30406
30838
  return `node ${cliPath} hook customize`;
@@ -30450,6 +30882,36 @@ function writeJson(path2, value) {
30450
30882
  function deepEqual(a, b) {
30451
30883
  return JSON.stringify(a) === JSON.stringify(b);
30452
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
+ }
30453
30915
  function mergeCursorHooks(targetPath, fragment, opts) {
30454
30916
  const existing = readJson(targetPath) ?? {};
30455
30917
  const hooks = existing.hooks ?? {};
@@ -30460,79 +30922,103 @@ function mergeCursorHooks(targetPath, fragment, opts) {
30460
30922
  const skeletonIdx = postToolUse.findIndex((entry) => isSkeletonHookCommand(entry.command));
30461
30923
  const canonical = { ...incoming, matcher: incoming.matcher ?? "Read" };
30462
30924
  if (skeletonIdx >= 0) {
30463
- const current = postToolUse[skeletonIdx];
30464
- const userEdited = current && !opts.forceHooks && (current.matcher !== canonical.matcher || !isSkeletonHookCommand(current.command));
30465
- if (userEdited) {
30466
- return {
30467
- platform: "cursor",
30468
- action: "conflict",
30469
- message: identityKey("cursor", "postToolUse", String(current.matcher ?? "Read"))
30470
- };
30471
- }
30472
- const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["command", "matcher"].includes(key)));
30473
- const merged = { ...extras, ...canonical };
30474
- if (deepEqual(current, merged))
30475
- return { platform: "cursor", action: "skipped" };
30476
- postToolUse[skeletonIdx] = merged;
30925
+ const conflict = mergeExistingCursorHook({ postToolUse, skeletonIdx, canonical, opts });
30926
+ if (conflict)
30927
+ return conflict;
30477
30928
  } else {
30478
30929
  postToolUse.push(canonical);
30479
30930
  }
30480
- const next = {
30481
- ...existing,
30482
- version: existing.version ?? 1,
30483
- hooks: { ...hooks, postToolUse }
30484
- };
30485
- if (deepEqual(existing, next))
30486
- return { platform: "cursor", action: "skipped" };
30487
- writeJson(targetPath, next);
30488
- return { platform: "cursor", action: skeletonIdx >= 0 ? "updated" : "added" };
30931
+ return persistCursorHooks({ targetPath, existing, hooks, postToolUse, skeletonIdx });
30489
30932
  }
30490
- function mergeNestedHooks(platform, targetPath, fragment, eventName, opts) {
30491
- const existing = readJson(targetPath) ?? {};
30492
- const rootHooks = existing.hooks ?? {};
30493
- const eventHooks = [...rootHooks[eventName] ?? []];
30494
- 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) {
30495
30986
  let changed = false;
30496
- for (const incomingGroup of incomingGroups) {
30497
- const matcher = incomingGroup.matcher ?? "";
30987
+ for (const incomingGroup of args.incomingGroups) {
30498
30988
  const incomingHook = incomingGroup.hooks?.[0];
30499
30989
  if (!incomingHook)
30500
30990
  continue;
30501
- const groupIdx = eventHooks.findIndex((group2) => group2.matcher === matcher);
30502
- if (groupIdx < 0) {
30503
- eventHooks.push({
30504
- ...incomingGroup,
30505
- hooks: [{ ...incomingHook }]
30506
- });
30507
- changed = true;
30508
- continue;
30509
- }
30510
- const group = eventHooks[groupIdx];
30511
- const hooks = [...group?.hooks ?? []];
30512
- const skeletonIdx = hooks.findIndex((entry) => isSkeletonHookCommand(entry.command));
30513
- if (skeletonIdx >= 0) {
30514
- const current = hooks[skeletonIdx];
30515
- const userEdited = current && !opts.forceHooks && (current.type !== incomingHook.type || !isSkeletonHookCommand(current.command));
30516
- if (userEdited) {
30517
- return {
30518
- platform,
30519
- action: "conflict",
30520
- message: identityKey(platform, eventName, matcher)
30521
- };
30522
- }
30523
- const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["type", "command"].includes(key)));
30524
- const merged = { ...extras, ...incomingHook };
30525
- if (!deepEqual(current, merged)) {
30526
- hooks[skeletonIdx] = merged;
30527
- changed = true;
30528
- }
30529
- } else {
30530
- 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)
30531
31002
  changed = true;
30532
- }
30533
- eventHooks[groupIdx] = { ...group, matcher, hooks };
30534
31003
  }
30535
- 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)
30536
31022
  return { platform, action: "skipped" };
30537
31023
  const next = {
30538
31024
  ...existing,
@@ -30548,11 +31034,23 @@ function mergeHookConfigs(opts) {
30548
31034
  results.push(mergeCursorHooks(cursorPath, cursorFragment, opts));
30549
31035
  const claudePath = join18(opts.cwd, ".claude/settings.json");
30550
31036
  const claudeFragment = loadFragment("claude-settings.fragment.json", opts.hookCommand);
30551
- 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
+ }));
30552
31044
  const codexPath = join18(opts.cwd, ".codex/hooks.json");
30553
31045
  if (existsSync21(join18(opts.cwd, ".codex"))) {
30554
31046
  const codexFragment = loadFragment("codex-hooks.fragment.json", opts.hookCommand);
30555
- 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
+ }));
30556
31054
  } else {
30557
31055
  results.push({ platform: "codex", action: "skipped", message: "missing .codex directory" });
30558
31056
  }
@@ -30657,26 +31155,36 @@ function runSkillsAdd(args, cwd) {
30657
31155
  });
30658
31156
  return result.status ?? 1;
30659
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
+ }
30660
31179
  function runInit(options = {}) {
30661
- const cwd = options.cwd ?? process.cwd();
31180
+ const cwd = options.cwd ?? process4.cwd();
30662
31181
  assertPackageResolvable(cwd);
30663
31182
  const scaffold = writeScaffold(cwd);
30664
31183
  const hookCommand = resolveHookCommand(cwd);
30665
- const hooks = mergeHookConfigs({
30666
- cwd,
30667
- hookCommand,
30668
- forceHooks: options.forceHooks
30669
- });
31184
+ const hooks = mergeHookConfigs({ cwd, hookCommand, forceHooks: options.forceHooks });
30670
31185
  const scripts = mergePackageJsonScripts(cwd);
30671
- for (const result of hooks) {
30672
- if (result.action === "conflict") {
30673
- console.error(`init: skipped ${result.platform} hook (user-edited) — re-run with --force-hooks to restore`);
30674
- } else if (result.action === "added") {
30675
- console.log(`init: added ${result.platform} customize hook`);
30676
- } else if (result.action === "updated") {
30677
- console.log(`init: updated ${result.platform} customize hook`);
30678
- }
30679
- }
31186
+ for (const result of hooks)
31187
+ logHookMergeResult(result);
30680
31188
  if (scaffold === "created") {
30681
31189
  console.log("init: wrote .skeleton/config.yaml and registry.md");
30682
31190
  } else {
@@ -30685,17 +31193,7 @@ function runInit(options = {}) {
30685
31193
  if (scripts === "updated") {
30686
31194
  console.log("init: merged validate/audit scripts into package.json");
30687
31195
  }
30688
- let skills = "skipped";
30689
- if (options.skills && !options.noSkills) {
30690
- const args = skillsAddArgs({ skillsFlags: options.skillsFlags });
30691
- const run = options.runSkillsCommand ?? runSkillsAdd;
30692
- const exitCode = run(args, cwd);
30693
- if (exitCode !== 0) {
30694
- throw new Error(`skills install failed: npx ${args.join(" ")}`);
30695
- }
30696
- skills = "installed";
30697
- console.log("init: installed /skeleton skill");
30698
- }
31196
+ const skills = installSkillsIfRequested(options, cwd);
30699
31197
  return { scaffold, hooks, scripts, skills };
30700
31198
  }
30701
31199
 
@@ -30786,7 +31284,7 @@ function sourceFingerprint(tsAbs, seen = new Set) {
30786
31284
  hash.update("\x00");
30787
31285
  hash.update(content3);
30788
31286
  hash.update("\x00");
30789
- for (const dep of localImportPaths(abs, content3).sort()) {
31287
+ for (const dep of localImportPaths(abs, content3).sort((a, b) => a.localeCompare(b))) {
30790
31288
  walk(dep);
30791
31289
  }
30792
31290
  }
@@ -30857,6 +31355,9 @@ async function runBuildPlugin(options = {}) {
30857
31355
  return { built, checked };
30858
31356
  }
30859
31357
 
31358
+ // src/references/run.ts
31359
+ import process6 from "node:process";
31360
+
30860
31361
  // src/references/sync.ts
30861
31362
  import {
30862
31363
  existsSync as existsSync24,
@@ -30867,14 +31368,13 @@ import {
30867
31368
  writeFileSync as writeFileSync4
30868
31369
  } from "node:fs";
30869
31370
  import { dirname as dirname12, join as join20, relative as relative11 } from "node:path";
31371
+ import process5 from "node:process";
30870
31372
  function resolveOwnership(root2, override) {
30871
31373
  if (override !== undefined)
30872
31374
  return override;
30873
31375
  try {
30874
31376
  return loadConfig(root2).skillOwnership;
30875
- } catch {
30876
- return;
30877
- }
31377
+ } catch {}
30878
31378
  }
30879
31379
  function walkMarkdownFiles2(dir, root2) {
30880
31380
  const files = [];
@@ -30894,89 +31394,97 @@ function walkMarkdownFiles2(dir, root2) {
30894
31394
  }
30895
31395
  return files;
30896
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
+ }
30897
31414
  function listGeneratedReferenceFiles(skillDir, skill) {
30898
31415
  const refsDir = join20(skillDir, "references");
30899
31416
  if (!existsSync24(refsDir))
30900
31417
  return [];
30901
31418
  const files = [];
30902
- const walk = (dir) => {
30903
- for (const entry of readdirSync6(dir, { withFileTypes: true })) {
30904
- const fullPath = join20(dir, entry.name);
30905
- if (entry.isDirectory()) {
30906
- walk(fullPath);
30907
- continue;
30908
- }
30909
- if (!entry.name.endsWith(".md"))
30910
- continue;
30911
- const content3 = readFileSync19(fullPath, "utf8");
30912
- if (isGeneratedReference(content3)) {
30913
- const refPath = normalizeRelPath(relative11(refsDir, fullPath));
30914
- files.push(generatedRefPath(skill, refPath));
30915
- }
30916
- }
30917
- };
30918
- walk(refsDir);
31419
+ collectGeneratedInDir({ dir: refsDir, refsDir, skill, files });
30919
31420
  return files;
30920
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
+ }
30921
31478
  function syncReferences(options = {}) {
30922
- const root2 = options.root ?? process.cwd();
31479
+ const root2 = options.root ?? process5.cwd();
30923
31480
  const canonicalDir = join20(root2, CANONICAL_REFS_DIR);
30924
31481
  if (!existsSync24(canonicalDir)) {
30925
31482
  throw new Error(`canonical references dir not found: ${CANONICAL_REFS_DIR}`);
30926
31483
  }
30927
- const result = {
30928
- written: [],
30929
- rewritten: [],
30930
- removed: [],
30931
- skipped: []
30932
- };
31484
+ const result = { written: [], rewritten: [], removed: [], skipped: [] };
30933
31485
  const plans = discoverSkillReferencePlans(root2, resolveOwnership(root2, options.ownership));
30934
- for (const plan of plans) {
30935
- const skillDir = join20(root2, plan.skill);
30936
- for (const refPath of plan.refPaths) {
30937
- const sourceRel = normalizeRelPath(join20(CANONICAL_REFS_DIR, refPath));
30938
- const canonicalPath = join20(root2, sourceRel);
30939
- if (!existsSync24(canonicalPath)) {
30940
- throw new Error(`canonical reference missing: ${sourceRel}`);
30941
- }
30942
- const targetRel = generatedRefPath(plan.skill, refPath);
30943
- const targetPath = join20(root2, targetRel);
30944
- const canonicalContent = readFileSync19(canonicalPath, "utf8");
30945
- const nextContent = formatGeneratedHeader(sourceRel) + canonicalContent;
30946
- if (!options.dryRun) {
30947
- mkdirSync3(dirname12(targetPath), { recursive: true });
30948
- }
30949
- const existing = existsSync24(targetPath) ? readFileSync19(targetPath, "utf8") : null;
30950
- if (existing !== nextContent) {
30951
- if (!options.dryRun)
30952
- writeFileSync4(targetPath, nextContent, "utf8");
30953
- result.written.push(targetRel);
30954
- } else {
30955
- result.skipped.push(targetRel);
30956
- }
30957
- }
30958
- if (options.rewriteLinks !== false) {
30959
- for (const relFile of walkMarkdownFiles2(skillDir, root2)) {
30960
- const filePath = join20(root2, relFile);
30961
- const content3 = readFileSync19(filePath, "utf8");
30962
- const next = rewriteSharedRefLinks(content3, relFile, plan.skill);
30963
- if (next !== content3) {
30964
- if (!options.dryRun)
30965
- writeFileSync4(filePath, next, "utf8");
30966
- result.rewritten.push(relFile);
30967
- }
30968
- }
30969
- }
30970
- for (const generatedRel of listGeneratedReferenceFiles(skillDir, plan.skill)) {
30971
- const refPath = generatedRel.slice(`${plan.skill}/references/`.length);
30972
- if (!plan.refPaths.has(refPath)) {
30973
- const fullPath = join20(root2, generatedRel);
30974
- if (!options.dryRun)
30975
- unlinkSync(fullPath);
30976
- result.removed.push(generatedRel);
30977
- }
30978
- }
30979
- }
31486
+ for (const plan of plans)
31487
+ syncPlan({ root: root2, plan, options, result });
30980
31488
  return result;
30981
31489
  }
30982
31490
 
@@ -30985,7 +31493,7 @@ function runReferencesSync(options = {}) {
30985
31493
  return syncReferences(options);
30986
31494
  }
30987
31495
  function runReferencesCheck(options = {}) {
30988
- const root2 = options.root ?? process.cwd();
31496
+ const root2 = options.root ?? process6.cwd();
30989
31497
  let ownership;
30990
31498
  try {
30991
31499
  ownership = loadConfig(root2).skillOwnership;
@@ -30999,22 +31507,17 @@ function runReferencesCheck(options = {}) {
30999
31507
  label: "References check"
31000
31508
  });
31001
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
+ }
31002
31517
  function printSyncResult(result) {
31003
- if (result.written.length > 0) {
31004
- console.log(`references sync: wrote ${result.written.length} file(s)`);
31005
- for (const file of result.written)
31006
- console.log(` + ${file}`);
31007
- }
31008
- if (result.rewritten.length > 0) {
31009
- console.log(`references sync: rewrote links in ${result.rewritten.length} file(s)`);
31010
- for (const file of result.rewritten)
31011
- console.log(` ~ ${file}`);
31012
- }
31013
- if (result.removed.length > 0) {
31014
- console.log(`references sync: removed ${result.removed.length} stale file(s)`);
31015
- for (const file of result.removed)
31016
- console.log(` - ${file}`);
31017
- }
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, "-");
31018
31521
  if (result.written.length === 0 && result.rewritten.length === 0 && result.removed.length === 0) {
31019
31522
  console.log(`references sync: up to date (${result.skipped.length} file(s) checked)`);
31020
31523
  }
@@ -31048,7 +31551,7 @@ function parseRegistryRows(content3) {
31048
31551
  for (const line of content3.split(`
31049
31552
  `)) {
31050
31553
  const match = REGISTRY_TABLE_ROW_RE2.exec(line);
31051
- if (!match?.[1] || !match[2])
31554
+ if (!(match?.[1] && match[2]))
31052
31555
  continue;
31053
31556
  rows.push({ topic: match[1].trim(), link: match[2].trim(), line });
31054
31557
  }
@@ -31079,7 +31582,8 @@ ${REGISTRY_TABLE_HEADER}
31079
31582
 
31080
31583
  `;
31081
31584
  }
31082
- function upsertRow(content3, topic, link2, section, root2) {
31585
+ function upsertRow(opts) {
31586
+ const { content: content3, topic, link: link2, section, root: root2 } = opts;
31083
31587
  const rows = parseRegistryRows(content3);
31084
31588
  const targetPath = link2;
31085
31589
  const existingByLink = rows.find((row) => row.link === targetPath);
@@ -31119,13 +31623,19 @@ ${newLine}
31119
31623
  `;
31120
31624
  return { content: appended, action: "added" };
31121
31625
  }
31122
- function registerPath(options) {
31123
- const root2 = options.root ?? findRepoRoot();
31124
- const relPath2 = normalizeRelPath(options.path);
31125
- const absPath = join21(root2, relPath2);
31126
- if (!existsSync25(absPath)) {
31127
- 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}`);
31128
31635
  }
31636
+ }
31637
+ function resolveRegistrationTopic(input) {
31638
+ const { options, root: root2, relPath: relPath2, absPath } = input;
31129
31639
  const content3 = readFileSync20(absPath, "utf8");
31130
31640
  let topic = options.topic ?? extractTopic(content3);
31131
31641
  if (!topic) {
@@ -31133,13 +31643,36 @@ function registerPath(options) {
31133
31643
  }
31134
31644
  const registryLink = toRegistryLink(root2, absPath);
31135
31645
  topic = ensureCustomizeTopic(topic, registryLink);
31136
- const section = inferSection(registryLink);
31646
+ return { topic, registryLink, section: inferSection(registryLink) };
31647
+ }
31648
+ function loadRegistryContent(root2) {
31137
31649
  const registryAbs = join21(root2, REGISTRY_REL_PATH);
31138
- let registryContent = existsSync25(registryAbs) ? readFileSync20(registryAbs, "utf8") : defaultRegistryContent();
31139
- if (!existsSync25(registryAbs) && !existsSync25(join21(root2, ".skeleton/config.yaml"))) {
31650
+ if (!(existsSync25(registryAbs) || existsSync25(join21(root2, ".skeleton/config.yaml")))) {
31140
31651
  throw new Error("Missing .skeleton/config.yaml — run skeleton init first");
31141
31652
  }
31142
- 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
+ });
31143
31676
  registryContent = updated;
31144
31677
  const result = {
31145
31678
  topic,
@@ -31148,6 +31681,7 @@ function registerPath(options) {
31148
31681
  action,
31149
31682
  warnOutsideScan: isOutsideScan(root2, relPath2)
31150
31683
  };
31684
+ const registryAbs = join21(root2, REGISTRY_REL_PATH);
31151
31685
  if (!options.dryRun && action !== "noop") {
31152
31686
  const dir = dirname13(registryAbs);
31153
31687
  if (!existsSync25(dir)) {
@@ -31158,15 +31692,7 @@ function registerPath(options) {
31158
31692
  if (result.warnOutsideScan) {
31159
31693
  console.error(`warning: ${relPath2} is outside scan.include — register succeeded but audit will not scan it`);
31160
31694
  }
31161
- if (options.json) {
31162
- console.log(JSON.stringify(result, null, 2));
31163
- } else if (options.dryRun) {
31164
- console.log(`dry-run: would ${action} registry row for ${relPath2} → ${topic}`);
31165
- } else if (action === "noop") {
31166
- console.log(`register: ${relPath2} already registered (${topic})`);
31167
- } else {
31168
- console.log(`register: ${action} ${relPath2} → ${topic}`);
31169
- }
31695
+ printRegisterResult(options, result, relPath2);
31170
31696
  return result;
31171
31697
  }
31172
31698
 
@@ -31212,41 +31738,49 @@ function isSkeletonYamlCandidate(normalized, ext) {
31212
31738
  return false;
31213
31739
  return true;
31214
31740
  }
31215
- 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) {
31216
31759
  const normalized = normalizeRelPath(relPath2);
31217
31760
  const ext = extname2(normalized).toLowerCase();
31218
31761
  const name = basename5(normalized);
31219
- if (SKIP_EXTENSIONS.has(ext))
31220
- return "skip";
31221
- if (COMMAND_CONFIG_NAMES.has(name))
31762
+ if (SKIP_EXTENSIONS.has(ext) || COMMAND_CONFIG_NAMES.has(name))
31222
31763
  return "skip";
31223
31764
  if (isSkeletonYamlCandidate(normalized, ext)) {
31224
- if (wiredPolicies.has(normalized))
31225
- return "policy";
31226
- return "skip";
31227
- }
31228
- if (isSkillPath(normalized, skillIndex)) {
31229
- if (isForeignSkillPath(normalized, skillIndex))
31230
- return "foreign-skill";
31231
- return "skills";
31232
- }
31233
- if (DOC_EXTENSIONS.has(ext)) {
31234
- const config = loadConfig(root2);
31235
- if (isInScanPerimeter(normalized, config, root2, skillIndex))
31236
- return "docs";
31237
- return "skip";
31238
- }
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;
31239
31773
  if (SHELL_EXTENSIONS.has(ext))
31240
31774
  return "shell";
31241
31775
  if (ext === ".json")
31242
31776
  return "json";
31243
31777
  return "skip";
31244
31778
  }
31245
- function isInScanPerimeter(relPath2, config, root2, skillIndex) {
31246
- 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)));
31247
31781
  if (scanned.has(relPath2))
31248
31782
  return true;
31249
- return config.scan.include.some((pattern) => matchesGlobScope(relPath2, pattern));
31783
+ return ctx.config.scan.include.some((pattern) => matchesGlobScope(relPath2, pattern));
31250
31784
  }
31251
31785
  function parseJsonContent(content3) {
31252
31786
  try {
@@ -31298,27 +31832,30 @@ function resolvePaths(options) {
31298
31832
  root: options.root
31299
31833
  });
31300
31834
  }
31301
- function codeValidationHint(root2) {
31302
- let pm2 = null;
31835
+ function packageManagerFromPackageJson(root2) {
31303
31836
  const pkgPath = join22(root2, "package.json");
31304
- if (existsSync26(pkgPath)) {
31305
- try {
31306
- const pkg = JSON.parse(readFileSync21(pkgPath, "utf8"));
31307
- const raw = pkg.packageManager?.split("@")[0];
31308
- if (raw === "bun" || raw === "npm" || raw === "pnpm" || raw === "yarn")
31309
- pm2 = raw;
31310
- } catch {}
31311
- }
31312
- if (!pm2) {
31313
- if (existsSync26(join22(root2, "bun.lock")) || existsSync26(join22(root2, "bun.lockb")))
31314
- pm2 = "bun";
31315
- else if (existsSync26(join22(root2, "pnpm-lock.yaml")))
31316
- pm2 = "pnpm";
31317
- else if (existsSync26(join22(root2, "yarn.lock")))
31318
- pm2 = "yarn";
31319
- else if (existsSync26(join22(root2, "package-lock.json")))
31320
- pm2 = "npm";
31321
- }
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) {
31322
31859
  switch (pm2) {
31323
31860
  case "bun":
31324
31861
  return " Run: bun test && bun run typecheck && bun run build";
@@ -31328,126 +31865,113 @@ function codeValidationHint(root2) {
31328
31865
  return " Run: pnpm test && pnpm run typecheck";
31329
31866
  case "yarn":
31330
31867
  return " Run: yarn test && yarn typecheck";
31331
- default:
31332
- return " Run your local code validation gates (test + typecheck + build).";
31333
31868
  }
31334
31869
  }
31335
- async function runValidateChanged(options = {}) {
31336
- const root2 = options.root ?? findRepoRoot();
31337
- const relPaths = resolvePaths(options);
31338
- if (relPaths.length === 0) {
31339
- console.log("validate changed: no changed files.");
31340
- return 0;
31341
- }
31342
- const config = loadConfig(root2);
31343
- const skillIndex = buildSkillIndex(root2, config.skillOwnership);
31344
- let wiredPolicies;
31345
- try {
31346
- wiredPolicies = await collectWiredPolicyRelPaths(root2, config);
31347
- } catch (error) {
31348
- console.error(`validate changed: ${error instanceof Error ? error.message : error}`);
31349
- return 1;
31350
- }
31351
- const buckets = {
31352
- docs: [],
31353
- skills: [],
31354
- shell: [],
31355
- json: [],
31356
- policy: []
31357
- };
31358
- let missing = 0;
31359
- let skipped = 0;
31360
- let foreignSkipped = 0;
31361
- const orphans = [];
31362
- for (const relPath2 of relPaths) {
31363
- const normalized = normalizeRelPath(relPath2);
31364
- const abs = join22(root2, normalized);
31365
- if (!existsSync26(abs)) {
31366
- missing++;
31367
- console.error(`validate changed: path not found: ${relPath2}`);
31368
- continue;
31369
- }
31370
- const ext = extname2(normalized).toLowerCase();
31371
- if (isSkeletonYamlCandidate(normalized, ext) && !wiredPolicies.has(normalized)) {
31372
- orphans.push(normalized);
31373
- continue;
31374
- }
31375
- const bucket = bucketFor(normalized, root2, wiredPolicies, skillIndex);
31376
- if (bucket === "skip") {
31377
- skipped++;
31378
- continue;
31379
- }
31380
- if (bucket === "foreign-skill") {
31381
- foreignSkipped++;
31382
- console.log(`validate changed: skipping foreign skill ${normalized} (owned upstream; see skills-lock.json / skillOwnership)`);
31383
- continue;
31384
- }
31385
- 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;
31386
31885
  }
31387
- if (orphans.length > 0) {
31388
- for (const orphan of orphans) {
31389
- console.error(`validate changed: ${orphan} is under .skeleton/ but not referenced by any plugin policies glob.
31390
- ` + " Export it from a plugin `policies` array (see docs/developer/plugins.md), or remove the file.");
31391
- }
31392
- return 1;
31886
+ const ext = extname2(normalized).toLowerCase();
31887
+ if (isSkeletonYamlCandidate(normalized, ext) && !ctx.wiredPolicies.has(normalized)) {
31888
+ state.orphans.push(normalized);
31889
+ return;
31393
31890
  }
31394
- const audited = buckets.docs.length + buckets.skills.length + buckets.shell.length + buckets.json.length + buckets.policy.length;
31395
- let exitCode = 0;
31396
- if (missing > 0 && audited === 0 && skipped === 0) {
31397
- console.error("validate changed: no paths existed on disk. Pass real paths or use --staged / --base.");
31398
- return 1;
31891
+ const bucket = bucketFor(normalized, bucketCtx);
31892
+ if (bucket === "skip") {
31893
+ state.skipped++;
31894
+ return;
31399
31895
  }
31400
- if (options.base) {
31401
- const globalExit = await runAudit({
31402
- suite: "self",
31403
- strict: false,
31404
- json: false,
31405
- paths: [],
31406
- only: null,
31407
- root: root2,
31408
- globalOnly: true
31409
- });
31410
- if (globalExit !== 0)
31411
- 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;
31412
31900
  }
31413
- if (skipped > 0 && audited === 0) {
31414
- console.error(`validate changed: all paths were skipped (code/config). This does not verify TypeScript or app code.
31415
- ` + codeValidationHint(root2));
31416
- 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 });
31417
31918
  }
31418
- if (buckets.docs.length > 0) {
31419
- const docExit = await runAudit({
31420
- suite: "docs",
31421
- strict: false,
31422
- json: false,
31423
- paths: buckets.docs,
31424
- only: null,
31425
- root: root2,
31426
- pathScopedOnly: true
31427
- });
31428
- if (docExit !== 0)
31429
- 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.");
31430
31925
  }
31431
- if (buckets.skills.length > 0) {
31432
- if (!options.base) {
31433
- 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).
31434
31959
  ` + ` Run: skeleton audit skills
31435
31960
  ` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
31436
- exitCode = 1;
31437
- } else {
31438
- const skillExit = await runAudit({
31439
- suite: "skills",
31440
- strict: false,
31441
- json: false,
31442
- paths: buckets.skills,
31443
- only: null,
31444
- root: root2,
31445
- pathScopedOnly: true
31446
- });
31447
- if (skillExit !== 0)
31448
- exitCode = 1;
31449
- }
31961
+ return 1;
31450
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;
31451
31975
  for (const relPath2 of buckets.shell) {
31452
31976
  if (validateShell(relPath2, root2) !== 0)
31453
31977
  exitCode = 1;
@@ -31460,49 +31984,124 @@ async function runValidateChanged(options = {}) {
31460
31984
  if (validatePolicy(relPath2, root2) !== 0)
31461
31985
  exitCode = 1;
31462
31986
  }
31463
- if (buckets.policy.length > 0) {
31464
- if (options.base) {
31465
- const proseExit = await runAudit({
31466
- suite: "docs",
31467
- strict: false,
31468
- json: false,
31469
- paths: [],
31470
- only: null,
31471
- root: root2
31472
- });
31473
- if (proseExit !== 0)
31474
- exitCode = 1;
31475
- const skillPaths = listSkillMarkdownPaths(root2, skillIndex);
31476
- if (skillPaths.length > 0) {
31477
- const skillProseExit = await runAudit({
31478
- suite: "skills",
31479
- strict: false,
31480
- json: false,
31481
- paths: skillPaths,
31482
- only: null,
31483
- root: root2,
31484
- pathScopedOnly: true
31485
- });
31486
- if (skillProseExit !== 0)
31487
- exitCode = 1;
31488
- }
31489
- } else {
31490
- 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).
31491
32019
  ` + ` Run: skeleton audit docs
31492
32020
  ` + ` And: skeleton audit skills
31493
32021
  ` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
31494
- return 1;
31495
- }
32022
+ return 1;
31496
32023
  }
31497
- if (exitCode === 0) {
31498
- const parts = [];
31499
- if (skipped > 0)
31500
- parts.push(`${skipped} path(s) skipped`);
31501
- if (foreignSkipped > 0)
31502
- parts.push(`${foreignSkipped} foreign skill(s) ignored`);
31503
- const note = parts.length > 0 ? ` (${parts.join(", ")})` : "";
31504
- 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;
31505
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);
31506
32105
  return exitCode;
31507
32106
  }
31508
32107
 
@@ -31534,123 +32133,153 @@ function parseRegisterArgs(argv) {
31534
32133
  json = true;
31535
32134
  else if (arg.startsWith("--topic="))
31536
32135
  topic = arg.slice("--topic=".length);
31537
- else if (!arg.startsWith("-") && !path2)
32136
+ else if (!(arg.startsWith("-") || path2))
31538
32137
  path2 = arg;
31539
32138
  }
31540
32139
  return { path: path2, topic, dryRun, json };
31541
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
+ }
31542
32266
  async function main() {
31543
- const argv = process.argv.slice(2);
32267
+ const argv = process7.argv.slice(2);
31544
32268
  const command = argv[0];
31545
32269
  if (!command || command === "--help" || command === "-h") {
31546
32270
  usage();
31547
- process.exit(command ? 0 : 1);
32271
+ process7.exit(command ? 0 : 1);
31548
32272
  }
31549
32273
  try {
31550
- if (command === "audit") {
31551
- const sub = argv[1];
31552
- if (sub !== "docs" && sub !== "self" && sub !== "skills") {
31553
- usage();
31554
- process.exit(1);
31555
- }
31556
- const options = parseAuditArgs(argv.slice(2));
31557
- options.suite = sub;
31558
- process.exit(await runAudit(options));
31559
- }
31560
- if (command === "build-plugin") {
31561
- const { entry, check } = parseBuildPluginArgs(argv.slice(1));
31562
- const root2 = findRepoRoot();
31563
- const result = await runBuildPlugin({ root: root2, entry, check });
31564
- if (check) {
31565
- console.log(result.checked.length === 0 ? "build-plugin --check: no plugins configured." : `build-plugin --check: ${result.checked.length} plugin(s) up to date.`);
31566
- } else {
31567
- console.log(result.built.length === 0 ? "build-plugin: no plugins configured." : `build-plugin: built ${result.built.length} plugin(s).`);
31568
- }
31569
- process.exit(0);
31570
- }
31571
- if (command === "validate" && argv[1] === "changed") {
31572
- const rest = argv.slice(2);
31573
- const paths = [];
31574
- let staged = false;
31575
- let base;
31576
- for (let i = 0;i < rest.length; i++) {
31577
- const arg = rest[i];
31578
- if (arg === "--staged")
31579
- staged = true;
31580
- else if (arg === "--base")
31581
- base = rest[++i];
31582
- else if (arg?.startsWith("--base="))
31583
- base = arg.slice("--base=".length);
31584
- else if (arg && !arg.startsWith("-"))
31585
- paths.push(arg);
31586
- }
31587
- process.exit(await runValidateChanged({ paths, staged, base }));
31588
- }
31589
- if (command === "register") {
31590
- const opts = parseRegisterArgs(argv.slice(1));
31591
- if (!opts.path) {
31592
- console.error("register: path required");
31593
- process.exit(1);
31594
- }
31595
- registerPath({
31596
- path: opts.path,
31597
- topic: opts.topic,
31598
- dryRun: opts.dryRun,
31599
- json: opts.json
31600
- });
31601
- process.exit(0);
31602
- }
31603
- if (command === "customize" && argv[1] === "resolve") {
31604
- const slug2 = argv[2];
31605
- const json = argv.includes("--json");
31606
- if (!slug2) {
31607
- console.error("customize resolve: slug required");
31608
- process.exit(1);
31609
- }
31610
- const result = resolveCustomizeFromRoot(slug2);
31611
- if (json) {
31612
- console.log(JSON.stringify(result, null, 2));
31613
- } else if (result.content) {
31614
- process.stdout.write(result.content);
31615
- }
31616
- process.exit(0);
31617
- }
31618
- if (command === "hook") {
31619
- if (argv[1] !== "customize") {
31620
- usage();
31621
- process.exit(1);
31622
- }
31623
- process.stdout.write(runCustomizeHook(readFileSync22(0, "utf8")));
31624
- process.exit(0);
31625
- }
31626
- if (command === "init") {
31627
- const parsed = parseInitArgs(argv.slice(1));
31628
- runInit(parsed);
31629
- process.exit(0);
31630
- }
31631
- if (command === "references") {
31632
- const sub = argv[1];
31633
- if (sub === "sync") {
31634
- const dryRun = argv.includes("--dry-run");
31635
- const rewriteLinks = !argv.includes("--no-rewrite-links");
31636
- const result = runReferencesSync({ dryRun, rewriteLinks });
31637
- printSyncResult(result);
31638
- process.exit(0);
31639
- }
31640
- if (sub === "check") {
31641
- process.exit(runReferencesCheck({
31642
- json: argv.includes("--json"),
31643
- strict: argv.includes("--strict")
31644
- }));
31645
- }
32274
+ const exitCode = await dispatchCommand(argv);
32275
+ if (exitCode === null) {
31646
32276
  usage();
31647
- process.exit(1);
32277
+ process7.exit(1);
31648
32278
  }
31649
- usage();
31650
- process.exit(1);
32279
+ process7.exit(exitCode);
31651
32280
  } catch (error) {
31652
32281
  console.error(String(error));
31653
- process.exit(1);
32282
+ process7.exit(1);
31654
32283
  }
31655
32284
  }
31656
32285
  main();