@csark0812/skeleton 1.6.2 → 1.6.3

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/README.md CHANGED
@@ -4,11 +4,13 @@
4
4
 
5
5
  <!-- doc-meta: owner=eng | last-reviewed=2026-08-16 -->
6
6
 
7
+ <!-- code-fit: targets=src/cli.ts surface=audit,validate,catalog,init,build-plugin,references -->
8
+
7
9
  Agent repos get messy fast. Skills get copied around, docs disagree, links go stale, and nobody remembers which file is actually canonical.
8
10
 
9
11
  Skeleton is an SSOT linter for that layer. Define the contract once; Skeleton checks it locally and in CI. If a canonical doc disappears, SSOT markers drift, a skill index stops matching disk, or a generated reference gets edited by hand, the audit fails before merge.
10
12
 
11
- Think ESLint — for the docs and skills your agents rely on.
13
+ Think ESLint — for the docs and skills your agents rely on. Primary CLI from `src/cli.ts`: `audit`, `validate`, `catalog`, `init`, `build-plugin`, `references` (plus `customize` / `hook` when using overlays). Commands dispatch through that entry file.
12
14
 
13
15
  Skeleton is **not** a runtime agent harness. It doesn't execute tools, enforce permissions, or manage memory. It checks whether the repo around those systems still holds together.
14
16
 
package/dist/cli.js CHANGED
@@ -15652,7 +15652,7 @@ var require_extend = __commonJS((exports, module) => {
15652
15652
  });
15653
15653
 
15654
15654
  // src/cli.ts
15655
- import { readFileSync as readFileSync24 } from "node:fs";
15655
+ import { readFileSync as readFileSync25 } from "node:fs";
15656
15656
  import process7 from "node:process";
15657
15657
 
15658
15658
  // src/audit/config/load.ts
@@ -30817,184 +30817,12 @@ function runBannedRule(ctx) {
30817
30817
  }
30818
30818
  var bannedRule = { id: "banned", run: runBannedRule };
30819
30819
 
30820
- // src/audit/rules/doc-meta.ts
30820
+ // src/audit/rules/code-fit.ts
30821
+ import { relative as relative8 } from "node:path";
30822
+
30823
+ // src/audit/core/code-fit.ts
30821
30824
  import { existsSync as existsSync13, readFileSync as readFileSync12 } from "node:fs";
30822
30825
  import { join as join12 } from "node:path";
30823
- function checkDocMetaBanner(relPath2, content3) {
30824
- if (DOC_META_RE.test(content3))
30825
- return null;
30826
- return issue("doc-meta", relPath2, "missing doc-meta comment (owner + last-reviewed)");
30827
- }
30828
- function checkStaleReview(input) {
30829
- const { relPath: relPath2, content: content3, today, staleDays } = input;
30830
- const reviewedStr = docMetaLastReviewed(content3);
30831
- if (!reviewedStr)
30832
- return null;
30833
- const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
30834
- if (Number.isNaN(reviewed.getTime()))
30835
- return null;
30836
- const ageDays = (today.getTime() - reviewed.getTime()) / 86400000;
30837
- if (ageDays <= staleDays)
30838
- return null;
30839
- return issue("doc-meta", relPath2, {
30840
- message: `doc-meta last-reviewed ${reviewedStr} exceeds re-read cadence (>${staleDays} days) — re-affirm the paper or bump after review`,
30841
- severity: "warning"
30842
- });
30843
- }
30844
- function checkGitFreshness(input) {
30845
- const { relPath: relPath2, content: content3, root: root2, lockedSkillSlugs } = input;
30846
- const reviewedStr = docMetaLastReviewed(content3);
30847
- if (!reviewedStr)
30848
- return null;
30849
- const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
30850
- if (Number.isNaN(reviewed.getTime()))
30851
- return null;
30852
- const slug2 = slugFromPath(relPath2, root2);
30853
- if (slug2 !== null && lockedSkillSlugs.has(slug2))
30854
- return null;
30855
- const gitDate = lastGitCommitDate(relPath2, root2);
30856
- if (!gitDate)
30857
- return null;
30858
- const committed = new Date(`${gitDate}T00:00:00Z`);
30859
- if (Number.isNaN(committed.getTime()))
30860
- return null;
30861
- if (committed.getTime() <= reviewed.getTime())
30862
- return null;
30863
- return issue("doc-meta", relPath2, {
30864
- message: `content changed after last-reviewed ${reviewedStr} (git: ${gitDate}) — review no longer covers latest edit; bump last-reviewed after review`,
30865
- severity: "warning"
30866
- });
30867
- }
30868
- function runDocMetaRule(ctx) {
30869
- const issues = [];
30870
- const today = new Date;
30871
- for (const relPath2 of ctx.docMetaPaths) {
30872
- const abs = join12(ctx.root, relPath2);
30873
- if (!existsSync13(abs))
30874
- continue;
30875
- const content3 = readFileSync12(abs, "utf8");
30876
- const banner = checkDocMetaBanner(relPath2, content3);
30877
- if (banner) {
30878
- issues.push(banner);
30879
- continue;
30880
- }
30881
- const stale = checkStaleReview({
30882
- relPath: relPath2,
30883
- content: content3,
30884
- today,
30885
- staleDays: ctx.config.daysUntilStale
30886
- });
30887
- if (stale)
30888
- issues.push(stale);
30889
- const git = checkGitFreshness({
30890
- relPath: relPath2,
30891
- content: content3,
30892
- root: ctx.root,
30893
- lockedSkillSlugs: ctx.lockedSkillSlugs
30894
- });
30895
- if (git)
30896
- issues.push(git);
30897
- }
30898
- return issues;
30899
- }
30900
- var docMetaRule = { id: "doc-meta", run: runDocMetaRule };
30901
-
30902
- // src/audit/rules/links.ts
30903
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "node:fs";
30904
- import { dirname as dirname7, resolve as resolve6 } from "node:path";
30905
- function resolveLink2(sourceFile, target) {
30906
- const withoutAnchor = target.split("#")[0]?.split("?")[0] ?? "";
30907
- if (!withoutAnchor)
30908
- return sourceFile;
30909
- return resolve6(dirname7(sourceFile), withoutAnchor);
30910
- }
30911
- function checkMissingSkill(input, relSource) {
30912
- if (!input.target.includes("/SKILL.md"))
30913
- return null;
30914
- const slug2 = SKILL_LINK_IN_TARGET_RE.exec(input.target)?.[1];
30915
- if (!(slug2 && !resolveSkillPath(input.ctx.skillIndex, input.ctx.root, slug2)))
30916
- return null;
30917
- return issue("links", relSource, {
30918
- message: `missing skill "${slug2}/SKILL.md"`,
30919
- link: input.linkLabel
30920
- });
30921
- }
30922
- function checkAgentFile(input, resolved, relSource) {
30923
- if (!((input.target.includes(".claude/agents/") || input.target.includes(".cursor/agents/")) && input.target.endsWith(".md"))) {
30924
- return null;
30925
- }
30926
- const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
30927
- if (existsSync14(agentPath))
30928
- return null;
30929
- return issue("links", relSource, { message: "missing agent file", link: input.linkLabel });
30930
- }
30931
- function checkBrokenPath(ctx) {
30932
- const { input, pathPart, resolved, relSource, relTarget } = ctx;
30933
- if (!(pathPart && !existsSync14(resolved)))
30934
- return null;
30935
- return issue("links", relSource, {
30936
- message: `broken link → ${relTarget}`,
30937
- link: input.linkLabel
30938
- });
30939
- }
30940
- function checkBrokenAnchor(ctx) {
30941
- const { input, anchor, resolved, relSource, relTarget } = ctx;
30942
- if (!(anchor && existsSync14(resolved)))
30943
- return null;
30944
- const targetContent = readFileSync13(resolved, "utf8");
30945
- const slugs = extractHeadingSlugs(targetContent, resolved);
30946
- const anchorSlug = slugifyAnchor(anchor);
30947
- if (slugs.has(anchorSlug))
30948
- return null;
30949
- return issue("links", relSource, {
30950
- message: `broken anchor → #${anchor} in ${relTarget}`,
30951
- link: input.linkLabel
30952
- });
30953
- }
30954
- function resolveTargetParts(sourceFile, target, root2) {
30955
- const relSource = relPath(sourceFile, root2);
30956
- const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
30957
- const pathPart = target.split("#")[0]?.split("?")[0] ?? "";
30958
- const resolved = resolveLink2(sourceFile, target);
30959
- const relTarget = relPath(resolved, root2);
30960
- return { relSource, anchor, pathPart, resolved, relTarget };
30961
- }
30962
- function validateTarget(input) {
30963
- const { ctx, sourceFile, target } = input;
30964
- if (isExternalLink(target) && !target.startsWith("#"))
30965
- return [];
30966
- if (isPlaceholderLink(target))
30967
- return [];
30968
- const parts = resolveTargetParts(sourceFile, target, ctx.root);
30969
- const missingSkill = checkMissingSkill(input, parts.relSource);
30970
- if (missingSkill)
30971
- return [missingSkill];
30972
- const agent = checkAgentFile(input, parts.resolved, parts.relSource);
30973
- if (agent)
30974
- return [agent];
30975
- const brokenPath = checkBrokenPath({ input, ...parts });
30976
- if (brokenPath)
30977
- return [brokenPath];
30978
- const brokenAnchor = checkBrokenAnchor({ input, ...parts });
30979
- return brokenAnchor ? [brokenAnchor] : [];
30980
- }
30981
- function runLinksRule(ctx) {
30982
- const issues = [];
30983
- for (const filePath of ctx.files) {
30984
- const content3 = readFileContent(filePath);
30985
- const links = extractLinksFromMarkdown(content3, filePath);
30986
- for (const { target, line } of links) {
30987
- const linkLabel = line ? `line ${line}` : target;
30988
- issues.push(...validateTarget({ ctx, sourceFile: filePath, target, linkLabel }));
30989
- }
30990
- }
30991
- return issues;
30992
- }
30993
- var linksRule = { id: "links", run: runLinksRule };
30994
-
30995
- // src/audit/rules/near-duplicate.ts
30996
- import { readFileSync as readFileSync14 } from "node:fs";
30997
- import { join as join13 } from "node:path";
30998
30826
 
30999
30827
  // src/audit/core/ssot-fit.ts
31000
30828
  var DEFAULT_SSOT_OVERLAP_MIN = 0.35;
@@ -31217,7 +31045,421 @@ function evaluateSsotFit(files, options = {}) {
31217
31045
  return issues;
31218
31046
  }
31219
31047
 
31048
+ // src/audit/core/code-fit.ts
31049
+ var DEFAULT_CODE_FIT_OVERLAP_MIN = 0.03;
31050
+ var DEFAULT_CODE_FIT_SURFACE_CAP = 25;
31051
+ var CODE_FIT_RE = /<!--\s*code-fit:\s*([^>]*?)-->/gi;
31052
+ function parseCodeFitMarkers(content3) {
31053
+ const withoutCode = content3.replace(/```[\s\S]*?```/g, `
31054
+ `).replace(/`[^`\n]+`/g, " ");
31055
+ const out = [];
31056
+ for (const match of withoutCode.matchAll(CODE_FIT_RE)) {
31057
+ const body = (match[1] ?? "").trim();
31058
+ const parsed = parseMarkerBody(body);
31059
+ if (parsed)
31060
+ out.push(parsed);
31061
+ }
31062
+ return out;
31063
+ }
31064
+ function parseMarkerBody(body) {
31065
+ const targetsMatch = /\btargets\s*=\s*([^\s]+)/i.exec(body);
31066
+ if (!targetsMatch?.[1])
31067
+ return null;
31068
+ const targets = targetsMatch[1].split(",").map((t) => t.trim()).filter(Boolean);
31069
+ if (targets.length === 0)
31070
+ return null;
31071
+ const surfaceMatch = /\bsurface\s*=\s*([^\s]+)/i.exec(body);
31072
+ const surface = surfaceMatch?.[1] ? surfaceMatch[1].split(",").map((s) => s.trim()).filter(Boolean) : null;
31073
+ return { targets, surface, raw: body };
31074
+ }
31075
+ function stripCodeNoise(source) {
31076
+ return source.replace(/\/\*[\s\S]*?\*\//g, `
31077
+ `).replace(/\/\/[^\n]*/g, `
31078
+ `).replace(/`(?:\\.|[^`\\])*`/g, " ").replace(/'(?:\\.|[^'\\])*'/g, " ").replace(/"(?:\\.|[^"\\])*"/g, " ");
31079
+ }
31080
+ function extractPublicSurface(source) {
31081
+ const names = new Set;
31082
+ for (const m of source.matchAll(/\bcase\s+["']([^"']+)["']\s*:/g)) {
31083
+ if (m[1])
31084
+ names.add(m[1]);
31085
+ }
31086
+ for (const m of source.matchAll(/\bexport\s+(?:async\s+)?(?:function|class|const|let|var|type|interface|enum)\s+([A-Za-z_$][\w$]*)/g)) {
31087
+ if (m[1])
31088
+ names.add(m[1]);
31089
+ }
31090
+ for (const m of source.matchAll(/\bexport\s+default\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/g)) {
31091
+ if (m[1])
31092
+ names.add(m[1]);
31093
+ }
31094
+ for (const m of source.matchAll(/\bexport\s+default\s+class\s+([A-Za-z_$][\w$]*)/g)) {
31095
+ if (m[1])
31096
+ names.add(m[1]);
31097
+ }
31098
+ collectExportListNames(source, names);
31099
+ return [...names].sort();
31100
+ }
31101
+ function collectExportListNames(source, names) {
31102
+ for (const m of source.matchAll(/\bexport\s*\{([^}]+)\}/g)) {
31103
+ const inner = m[1] ?? "";
31104
+ for (const part of inner.split(",")) {
31105
+ addExportListPart(part.trim(), names);
31106
+ }
31107
+ }
31108
+ }
31109
+ function addExportListPart(cleaned, names) {
31110
+ if (!cleaned || cleaned === "type" || cleaned === "typeof")
31111
+ return;
31112
+ const asMatch = /^([\w$]+)\s+as\s+([\w$]+)$/.exec(cleaned);
31113
+ if (asMatch?.[2]) {
31114
+ names.add(asMatch[2]);
31115
+ return;
31116
+ }
31117
+ const typeAs = /^type\s+([\w$]+)(?:\s+as\s+([\w$]+))?$/.exec(cleaned);
31118
+ if (typeAs?.[1]) {
31119
+ names.add(typeAs[2] ?? typeAs[1]);
31120
+ return;
31121
+ }
31122
+ const id = /^([\w$]+)$/.exec(cleaned);
31123
+ if (id?.[1])
31124
+ names.add(id[1]);
31125
+ }
31126
+ function codeIdentifiers(source) {
31127
+ const stripped = stripCodeNoise(source);
31128
+ return uniqueContentTokens(stripped.replace(/[^A-Za-z0-9_$]+/g, " ").replace(/_/g, " "));
31129
+ }
31130
+ function identifierOverlap(docContent, codeSource) {
31131
+ const codeIds = new Set(codeIdentifiers(codeSource));
31132
+ if (codeIds.size === 0)
31133
+ return 1;
31134
+ const docToks = uniqueContentTokens(docContent);
31135
+ if (docToks.length === 0)
31136
+ return 0;
31137
+ let hit = 0;
31138
+ for (const t of docToks) {
31139
+ if (codeIds.has(t))
31140
+ hit++;
31141
+ }
31142
+ return hit / docToks.length;
31143
+ }
31144
+ function nameInDoc(name, docContent) {
31145
+ const docToks = new Set(contentTokens(docContent));
31146
+ const parts = uniqueContentTokens(name.replace(/_/g, " "));
31147
+ if (parts.length === 0) {
31148
+ return docToks.has(name.toLowerCase());
31149
+ }
31150
+ const asToken = contentTokens(name);
31151
+ if (asToken.some((t) => docToks.has(t)))
31152
+ return true;
31153
+ return parts.every((p) => docToks.has(p));
31154
+ }
31155
+ function nameExistsInTarget(name, autoSurface, source) {
31156
+ if (autoSurface.includes(name))
31157
+ return true;
31158
+ const ids = new Set(extractPublicSurface(source));
31159
+ if (ids.has(name))
31160
+ return true;
31161
+ const re = new RegExp(`\\b${escapeRegExp(name)}\\b`);
31162
+ return re.test(source);
31163
+ }
31164
+ function escapeRegExp(s) {
31165
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31166
+ }
31167
+ function pushCoverageGaps(issues, input, effective) {
31168
+ if (effective.length === 0)
31169
+ return;
31170
+ const missing = effective.filter((n) => !nameInDoc(n, input.docContent));
31171
+ if (missing.length === 0)
31172
+ return;
31173
+ issues.push({
31174
+ path: input.docPath,
31175
+ message: `code-fit coverage: doc does not mention ${missing.map((m) => `"${m}"`).join(", ")} (from ${input.target})`,
31176
+ link: input.target
31177
+ });
31178
+ }
31179
+ function pushLexicalGap(issues, input, source) {
31180
+ const overlapMin = input.options.overlapMin ?? DEFAULT_CODE_FIT_OVERLAP_MIN;
31181
+ const overlap = identifierOverlap(input.docContent, source);
31182
+ if (overlap + 0.000000001 >= overlapMin)
31183
+ return;
31184
+ issues.push({
31185
+ path: input.docPath,
31186
+ message: `code-fit lexical overlap ${(overlap * 100).toFixed(0)}% < ${(overlapMin * 100).toFixed(0)}% vs ${input.target}`,
31187
+ link: input.target
31188
+ });
31189
+ }
31190
+ function evaluateTarget(input) {
31191
+ const issues = [];
31192
+ const abs = join12(input.options.root, input.target);
31193
+ if (!existsSync13(abs)) {
31194
+ issues.push({
31195
+ path: input.docPath,
31196
+ message: `code-fit target missing: ${input.target}`,
31197
+ link: input.target
31198
+ });
31199
+ return issues;
31200
+ }
31201
+ const source = readFileSync12(abs, "utf8");
31202
+ const auto = extractPublicSurface(source);
31203
+ const cap = input.options.surfaceCap ?? DEFAULT_CODE_FIT_SURFACE_CAP;
31204
+ if (input.surfaceOverride === null && auto.length > cap) {
31205
+ issues.push({
31206
+ path: input.docPath,
31207
+ message: `code-fit auto-surface has ${auto.length} names (cap ${cap}); add surface=… to the marker for ${input.target}`,
31208
+ link: input.target
31209
+ });
31210
+ return issues;
31211
+ }
31212
+ if (input.surfaceOverride !== null) {
31213
+ for (const name of input.surfaceOverride) {
31214
+ if (!nameExistsInTarget(name, auto, source)) {
31215
+ issues.push({
31216
+ path: input.docPath,
31217
+ message: `code-fit surface name "${name}" not found in ${input.target}`,
31218
+ link: input.target
31219
+ });
31220
+ }
31221
+ }
31222
+ }
31223
+ const effective = input.surfaceOverride !== null ? input.surfaceOverride : auto;
31224
+ pushCoverageGaps(issues, input, effective);
31225
+ pushLexicalGap(issues, input, source);
31226
+ return issues;
31227
+ }
31228
+ function evaluateCodeFitDoc(docPath, docContent, options) {
31229
+ const markers = parseCodeFitMarkers(docContent);
31230
+ if (markers.length === 0)
31231
+ return [];
31232
+ const issues = [];
31233
+ for (const marker of markers) {
31234
+ if (marker.targets.length === 0) {
31235
+ issues.push({
31236
+ path: docPath,
31237
+ message: "code-fit marker missing targets="
31238
+ });
31239
+ continue;
31240
+ }
31241
+ for (const target of marker.targets) {
31242
+ issues.push(...evaluateTarget({
31243
+ docPath,
31244
+ docContent,
31245
+ target,
31246
+ surfaceOverride: marker.surface,
31247
+ options
31248
+ }));
31249
+ }
31250
+ }
31251
+ return issues;
31252
+ }
31253
+
31254
+ // src/audit/rules/code-fit.ts
31255
+ function runCodeFitRule(ctx) {
31256
+ const overlapMin = ctx.config.docsLint?.codeFitOverlapMin ?? DEFAULT_CODE_FIT_OVERLAP_MIN;
31257
+ const surfaceCap = ctx.config.docsLint?.codeFitSurfaceCap ?? DEFAULT_CODE_FIT_SURFACE_CAP;
31258
+ const options = { root: ctx.root, overlapMin, surfaceCap };
31259
+ const corpus = collectScanFiles(ctx.config, ctx.root, ctx.skillIndex);
31260
+ const issues = [];
31261
+ for (const abs of corpus) {
31262
+ const content3 = readFileContent(abs);
31263
+ if (!parseCodeFitMarkers(content3).length)
31264
+ continue;
31265
+ const rel = normalizeRelPath(relative8(ctx.root, abs));
31266
+ for (const fit of evaluateCodeFitDoc(rel, content3, options)) {
31267
+ issues.push(issue("code-fit", fit.path, {
31268
+ message: fit.message,
31269
+ link: fit.link,
31270
+ severity: "error"
31271
+ }));
31272
+ }
31273
+ }
31274
+ return issues;
31275
+ }
31276
+ var codeFitRule = {
31277
+ id: "code-fit",
31278
+ alwaysRun: true,
31279
+ run: runCodeFitRule
31280
+ };
31281
+
31282
+ // src/audit/rules/doc-meta.ts
31283
+ import { existsSync as existsSync14, readFileSync as readFileSync13 } from "node:fs";
31284
+ import { join as join13 } from "node:path";
31285
+ function checkDocMetaBanner(relPath2, content3) {
31286
+ if (DOC_META_RE.test(content3))
31287
+ return null;
31288
+ return issue("doc-meta", relPath2, "missing doc-meta comment (owner + last-reviewed)");
31289
+ }
31290
+ function checkStaleReview(input) {
31291
+ const { relPath: relPath2, content: content3, today, staleDays } = input;
31292
+ const reviewedStr = docMetaLastReviewed(content3);
31293
+ if (!reviewedStr)
31294
+ return null;
31295
+ const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
31296
+ if (Number.isNaN(reviewed.getTime()))
31297
+ return null;
31298
+ const ageDays = (today.getTime() - reviewed.getTime()) / 86400000;
31299
+ if (ageDays <= staleDays)
31300
+ return null;
31301
+ return issue("doc-meta", relPath2, {
31302
+ message: `doc-meta last-reviewed ${reviewedStr} exceeds re-read cadence (>${staleDays} days) — re-affirm the paper or bump after review`,
31303
+ severity: "warning"
31304
+ });
31305
+ }
31306
+ function gitFreshnessMessage(reviewedStr, gitDate) {
31307
+ return `content changed after last-reviewed ${reviewedStr} (git: ${gitDate}) — REQUIRED: re-read the entire document, then bump last-reviewed only if the content is still correct; do not change the date alone`;
31308
+ }
31309
+ function checkGitFreshness(input) {
31310
+ const { relPath: relPath2, content: content3, root: root2, lockedSkillSlugs } = input;
31311
+ const reviewedStr = docMetaLastReviewed(content3);
31312
+ if (!reviewedStr)
31313
+ return null;
31314
+ const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
31315
+ if (Number.isNaN(reviewed.getTime()))
31316
+ return null;
31317
+ const slug2 = slugFromPath(relPath2, root2);
31318
+ if (slug2 !== null && lockedSkillSlugs.has(slug2))
31319
+ return null;
31320
+ const gitDate = lastGitCommitDate(relPath2, root2);
31321
+ if (!gitDate)
31322
+ return null;
31323
+ const committed = new Date(`${gitDate}T00:00:00Z`);
31324
+ if (Number.isNaN(committed.getTime()))
31325
+ return null;
31326
+ if (committed.getTime() <= reviewed.getTime())
31327
+ return null;
31328
+ return issue("doc-meta", relPath2, {
31329
+ message: gitFreshnessMessage(reviewedStr, gitDate),
31330
+ severity: "warning"
31331
+ });
31332
+ }
31333
+ function runDocMetaRule(ctx) {
31334
+ const issues = [];
31335
+ const today = new Date;
31336
+ for (const relPath2 of ctx.docMetaPaths) {
31337
+ const abs = join13(ctx.root, relPath2);
31338
+ if (!existsSync14(abs))
31339
+ continue;
31340
+ const content3 = readFileSync13(abs, "utf8");
31341
+ const banner = checkDocMetaBanner(relPath2, content3);
31342
+ if (banner) {
31343
+ issues.push(banner);
31344
+ continue;
31345
+ }
31346
+ const stale = checkStaleReview({
31347
+ relPath: relPath2,
31348
+ content: content3,
31349
+ today,
31350
+ staleDays: ctx.config.daysUntilStale
31351
+ });
31352
+ if (stale)
31353
+ issues.push(stale);
31354
+ const git = checkGitFreshness({
31355
+ relPath: relPath2,
31356
+ content: content3,
31357
+ root: ctx.root,
31358
+ lockedSkillSlugs: ctx.lockedSkillSlugs
31359
+ });
31360
+ if (git)
31361
+ issues.push(git);
31362
+ }
31363
+ return issues;
31364
+ }
31365
+ var docMetaRule = { id: "doc-meta", run: runDocMetaRule };
31366
+
31367
+ // src/audit/rules/links.ts
31368
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "node:fs";
31369
+ import { dirname as dirname7, resolve as resolve6 } from "node:path";
31370
+ function resolveLink2(sourceFile, target) {
31371
+ const withoutAnchor = target.split("#")[0]?.split("?")[0] ?? "";
31372
+ if (!withoutAnchor)
31373
+ return sourceFile;
31374
+ return resolve6(dirname7(sourceFile), withoutAnchor);
31375
+ }
31376
+ function checkMissingSkill(input, relSource) {
31377
+ if (!input.target.includes("/SKILL.md"))
31378
+ return null;
31379
+ const slug2 = SKILL_LINK_IN_TARGET_RE.exec(input.target)?.[1];
31380
+ if (!(slug2 && !resolveSkillPath(input.ctx.skillIndex, input.ctx.root, slug2)))
31381
+ return null;
31382
+ return issue("links", relSource, {
31383
+ message: `missing skill "${slug2}/SKILL.md"`,
31384
+ link: input.linkLabel
31385
+ });
31386
+ }
31387
+ function checkAgentFile(input, resolved, relSource) {
31388
+ if (!((input.target.includes(".claude/agents/") || input.target.includes(".cursor/agents/")) && input.target.endsWith(".md"))) {
31389
+ return null;
31390
+ }
31391
+ const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
31392
+ if (existsSync15(agentPath))
31393
+ return null;
31394
+ return issue("links", relSource, { message: "missing agent file", link: input.linkLabel });
31395
+ }
31396
+ function checkBrokenPath(ctx) {
31397
+ const { input, pathPart, resolved, relSource, relTarget } = ctx;
31398
+ if (!(pathPart && !existsSync15(resolved)))
31399
+ return null;
31400
+ return issue("links", relSource, {
31401
+ message: `broken link → ${relTarget}`,
31402
+ link: input.linkLabel
31403
+ });
31404
+ }
31405
+ function checkBrokenAnchor(ctx) {
31406
+ const { input, anchor, resolved, relSource, relTarget } = ctx;
31407
+ if (!(anchor && existsSync15(resolved)))
31408
+ return null;
31409
+ const targetContent = readFileSync14(resolved, "utf8");
31410
+ const slugs = extractHeadingSlugs(targetContent, resolved);
31411
+ const anchorSlug = slugifyAnchor(anchor);
31412
+ if (slugs.has(anchorSlug))
31413
+ return null;
31414
+ return issue("links", relSource, {
31415
+ message: `broken anchor → #${anchor} in ${relTarget}`,
31416
+ link: input.linkLabel
31417
+ });
31418
+ }
31419
+ function resolveTargetParts(sourceFile, target, root2) {
31420
+ const relSource = relPath(sourceFile, root2);
31421
+ const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
31422
+ const pathPart = target.split("#")[0]?.split("?")[0] ?? "";
31423
+ const resolved = resolveLink2(sourceFile, target);
31424
+ const relTarget = relPath(resolved, root2);
31425
+ return { relSource, anchor, pathPart, resolved, relTarget };
31426
+ }
31427
+ function validateTarget(input) {
31428
+ const { ctx, sourceFile, target } = input;
31429
+ if (isExternalLink(target) && !target.startsWith("#"))
31430
+ return [];
31431
+ if (isPlaceholderLink(target))
31432
+ return [];
31433
+ const parts = resolveTargetParts(sourceFile, target, ctx.root);
31434
+ const missingSkill = checkMissingSkill(input, parts.relSource);
31435
+ if (missingSkill)
31436
+ return [missingSkill];
31437
+ const agent = checkAgentFile(input, parts.resolved, parts.relSource);
31438
+ if (agent)
31439
+ return [agent];
31440
+ const brokenPath = checkBrokenPath({ input, ...parts });
31441
+ if (brokenPath)
31442
+ return [brokenPath];
31443
+ const brokenAnchor = checkBrokenAnchor({ input, ...parts });
31444
+ return brokenAnchor ? [brokenAnchor] : [];
31445
+ }
31446
+ function runLinksRule(ctx) {
31447
+ const issues = [];
31448
+ for (const filePath of ctx.files) {
31449
+ const content3 = readFileContent(filePath);
31450
+ const links = extractLinksFromMarkdown(content3, filePath);
31451
+ for (const { target, line } of links) {
31452
+ const linkLabel = line ? `line ${line}` : target;
31453
+ issues.push(...validateTarget({ ctx, sourceFile: filePath, target, linkLabel }));
31454
+ }
31455
+ }
31456
+ return issues;
31457
+ }
31458
+ var linksRule = { id: "links", run: runLinksRule };
31459
+
31220
31460
  // src/audit/rules/near-duplicate.ts
31461
+ import { readFileSync as readFileSync15 } from "node:fs";
31462
+ import { join as join14 } from "node:path";
31221
31463
  var DEFAULT_THRESHOLD = 0.72;
31222
31464
  var SHINGLE_N = 3;
31223
31465
  function tokenize2(text5) {
@@ -31282,7 +31524,7 @@ function runNearDuplicateRule(ctx) {
31282
31524
  const ignored = ignoredPairSet(ctx);
31283
31525
  const entries = eligibleEntries(ctx);
31284
31526
  const fingerprints = entries.map((e) => {
31285
- const content3 = readFileSync14(join13(ctx.root, e.path), "utf8");
31527
+ const content3 = readFileSync15(join14(ctx.root, e.path), "utf8");
31286
31528
  const tokens = tokenize2(bodyWithoutSsotNoise(content3));
31287
31529
  return {
31288
31530
  path: e.path,
@@ -31406,16 +31648,16 @@ function runScanRootsRule(ctx) {
31406
31648
  var scanRootsRule = { id: "scan-roots", run: runScanRootsRule };
31407
31649
 
31408
31650
  // src/audit/rules/skill-index.ts
31409
- import { existsSync as existsSync15, readdirSync as readdirSync5, readFileSync as readFileSync15 } from "node:fs";
31410
- import { join as join14, relative as relative8 } from "node:path";
31651
+ import { existsSync as existsSync16, readdirSync as readdirSync5, readFileSync as readFileSync16 } from "node:fs";
31652
+ import { join as join15, relative as relative9 } from "node:path";
31411
31653
  function walkSkillMarkdown(dir) {
31412
31654
  const files = [];
31413
- if (!existsSync15(dir))
31655
+ if (!existsSync16(dir))
31414
31656
  return files;
31415
31657
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
31416
31658
  if (entry.name.startsWith("."))
31417
31659
  continue;
31418
- const fullPath = join14(dir, entry.name);
31660
+ const fullPath = join15(dir, entry.name);
31419
31661
  if (entry.isDirectory()) {
31420
31662
  files.push(...walkSkillMarkdown(fullPath));
31421
31663
  continue;
@@ -31441,8 +31683,8 @@ function parseReadmeTaxonomySlugs(content3) {
31441
31683
  }
31442
31684
  function scanFileForSkillLinks(ctx, filePath, index2) {
31443
31685
  const issues = [];
31444
- const rel = relative8(ctx.root, filePath).replace(/\\/g, "/");
31445
- const content3 = readFileSync15(filePath, "utf8");
31686
+ const rel = relative9(ctx.root, filePath).replace(/\\/g, "/");
31687
+ const content3 = readFileSync16(filePath, "utf8");
31446
31688
  if (isGeneratedReference(content3))
31447
31689
  return issues;
31448
31690
  for (const match of content3.matchAll(SKILL_LINK_RE)) {
@@ -31457,14 +31699,14 @@ function scanFileForSkillLinks(ctx, filePath, index2) {
31457
31699
  }
31458
31700
  function taxonomyIssuesForReadme(input) {
31459
31701
  const { ctx, index: index2, skillRoot, diskSlugs, nonPublic } = input;
31460
- const readmePath = join14(ctx.root, skillRoot.relPath, "README.md");
31461
- if (!existsSync15(readmePath))
31702
+ const readmePath = join15(ctx.root, skillRoot.relPath, "README.md");
31703
+ if (!existsSync16(readmePath))
31462
31704
  return [];
31463
- const readme = readFileSync15(readmePath, "utf8");
31705
+ const readme = readFileSync16(readmePath, "utf8");
31464
31706
  if (!readme.includes("## Taxonomy"))
31465
31707
  return [];
31466
31708
  const taxonomySlugs = parseReadmeTaxonomySlugs(readme);
31467
- const nestedSlugs = diskSlugs.filter((slug2) => existsSync15(join14(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
31709
+ const nestedSlugs = diskSlugs.filter((slug2) => existsSync16(join15(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
31468
31710
  const foreign = new Set(index2.foreignSlugs);
31469
31711
  const publicSlugs = nestedSlugs.filter((slug2) => !(nonPublic.has(slug2) || foreign.has(slug2)));
31470
31712
  const relReadme = `${skillRoot.relPath}/README.md`;
@@ -31497,10 +31739,10 @@ function slugsForRoot(skillRoot, index2, owned) {
31497
31739
  function auditSkillRoot(input) {
31498
31740
  const { ctx, index: index2, skillRoot, owned } = input;
31499
31741
  const issues = [];
31500
- const base = skillRoot.kind === "nested" ? join14(ctx.root, skillRoot.relPath) : ctx.root;
31742
+ const base = skillRoot.kind === "nested" ? join15(ctx.root, skillRoot.relPath) : ctx.root;
31501
31743
  for (const slug2 of slugsForRoot(skillRoot, index2, owned)) {
31502
- const skillDir = join14(base, slug2);
31503
- if (!existsSync15(skillDir))
31744
+ const skillDir = join15(base, slug2);
31745
+ if (!existsSync16(skillDir))
31504
31746
  continue;
31505
31747
  for (const skillMd of walkSkillMarkdown(skillDir)) {
31506
31748
  issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
@@ -31550,8 +31792,8 @@ function runSsotRule(ctx) {
31550
31792
  var ssotRule = { id: "ssot", run: runSsotRule };
31551
31793
 
31552
31794
  // src/audit/rules/ssot-summary.ts
31553
- import { readFileSync as readFileSync16 } from "node:fs";
31554
- import { join as join15 } from "node:path";
31795
+ import { readFileSync as readFileSync17 } from "node:fs";
31796
+ import { join as join16 } from "node:path";
31555
31797
  function runSsotSummaryRule(ctx) {
31556
31798
  const overlapMin = ctx.config.docsLint?.ssotOverlapMin ?? DEFAULT_SSOT_OVERLAP_MIN;
31557
31799
  const margin = ctx.config.docsLint?.ssotBetterMatchMargin ?? DEFAULT_BETTER_MATCH_MARGIN;
@@ -31559,7 +31801,7 @@ function runSsotSummaryRule(ctx) {
31559
31801
  const files = ctx.ssotEntries.map((entry) => ({
31560
31802
  path: entry.path,
31561
31803
  summary: entry.summary,
31562
- content: readFileSync16(join15(ctx.root, entry.path), "utf8")
31804
+ content: readFileSync17(join16(ctx.root, entry.path), "utf8")
31563
31805
  }));
31564
31806
  return evaluateSsotFit(files, {
31565
31807
  overlapMin,
@@ -31582,6 +31824,7 @@ var docsRules = [
31582
31824
  { ...coverageGapsRule, global: true },
31583
31825
  linksRule,
31584
31826
  docMetaRule,
31827
+ codeFitRule,
31585
31828
  { ...bannedRule, global: true },
31586
31829
  prosePolicyRule
31587
31830
  ];
@@ -31720,8 +31963,8 @@ function shouldRunRule(rule, options, pathScoped) {
31720
31963
  if (options.globalOnly)
31721
31964
  return Boolean(rule.global);
31722
31965
  if (options.pathScopedOnly)
31723
- return !rule.global;
31724
- if (pathScoped && rule.global)
31966
+ return !rule.global || Boolean(rule.alwaysRun);
31967
+ if (pathScoped && rule.global && !rule.alwaysRun)
31725
31968
  return false;
31726
31969
  return true;
31727
31970
  }
@@ -31779,8 +32022,8 @@ async function runAudit(options) {
31779
32022
  }
31780
32023
 
31781
32024
  // src/catalog.ts
31782
- import { existsSync as existsSync16, mkdirSync, readFileSync as readFileSync17, writeFileSync as writeFileSync2 } from "node:fs";
31783
- import { dirname as dirname8, join as join16 } from "node:path";
32025
+ import { existsSync as existsSync17, mkdirSync, readFileSync as readFileSync18, writeFileSync as writeFileSync2 } from "node:fs";
32026
+ import { dirname as dirname8, join as join17 } from "node:path";
31784
32027
  function renderCatalog(entries) {
31785
32028
  const lines = [
31786
32029
  "# Skeleton catalog",
@@ -31809,17 +32052,17 @@ function buildCatalogContent(root2) {
31809
32052
  }
31810
32053
  function checkCatalog(root2) {
31811
32054
  const { content: expected, entries } = buildCatalogContent(root2);
31812
- const abs = join16(root2, CATALOG_REL_PATH);
31813
- if (!existsSync16(abs)) {
32055
+ const abs = join17(root2, CATALOG_REL_PATH);
32056
+ if (!existsSync17(abs)) {
31814
32057
  return { ok: false, missing: true, stale: false, expected, actual: null, entries };
31815
32058
  }
31816
- const actual = readFileSync17(abs, "utf8");
32059
+ const actual = readFileSync18(abs, "utf8");
31817
32060
  const stale = actual !== expected;
31818
32061
  return { ok: !stale, missing: false, stale, expected, actual, entries };
31819
32062
  }
31820
32063
  function writeCatalog(root2) {
31821
32064
  const { content: content3, entries } = buildCatalogContent(root2);
31822
- const abs = join16(root2, CATALOG_REL_PATH);
32065
+ const abs = join17(root2, CATALOG_REL_PATH);
31823
32066
  mkdirSync(dirname8(abs), { recursive: true });
31824
32067
  writeFileSync2(abs, content3, "utf8");
31825
32068
  return { path: normalizeRelPath(CATALOG_REL_PATH), entries };
@@ -31845,20 +32088,20 @@ function runCatalogCli(options = {}) {
31845
32088
  }
31846
32089
 
31847
32090
  // src/customize/resolve.ts
31848
- import { existsSync as existsSync17, readFileSync as readFileSync18 } from "node:fs";
31849
- import { basename as basename3, join as join17, relative as relative9 } from "node:path";
32091
+ import { existsSync as existsSync18, readFileSync as readFileSync19 } from "node:fs";
32092
+ import { basename as basename3, join as join18, relative as relative10 } from "node:path";
31850
32093
  function customizeDir(root2) {
31851
- return join17(root2, REGISTRY_DIR_REL, "customize");
32094
+ return join18(root2, REGISTRY_DIR_REL, "customize");
31852
32095
  }
31853
32096
  function customizePathForSlug(root2, slug2) {
31854
- return join17(customizeDir(root2), `${slug2}.md`);
32097
+ return join18(customizeDir(root2), `${slug2}.md`);
31855
32098
  }
31856
32099
  function resolveSlugFile(root2, slug2) {
31857
32100
  const direct = customizePathForSlug(root2, slug2);
31858
- if (existsSync17(direct)) {
32101
+ if (existsSync18(direct)) {
31859
32102
  return {
31860
- content: readFileSync18(direct, "utf8"),
31861
- path: normalizeRelPath(relative9(root2, direct))
32103
+ content: readFileSync19(direct, "utf8"),
32104
+ path: normalizeRelPath(relative10(root2, direct))
31862
32105
  };
31863
32106
  }
31864
32107
  return { content: null, path: null };
@@ -31879,11 +32122,11 @@ function readAlwaysInclude(root2, basenames, skipBasename) {
31879
32122
  const file = basename3(name);
31880
32123
  if (skipBasename && file === skipBasename)
31881
32124
  continue;
31882
- const abs = join17(dir, file);
31883
- if (!existsSync17(abs))
32125
+ const abs = join18(dir, file);
32126
+ if (!existsSync18(abs))
31884
32127
  continue;
31885
- parts.push(readFileSync18(abs, "utf8").trimEnd());
31886
- paths.push(normalizeRelPath(relative9(root2, abs)));
32128
+ parts.push(readFileSync19(abs, "utf8").trimEnd());
32129
+ paths.push(normalizeRelPath(relative10(root2, abs)));
31887
32130
  }
31888
32131
  return { parts, paths };
31889
32132
  }
@@ -32006,39 +32249,39 @@ Customize override for /${slug2} (from ${from}):
32006
32249
 
32007
32250
  // src/init/init.ts
32008
32251
  import { spawnSync as spawnSync2 } from "node:child_process";
32009
- import { copyFileSync, existsSync as existsSync21, mkdirSync as mkdirSync3, readFileSync as readFileSync20 } from "node:fs";
32010
- import { join as join21 } from "node:path";
32252
+ import { copyFileSync, existsSync as existsSync22, mkdirSync as mkdirSync3, readFileSync as readFileSync21 } from "node:fs";
32253
+ import { join as join22 } from "node:path";
32011
32254
  import process4 from "node:process";
32012
32255
 
32013
32256
  // src/init/merge-hooks.ts
32014
- import { existsSync as existsSync20, mkdirSync as mkdirSync2, readFileSync as readFileSync19, writeFileSync as writeFileSync3 } from "node:fs";
32015
- import { dirname as dirname11, join as join20 } from "node:path";
32257
+ import { existsSync as existsSync21, mkdirSync as mkdirSync2, readFileSync as readFileSync20, writeFileSync as writeFileSync3 } from "node:fs";
32258
+ import { dirname as dirname11, join as join21 } from "node:path";
32016
32259
 
32017
32260
  // src/init/package-paths.ts
32018
- import { existsSync as existsSync18 } from "node:fs";
32019
- import { dirname as dirname9, join as join18 } from "node:path";
32261
+ import { existsSync as existsSync19 } from "node:fs";
32262
+ import { dirname as dirname9, join as join19 } from "node:path";
32020
32263
  import { fileURLToPath as fileURLToPath5 } from "node:url";
32021
32264
  var MODULE_DIR = dirname9(fileURLToPath5(import.meta.url));
32022
- var PACKAGE_ROOT_CANDIDATES = [join18(MODULE_DIR, "../.."), join18(MODULE_DIR, "..")];
32265
+ var PACKAGE_ROOT_CANDIDATES = [join19(MODULE_DIR, "../.."), join19(MODULE_DIR, "..")];
32023
32266
  function resolvePackageRoot() {
32024
32267
  for (const candidate of PACKAGE_ROOT_CANDIDATES) {
32025
- if (existsSync18(join18(candidate, "package.json")))
32268
+ if (existsSync19(join19(candidate, "package.json")))
32026
32269
  return candidate;
32027
32270
  }
32028
32271
  throw new Error("Could not resolve @csark0812/skeleton package root");
32029
32272
  }
32030
32273
  function resolveTemplatesDir() {
32031
- const dir = join18(resolvePackageRoot(), "templates/skeleton-init");
32032
- if (!existsSync18(dir)) {
32274
+ const dir = join19(resolvePackageRoot(), "templates/skeleton-init");
32275
+ if (!existsSync19(dir)) {
32033
32276
  throw new Error("Missing templates/skeleton-init in package");
32034
32277
  }
32035
32278
  return dir;
32036
32279
  }
32037
32280
 
32038
32281
  // src/init/resolve-hook-command.ts
32039
- import { existsSync as existsSync19, realpathSync as realpathSync6 } from "node:fs";
32282
+ import { existsSync as existsSync20, realpathSync as realpathSync6 } from "node:fs";
32040
32283
  import { createRequire as createRequire3 } from "node:module";
32041
- import { dirname as dirname10, join as join19, relative as relative10, resolve as resolve7 } from "node:path";
32284
+ import { dirname as dirname10, join as join20, relative as relative11, resolve as resolve7 } from "node:path";
32042
32285
  var PACKAGE_NAME = "@csark0812/skeleton";
32043
32286
  var CLI_DIST = "dist/cli.js";
32044
32287
  var PACKAGE_ROOT = resolvePackageRoot();
@@ -32052,12 +32295,12 @@ function safeRealpath2(path2) {
32052
32295
  }
32053
32296
  }
32054
32297
  function toRepoRelative(cwd, absPath) {
32055
- const rel = relative10(safeRealpath2(cwd), safeRealpath2(absPath)).replace(/\\/g, "/");
32298
+ const rel = relative11(safeRealpath2(cwd), safeRealpath2(absPath)).replace(/\\/g, "/");
32056
32299
  return rel.startsWith("..") ? absPath.replace(/\\/g, "/") : rel;
32057
32300
  }
32058
32301
  function tryResolvePublishedCli(cwd) {
32059
32302
  try {
32060
- const req = createRequire3(join19(cwd, "package.json"));
32303
+ const req = createRequire3(join20(cwd, "package.json"));
32061
32304
  return req.resolve(`${PACKAGE_NAME}/${CLI_DIST}`);
32062
32305
  } catch {
32063
32306
  return null;
@@ -32066,8 +32309,8 @@ function tryResolvePublishedCli(cwd) {
32066
32309
  function walkNodeModulesCli(cwd) {
32067
32310
  let dir = cwd;
32068
32311
  while (true) {
32069
- const candidate = join19(dir, "node_modules", PACKAGE_NAME, CLI_DIST);
32070
- if (existsSync19(candidate))
32312
+ const candidate = join20(dir, "node_modules", PACKAGE_NAME, CLI_DIST);
32313
+ if (existsSync20(candidate))
32071
32314
  return candidate;
32072
32315
  const parent = dirname10(dir);
32073
32316
  if (parent === dir)
@@ -32077,7 +32320,7 @@ function walkNodeModulesCli(cwd) {
32077
32320
  return null;
32078
32321
  }
32079
32322
  function isInsidePackageRoot(cwd) {
32080
- const rel = relative10(PACKAGE_ROOT, resolve7(cwd)).replace(/\\/g, "/");
32323
+ const rel = relative11(PACKAGE_ROOT, resolve7(cwd)).replace(/\\/g, "/");
32081
32324
  return rel === "" || !(rel.startsWith("..") || rel.startsWith("/"));
32082
32325
  }
32083
32326
  function nodeCliHookCommand(cliPath) {
@@ -32108,14 +32351,14 @@ function identityKey(platform, event, matcher) {
32108
32351
  return `skeleton:customize:${platform}:${event}:${matcher}`;
32109
32352
  }
32110
32353
  function loadFragment(name, hookCommand) {
32111
- const raw = readFileSync19(join20(TEMPLATES_DIR, name), "utf8");
32354
+ const raw = readFileSync20(join21(TEMPLATES_DIR, name), "utf8");
32112
32355
  return JSON.parse(raw.replaceAll("{{HOOK_COMMAND}}", hookCommand));
32113
32356
  }
32114
32357
  function readJson(path2) {
32115
- if (!existsSync20(path2))
32358
+ if (!existsSync21(path2))
32116
32359
  return null;
32117
32360
  try {
32118
- return JSON.parse(readFileSync19(path2, "utf8"));
32361
+ return JSON.parse(readFileSync20(path2, "utf8"));
32119
32362
  } catch (error) {
32120
32363
  throw new Error(`Invalid JSON in ${path2}: ${error}`);
32121
32364
  }
@@ -32275,10 +32518,10 @@ function mergeNestedHooks(args) {
32275
32518
  }
32276
32519
  function mergeHookConfigs(opts) {
32277
32520
  const results = [];
32278
- const cursorPath = join20(opts.cwd, ".cursor/hooks.json");
32521
+ const cursorPath = join21(opts.cwd, ".cursor/hooks.json");
32279
32522
  const cursorFragment = loadFragment("cursor-hooks.fragment.json", opts.hookCommand);
32280
32523
  results.push(mergeCursorHooks(cursorPath, cursorFragment, opts));
32281
- const claudePath = join20(opts.cwd, ".claude/settings.json");
32524
+ const claudePath = join21(opts.cwd, ".claude/settings.json");
32282
32525
  const claudeFragment = loadFragment("claude-settings.fragment.json", opts.hookCommand);
32283
32526
  results.push(mergeNestedHooks({
32284
32527
  platform: "claude",
@@ -32287,8 +32530,8 @@ function mergeHookConfigs(opts) {
32287
32530
  eventName: "PostToolUse",
32288
32531
  opts
32289
32532
  }));
32290
- const codexPath = join20(opts.cwd, ".codex/hooks.json");
32291
- if (existsSync20(join20(opts.cwd, ".codex"))) {
32533
+ const codexPath = join21(opts.cwd, ".codex/hooks.json");
32534
+ if (existsSync21(join21(opts.cwd, ".codex"))) {
32292
32535
  const codexFragment = loadFragment("codex-hooks.fragment.json", opts.hookCommand);
32293
32536
  results.push(mergeNestedHooks({
32294
32537
  platform: "codex",
@@ -32303,11 +32546,11 @@ function mergeHookConfigs(opts) {
32303
32546
  return results;
32304
32547
  }
32305
32548
  function mergePackageJsonScripts(cwd) {
32306
- const pkgPath = join20(cwd, "package.json");
32307
- if (!existsSync20(pkgPath))
32549
+ const pkgPath = join21(cwd, "package.json");
32550
+ if (!existsSync21(pkgPath))
32308
32551
  return "skipped";
32309
- const fragment = JSON.parse(readFileSync19(join20(TEMPLATES_DIR, "package.json.scripts.fragment.json"), "utf8"));
32310
- const pkg = JSON.parse(readFileSync19(pkgPath, "utf8"));
32552
+ const fragment = JSON.parse(readFileSync20(join21(TEMPLATES_DIR, "package.json.scripts.fragment.json"), "utf8"));
32553
+ const pkg = JSON.parse(readFileSync20(pkgPath, "utf8"));
32311
32554
  pkg.scripts ??= {};
32312
32555
  let changed = false;
32313
32556
  for (const [key, value] of Object.entries(fragment)) {
@@ -32363,23 +32606,23 @@ function skillsAddArgs(options = {}) {
32363
32606
  // src/init/init.ts
32364
32607
  var TEMPLATES_DIR2 = resolveTemplatesDir();
32365
32608
  function writeScaffold(cwd) {
32366
- const skeletonDir2 = join21(cwd, ".skeleton");
32609
+ const skeletonDir2 = join22(cwd, ".skeleton");
32367
32610
  mkdirSync3(skeletonDir2, { recursive: true });
32368
32611
  let created = false;
32369
- const tomlPath = join21(cwd, "skeleton.toml");
32370
- const legacyYaml = join21(skeletonDir2, "config.yaml");
32371
- if (!(existsSync21(tomlPath) || existsSync21(legacyYaml))) {
32372
- copyFileSync(join21(TEMPLATES_DIR2, "skeleton.toml"), tomlPath);
32612
+ const tomlPath = join22(cwd, "skeleton.toml");
32613
+ const legacyYaml = join22(skeletonDir2, "config.yaml");
32614
+ if (!(existsSync22(tomlPath) || existsSync22(legacyYaml))) {
32615
+ copyFileSync(join22(TEMPLATES_DIR2, "skeleton.toml"), tomlPath);
32373
32616
  created = true;
32374
32617
  }
32375
- mkdirSync3(join21(skeletonDir2, "customize"), { recursive: true });
32618
+ mkdirSync3(join22(skeletonDir2, "customize"), { recursive: true });
32376
32619
  return created ? "created" : "skipped";
32377
32620
  }
32378
32621
  function assertPackageResolvable(cwd) {
32379
- const pkgPath = join21(cwd, "package.json");
32380
- if (!existsSync21(pkgPath))
32622
+ const pkgPath = join22(cwd, "package.json");
32623
+ if (!existsSync22(pkgPath))
32381
32624
  return;
32382
- const pkg = JSON.parse(readFileSync20(pkgPath, "utf8"));
32625
+ const pkg = JSON.parse(readFileSync21(pkgPath, "utf8"));
32383
32626
  const hasDep = pkg.devDependencies?.["@csark0812/skeleton"] || pkg.dependencies?.["@csark0812/skeleton"];
32384
32627
  if (!hasDep) {
32385
32628
  try {
@@ -32463,7 +32706,7 @@ function parseInitArgs(argv) {
32463
32706
  // src/plugins/build.ts
32464
32707
  import { spawnSync as spawnSync3 } from "node:child_process";
32465
32708
  import { createHash } from "node:crypto";
32466
- import { existsSync as existsSync22, readFileSync as readFileSync21, writeFileSync as writeFileSync4 } from "node:fs";
32709
+ import { existsSync as existsSync23, readFileSync as readFileSync22, writeFileSync as writeFileSync4 } from "node:fs";
32467
32710
  import { basename as basename4, dirname as dirname12, resolve as resolve8 } from "node:path";
32468
32711
  function parseBuildPluginArgs(argv) {
32469
32712
  let check = false;
@@ -32504,7 +32747,7 @@ function localImportPaths(tsAbs, content3) {
32504
32747
  candidates.push(resolve8(dir, spec), resolve8(dir, `${spec}.ts`), resolve8(dir, `${spec}.js`), resolve8(dir, spec, "index.ts"));
32505
32748
  }
32506
32749
  for (const candidate of candidates) {
32507
- if (existsSync22(candidate) && candidate.endsWith(".ts")) {
32750
+ if (existsSync23(candidate) && candidate.endsWith(".ts")) {
32508
32751
  deps.push(candidate);
32509
32752
  break;
32510
32753
  }
@@ -32521,7 +32764,7 @@ function sourceFingerprint(tsAbs, seen = new Set) {
32521
32764
  if (seen.has(abs))
32522
32765
  return;
32523
32766
  seen.add(abs);
32524
- const content3 = readFileSync21(abs, "utf8");
32767
+ const content3 = readFileSync22(abs, "utf8");
32525
32768
  hash.update(basename4(abs));
32526
32769
  hash.update("\x00");
32527
32770
  hash.update(content3);
@@ -32539,7 +32782,7 @@ function writeStamp(tsAbs, mjsAbs) {
32539
32782
  }
32540
32783
  async function buildOne(tsAbs) {
32541
32784
  const mjsAbs = mjsPathForTs(tsAbs);
32542
- if (!existsSync22(tsAbs)) {
32785
+ if (!existsSync23(tsAbs)) {
32543
32786
  throw new Error(`Plugin source not found: ${tsAbs}`);
32544
32787
  }
32545
32788
  const proc = spawnSync3("bun", ["build", tsAbs, "--target=node", "--format=esm", `--outfile=${mjsAbs}`, "--packages=external"], { encoding: "utf8" });
@@ -32559,17 +32802,17 @@ ${proc.stderr || proc.stdout || `exit ${proc.status}`}`);
32559
32802
  }
32560
32803
  function checkOne(tsAbs) {
32561
32804
  const mjsAbs = mjsPathForTs(tsAbs);
32562
- if (!existsSync22(mjsAbs)) {
32805
+ if (!existsSync23(mjsAbs)) {
32563
32806
  throw new Error(`Plugin not built: ${tsAbs} (missing ${mjsAbs}). Run: skeleton build-plugin`);
32564
32807
  }
32565
- if (!existsSync22(tsAbs)) {
32808
+ if (!existsSync23(tsAbs)) {
32566
32809
  throw new Error(`Plugin source not found: ${tsAbs}`);
32567
32810
  }
32568
32811
  const stampAbs = stampPathForMjs(mjsAbs);
32569
- if (!existsSync22(stampAbs)) {
32812
+ if (!existsSync23(stampAbs)) {
32570
32813
  throw new Error(`Plugin stale: ${mjsAbs} has no fingerprint stamp. Run: skeleton build-plugin`);
32571
32814
  }
32572
- const expected = readFileSync21(stampAbs, "utf8").trim();
32815
+ const expected = readFileSync22(stampAbs, "utf8").trim();
32573
32816
  const actual = sourceFingerprint(tsAbs);
32574
32817
  if (expected !== actual) {
32575
32818
  throw new Error(`Plugin stale: ${mjsAbs} does not match ${tsAbs} (or local imports). Run: skeleton build-plugin`);
@@ -32602,14 +32845,14 @@ import process6 from "node:process";
32602
32845
 
32603
32846
  // src/references/sync.ts
32604
32847
  import {
32605
- existsSync as existsSync23,
32848
+ existsSync as existsSync24,
32606
32849
  mkdirSync as mkdirSync4,
32607
32850
  readdirSync as readdirSync6,
32608
- readFileSync as readFileSync22,
32851
+ readFileSync as readFileSync23,
32609
32852
  unlinkSync,
32610
32853
  writeFileSync as writeFileSync5
32611
32854
  } from "node:fs";
32612
- import { dirname as dirname13, join as join22, relative as relative11 } from "node:path";
32855
+ import { dirname as dirname13, join as join23, relative as relative12 } from "node:path";
32613
32856
  import process5 from "node:process";
32614
32857
  function resolveOwnership(root2, override) {
32615
32858
  if (override !== undefined)
@@ -32620,18 +32863,18 @@ function resolveOwnership(root2, override) {
32620
32863
  }
32621
32864
  function walkMarkdownFiles2(dir, root2) {
32622
32865
  const files = [];
32623
- if (!existsSync23(dir))
32866
+ if (!existsSync24(dir))
32624
32867
  return files;
32625
32868
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
32626
32869
  if (entry.name.startsWith("."))
32627
32870
  continue;
32628
- const fullPath = join22(dir, entry.name);
32871
+ const fullPath = join23(dir, entry.name);
32629
32872
  if (entry.isDirectory()) {
32630
32873
  files.push(...walkMarkdownFiles2(fullPath, root2));
32631
32874
  continue;
32632
32875
  }
32633
32876
  if (entry.name.endsWith(".md")) {
32634
- files.push(normalizeRelPath(relative11(root2, fullPath)));
32877
+ files.push(normalizeRelPath(relative12(root2, fullPath)));
32635
32878
  }
32636
32879
  }
32637
32880
  return files;
@@ -32639,23 +32882,23 @@ function walkMarkdownFiles2(dir, root2) {
32639
32882
  function collectGeneratedInDir(input) {
32640
32883
  const { dir, refsDir, skill, files } = input;
32641
32884
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
32642
- const fullPath = join22(dir, entry.name);
32885
+ const fullPath = join23(dir, entry.name);
32643
32886
  if (entry.isDirectory()) {
32644
32887
  collectGeneratedInDir({ dir: fullPath, refsDir, skill, files });
32645
32888
  continue;
32646
32889
  }
32647
32890
  if (!entry.name.endsWith(".md"))
32648
32891
  continue;
32649
- const content3 = readFileSync22(fullPath, "utf8");
32892
+ const content3 = readFileSync23(fullPath, "utf8");
32650
32893
  if (!isGeneratedReference(content3))
32651
32894
  continue;
32652
- const refPath = normalizeRelPath(relative11(refsDir, fullPath));
32895
+ const refPath = normalizeRelPath(relative12(refsDir, fullPath));
32653
32896
  files.push(generatedRefPath(skill, refPath));
32654
32897
  }
32655
32898
  }
32656
32899
  function listGeneratedReferenceFiles(skillDir, skill) {
32657
- const refsDir = join22(skillDir, "references");
32658
- if (!existsSync23(refsDir))
32900
+ const refsDir = join23(skillDir, "references");
32901
+ if (!existsSync24(refsDir))
32659
32902
  return [];
32660
32903
  const files = [];
32661
32904
  collectGeneratedInDir({ dir: refsDir, refsDir, skill, files });
@@ -32663,18 +32906,18 @@ function listGeneratedReferenceFiles(skillDir, skill) {
32663
32906
  }
32664
32907
  function syncGeneratedCopy(ctx, refPath) {
32665
32908
  const { root: root2, plan, options, result } = ctx;
32666
- const sourceRel = normalizeRelPath(join22(CANONICAL_REFS_DIR, refPath));
32667
- const canonicalPath = join22(root2, sourceRel);
32668
- if (!existsSync23(canonicalPath)) {
32909
+ const sourceRel = normalizeRelPath(join23(CANONICAL_REFS_DIR, refPath));
32910
+ const canonicalPath = join23(root2, sourceRel);
32911
+ if (!existsSync24(canonicalPath)) {
32669
32912
  throw new Error(`canonical reference missing: ${sourceRel}`);
32670
32913
  }
32671
32914
  const targetRel = generatedRefPath(plan.skill, refPath);
32672
- const targetPath = join22(root2, targetRel);
32673
- const canonicalContent = readFileSync22(canonicalPath, "utf8");
32915
+ const targetPath = join23(root2, targetRel);
32916
+ const canonicalContent = readFileSync23(canonicalPath, "utf8");
32674
32917
  const nextContent = formatGeneratedHeader(sourceRel) + canonicalContent;
32675
32918
  if (!options.dryRun)
32676
32919
  mkdirSync4(dirname13(targetPath), { recursive: true });
32677
- const existing = existsSync23(targetPath) ? readFileSync22(targetPath, "utf8") : null;
32920
+ const existing = existsSync24(targetPath) ? readFileSync23(targetPath, "utf8") : null;
32678
32921
  if (existing !== nextContent) {
32679
32922
  if (!options.dryRun)
32680
32923
  writeFileSync5(targetPath, nextContent, "utf8");
@@ -32688,8 +32931,8 @@ function rewritePlanLinks(ctx, skillDir) {
32688
32931
  if (options.rewriteLinks === false)
32689
32932
  return;
32690
32933
  for (const relFile of walkMarkdownFiles2(skillDir, root2)) {
32691
- const filePath = join22(root2, relFile);
32692
- const content3 = readFileSync22(filePath, "utf8");
32934
+ const filePath = join23(root2, relFile);
32935
+ const content3 = readFileSync23(filePath, "utf8");
32693
32936
  const next = rewriteSharedRefLinks(content3, relFile, plan.skill);
32694
32937
  if (next === content3)
32695
32938
  continue;
@@ -32705,12 +32948,12 @@ function removeStaleGenerated(ctx, skillDir) {
32705
32948
  if (plan.refPaths.has(refPath))
32706
32949
  continue;
32707
32950
  if (!options.dryRun)
32708
- unlinkSync(join22(root2, generatedRel));
32951
+ unlinkSync(join23(root2, generatedRel));
32709
32952
  result.removed.push(generatedRel);
32710
32953
  }
32711
32954
  }
32712
32955
  function syncPlan(ctx) {
32713
- const skillDir = join22(ctx.root, ctx.plan.skill);
32956
+ const skillDir = join23(ctx.root, ctx.plan.skill);
32714
32957
  for (const refPath of ctx.plan.refPaths) {
32715
32958
  syncGeneratedCopy(ctx, refPath);
32716
32959
  }
@@ -32719,8 +32962,8 @@ function syncPlan(ctx) {
32719
32962
  }
32720
32963
  function syncReferences(options = {}) {
32721
32964
  const root2 = options.root ?? process5.cwd();
32722
- const canonicalDir = join22(root2, CANONICAL_REFS_DIR);
32723
- if (!existsSync23(canonicalDir)) {
32965
+ const canonicalDir = join23(root2, CANONICAL_REFS_DIR);
32966
+ if (!existsSync24(canonicalDir)) {
32724
32967
  throw new Error(`canonical references dir not found: ${CANONICAL_REFS_DIR}`);
32725
32968
  }
32726
32969
  const result = { written: [], rewritten: [], removed: [], skipped: [] };
@@ -32767,8 +33010,8 @@ function printSyncResult(result) {
32767
33010
 
32768
33011
  // src/validate/changed.ts
32769
33012
  import { spawnSync as spawnSync5 } from "node:child_process";
32770
- import { existsSync as existsSync24, readFileSync as readFileSync23 } from "node:fs";
32771
- import { basename as basename5, extname as extname2, join as join23 } from "node:path";
33013
+ import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
33014
+ import { basename as basename5, extname as extname2, join as join24 } from "node:path";
32772
33015
 
32773
33016
  // src/validate/git-diff.ts
32774
33017
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -32861,9 +33104,9 @@ function parseJsonContent(content3) {
32861
33104
  }
32862
33105
  }
32863
33106
  function validateJson(relPath2, root2) {
32864
- const abs = join23(root2, relPath2);
33107
+ const abs = join24(root2, relPath2);
32865
33108
  try {
32866
- parseJsonContent(readFileSync23(abs, "utf8"));
33109
+ parseJsonContent(readFileSync24(abs, "utf8"));
32867
33110
  return 0;
32868
33111
  } catch (error) {
32869
33112
  console.error(`validate changed: invalid JSON in ${relPath2}: ${error}`);
@@ -32871,9 +33114,9 @@ function validateJson(relPath2, root2) {
32871
33114
  }
32872
33115
  }
32873
33116
  function validatePolicy(relPath2, root2) {
32874
- const abs = join23(root2, relPath2);
33117
+ const abs = join24(root2, relPath2);
32875
33118
  try {
32876
- loadPolicyFile(abs, readFileSync23(abs, "utf8"));
33119
+ loadPolicyFile(abs, readFileSync24(abs, "utf8"));
32877
33120
  return 0;
32878
33121
  } catch (error) {
32879
33122
  console.error(`validate changed: invalid policy ${relPath2}: ${error}`);
@@ -32881,7 +33124,7 @@ function validatePolicy(relPath2, root2) {
32881
33124
  }
32882
33125
  }
32883
33126
  function validateShell(relPath2, root2) {
32884
- const abs = join23(root2, relPath2);
33127
+ const abs = join24(root2, relPath2);
32885
33128
  const shellcheck = spawnSync5("shellcheck", [abs], { encoding: "utf8" });
32886
33129
  if (shellcheck.status === 0)
32887
33130
  return 0;
@@ -32902,11 +33145,11 @@ function resolvePaths(options) {
32902
33145
  });
32903
33146
  }
32904
33147
  function packageManagerFromPackageJson(root2) {
32905
- const pkgPath = join23(root2, "package.json");
32906
- if (!existsSync24(pkgPath))
33148
+ const pkgPath = join24(root2, "package.json");
33149
+ if (!existsSync25(pkgPath))
32907
33150
  return null;
32908
33151
  try {
32909
- const pkg = JSON.parse(readFileSync23(pkgPath, "utf8"));
33152
+ const pkg = JSON.parse(readFileSync24(pkgPath, "utf8"));
32910
33153
  const raw = pkg.packageManager?.split("@")[0];
32911
33154
  if (raw === "bun" || raw === "npm" || raw === "pnpm" || raw === "yarn")
32912
33155
  return raw;
@@ -32914,13 +33157,13 @@ function packageManagerFromPackageJson(root2) {
32914
33157
  return null;
32915
33158
  }
32916
33159
  function packageManagerFromLockfiles(root2) {
32917
- if (existsSync24(join23(root2, "bun.lock")) || existsSync24(join23(root2, "bun.lockb")))
33160
+ if (existsSync25(join24(root2, "bun.lock")) || existsSync25(join24(root2, "bun.lockb")))
32918
33161
  return "bun";
32919
- if (existsSync24(join23(root2, "pnpm-lock.yaml")))
33162
+ if (existsSync25(join24(root2, "pnpm-lock.yaml")))
32920
33163
  return "pnpm";
32921
- if (existsSync24(join23(root2, "yarn.lock")))
33164
+ if (existsSync25(join24(root2, "yarn.lock")))
32922
33165
  return "yarn";
32923
- if (existsSync24(join23(root2, "package-lock.json")))
33166
+ if (existsSync25(join24(root2, "package-lock.json")))
32924
33167
  return "npm";
32925
33168
  return null;
32926
33169
  }
@@ -32946,8 +33189,8 @@ function emptyBuckets() {
32946
33189
  function classifySinglePath(input) {
32947
33190
  const { relPath: relPath2, ctx, state, bucketCtx } = input;
32948
33191
  const normalized = normalizeRelPath(relPath2);
32949
- const abs = join23(ctx.root, normalized);
32950
- if (!existsSync24(abs)) {
33192
+ const abs = join24(ctx.root, normalized);
33193
+ if (!existsSync25(abs)) {
32951
33194
  state.missing++;
32952
33195
  console.error(`validate changed: path not found: ${relPath2}`);
32953
33196
  return;
@@ -33261,7 +33504,7 @@ function handleHook(argv) {
33261
33504
  usage();
33262
33505
  return 1;
33263
33506
  }
33264
- process7.stdout.write(runCustomizeHook(readFileSync24(0, "utf8")));
33507
+ process7.stdout.write(runCustomizeHook(readFileSync25(0, "utf8")));
33265
33508
  return 0;
33266
33509
  }
33267
33510
  function handleInit(argv) {
@@ -25,6 +25,8 @@ export type AuditSuite = "docs" | "skills";
25
25
  export interface AuditRule {
26
26
  id: string;
27
27
  global?: boolean;
28
+ /** When true, still run under `--paths` (see code-fit). */
29
+ alwaysRun?: boolean;
28
30
  suites?: AuditSuite[];
29
31
  run: (ctx: AuditContext) => Issue[];
30
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@csark0812/skeleton",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "SSOT audit CLI for agent harness repos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -47,7 +47,7 @@
47
47
  "docsLint": {
48
48
  "type": "object",
49
49
  "additionalProperties": false,
50
- "description": "Near-duplicate and SSOT-summary lint tunables",
50
+ "description": "Near-duplicate, SSOT-summary, and code-fit lint tunables",
51
51
  "properties": {
52
52
  "nearDuplicateThreshold": {
53
53
  "type": "number",
@@ -85,6 +85,17 @@
85
85
  "type": "array",
86
86
  "items": { "type": "string", "minLength": 1 },
87
87
  "description": "Globs excluded from near-dupe / duplicate-SSOT"
88
+ },
89
+ "codeFitOverlapMin": {
90
+ "type": "number",
91
+ "minimum": 0,
92
+ "maximum": 1,
93
+ "description": "Min fraction of doc tokens that must also appear as code identifiers (default 0.03)"
94
+ },
95
+ "codeFitSurfaceCap": {
96
+ "type": "integer",
97
+ "minimum": 1,
98
+ "description": "Max auto-extracted surface names before surface= is required (default 25)"
88
99
  }
89
100
  }
90
101
  },