@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 +3 -1
- package/dist/cli.js +541 -298
- package/dist/plugin-types.d.ts +2 -0
- package/package.json +1 -1
- package/schemas/config.schema.json +12 -1
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
|
|
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/
|
|
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 =
|
|
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
|
|
31410
|
-
import { join as
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
31445
|
-
const content3 =
|
|
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 =
|
|
31461
|
-
if (!
|
|
31702
|
+
const readmePath = join15(ctx.root, skillRoot.relPath, "README.md");
|
|
31703
|
+
if (!existsSync16(readmePath))
|
|
31462
31704
|
return [];
|
|
31463
|
-
const readme =
|
|
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) =>
|
|
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" ?
|
|
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 =
|
|
31503
|
-
if (!
|
|
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
|
|
31554
|
-
import { join as
|
|
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:
|
|
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
|
|
31783
|
-
import { dirname as dirname8, join as
|
|
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 =
|
|
31813
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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
|
|
31849
|
-
import { basename as basename3, join as
|
|
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
|
|
32094
|
+
return join18(root2, REGISTRY_DIR_REL, "customize");
|
|
31852
32095
|
}
|
|
31853
32096
|
function customizePathForSlug(root2, slug2) {
|
|
31854
|
-
return
|
|
32097
|
+
return join18(customizeDir(root2), `${slug2}.md`);
|
|
31855
32098
|
}
|
|
31856
32099
|
function resolveSlugFile(root2, slug2) {
|
|
31857
32100
|
const direct = customizePathForSlug(root2, slug2);
|
|
31858
|
-
if (
|
|
32101
|
+
if (existsSync18(direct)) {
|
|
31859
32102
|
return {
|
|
31860
|
-
content:
|
|
31861
|
-
path: normalizeRelPath(
|
|
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 =
|
|
31883
|
-
if (!
|
|
32125
|
+
const abs = join18(dir, file);
|
|
32126
|
+
if (!existsSync18(abs))
|
|
31884
32127
|
continue;
|
|
31885
|
-
parts.push(
|
|
31886
|
-
paths.push(normalizeRelPath(
|
|
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
|
|
32010
|
-
import { join as
|
|
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
|
|
32015
|
-
import { dirname as dirname11, join as
|
|
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
|
|
32019
|
-
import { dirname as dirname9, join as
|
|
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 = [
|
|
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 (
|
|
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 =
|
|
32032
|
-
if (!
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
32070
|
-
if (
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
32358
|
+
if (!existsSync21(path2))
|
|
32116
32359
|
return null;
|
|
32117
32360
|
try {
|
|
32118
|
-
return JSON.parse(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
32291
|
-
if (
|
|
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 =
|
|
32307
|
-
if (!
|
|
32549
|
+
const pkgPath = join21(cwd, "package.json");
|
|
32550
|
+
if (!existsSync21(pkgPath))
|
|
32308
32551
|
return "skipped";
|
|
32309
|
-
const fragment = JSON.parse(
|
|
32310
|
-
const pkg = JSON.parse(
|
|
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 =
|
|
32609
|
+
const skeletonDir2 = join22(cwd, ".skeleton");
|
|
32367
32610
|
mkdirSync3(skeletonDir2, { recursive: true });
|
|
32368
32611
|
let created = false;
|
|
32369
|
-
const tomlPath =
|
|
32370
|
-
const legacyYaml =
|
|
32371
|
-
if (!(
|
|
32372
|
-
copyFileSync(
|
|
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(
|
|
32618
|
+
mkdirSync3(join22(skeletonDir2, "customize"), { recursive: true });
|
|
32376
32619
|
return created ? "created" : "skipped";
|
|
32377
32620
|
}
|
|
32378
32621
|
function assertPackageResolvable(cwd) {
|
|
32379
|
-
const pkgPath =
|
|
32380
|
-
if (!
|
|
32622
|
+
const pkgPath = join22(cwd, "package.json");
|
|
32623
|
+
if (!existsSync22(pkgPath))
|
|
32381
32624
|
return;
|
|
32382
|
-
const pkg = JSON.parse(
|
|
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
|
|
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 (
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
32805
|
+
if (!existsSync23(mjsAbs)) {
|
|
32563
32806
|
throw new Error(`Plugin not built: ${tsAbs} (missing ${mjsAbs}). Run: skeleton build-plugin`);
|
|
32564
32807
|
}
|
|
32565
|
-
if (!
|
|
32808
|
+
if (!existsSync23(tsAbs)) {
|
|
32566
32809
|
throw new Error(`Plugin source not found: ${tsAbs}`);
|
|
32567
32810
|
}
|
|
32568
32811
|
const stampAbs = stampPathForMjs(mjsAbs);
|
|
32569
|
-
if (!
|
|
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 =
|
|
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
|
|
32848
|
+
existsSync as existsSync24,
|
|
32606
32849
|
mkdirSync as mkdirSync4,
|
|
32607
32850
|
readdirSync as readdirSync6,
|
|
32608
|
-
readFileSync as
|
|
32851
|
+
readFileSync as readFileSync23,
|
|
32609
32852
|
unlinkSync,
|
|
32610
32853
|
writeFileSync as writeFileSync5
|
|
32611
32854
|
} from "node:fs";
|
|
32612
|
-
import { dirname as dirname13, join as
|
|
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 (!
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
32892
|
+
const content3 = readFileSync23(fullPath, "utf8");
|
|
32650
32893
|
if (!isGeneratedReference(content3))
|
|
32651
32894
|
continue;
|
|
32652
|
-
const refPath = normalizeRelPath(
|
|
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 =
|
|
32658
|
-
if (!
|
|
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(
|
|
32667
|
-
const canonicalPath =
|
|
32668
|
-
if (!
|
|
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 =
|
|
32673
|
-
const canonicalContent =
|
|
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 =
|
|
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 =
|
|
32692
|
-
const content3 =
|
|
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(
|
|
32951
|
+
unlinkSync(join23(root2, generatedRel));
|
|
32709
32952
|
result.removed.push(generatedRel);
|
|
32710
32953
|
}
|
|
32711
32954
|
}
|
|
32712
32955
|
function syncPlan(ctx) {
|
|
32713
|
-
const skillDir =
|
|
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 =
|
|
32723
|
-
if (!
|
|
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
|
|
32771
|
-
import { basename as basename5, extname as extname2, join as
|
|
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 =
|
|
33107
|
+
const abs = join24(root2, relPath2);
|
|
32865
33108
|
try {
|
|
32866
|
-
parseJsonContent(
|
|
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 =
|
|
33117
|
+
const abs = join24(root2, relPath2);
|
|
32875
33118
|
try {
|
|
32876
|
-
loadPolicyFile(abs,
|
|
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 =
|
|
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 =
|
|
32906
|
-
if (!
|
|
33148
|
+
const pkgPath = join24(root2, "package.json");
|
|
33149
|
+
if (!existsSync25(pkgPath))
|
|
32907
33150
|
return null;
|
|
32908
33151
|
try {
|
|
32909
|
-
const pkg = JSON.parse(
|
|
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 (
|
|
33160
|
+
if (existsSync25(join24(root2, "bun.lock")) || existsSync25(join24(root2, "bun.lockb")))
|
|
32918
33161
|
return "bun";
|
|
32919
|
-
if (
|
|
33162
|
+
if (existsSync25(join24(root2, "pnpm-lock.yaml")))
|
|
32920
33163
|
return "pnpm";
|
|
32921
|
-
if (
|
|
33164
|
+
if (existsSync25(join24(root2, "yarn.lock")))
|
|
32922
33165
|
return "yarn";
|
|
32923
|
-
if (
|
|
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 =
|
|
32950
|
-
if (!
|
|
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(
|
|
33507
|
+
process7.stdout.write(runCustomizeHook(readFileSync25(0, "utf8")));
|
|
33265
33508
|
return 0;
|
|
33266
33509
|
}
|
|
33267
33510
|
function handleInit(argv) {
|
package/dist/plugin-types.d.ts
CHANGED
|
@@ -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
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"docsLint": {
|
|
48
48
|
"type": "object",
|
|
49
49
|
"additionalProperties": false,
|
|
50
|
-
"description": "Near-duplicate
|
|
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
|
},
|