@csark0812/skeleton 2.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -15678,7 +15678,7 @@ var require_extend = __commonJS((exports, module) => {
15678
15678
  });
15679
15679
 
15680
15680
  // src/cli.ts
15681
- import { readFileSync as readFileSync26 } from "node:fs";
15681
+ import { readFileSync as readFileSync25 } from "node:fs";
15682
15682
  import process9 from "node:process";
15683
15683
 
15684
15684
  // src/audit/config/load.ts
@@ -30641,438 +30641,61 @@ function printTextReport(ctx) {
30641
30641
 
30642
30642
  // src/audit/core/review-proof.ts
30643
30643
  import { createHash } from "node:crypto";
30644
- import { existsSync as existsSync13, mkdirSync as mkdirSync2, readFileSync as readFileSync12, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
30644
+ import { existsSync as existsSync13, mkdirSync as mkdirSync2, readFileSync as readFileSync11, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
30645
30645
  import { dirname as dirname8, relative as relative6 } from "node:path";
30646
30646
 
30647
- // src/audit/core/code-fit.ts
30648
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
30649
- import { join as join11 } from "node:path";
30650
-
30651
- // src/audit/core/ssot-fit.ts
30652
- var DEFAULT_SSOT_OVERLAP_MIN = 0.35;
30653
- var DEFAULT_BETTER_MATCH_MARGIN = 0.15;
30654
- var STOP = new Set([
30655
- "a",
30656
- "an",
30657
- "the",
30658
- "and",
30659
- "or",
30660
- "of",
30661
- "for",
30662
- "to",
30663
- "in",
30664
- "on",
30665
- "with",
30666
- "this",
30667
- "that",
30668
- "is",
30669
- "are",
30670
- "be",
30671
- "as",
30672
- "by",
30673
- "from",
30674
- "at",
30675
- "it",
30676
- "its"
30677
- ]);
30678
- var STEM_BLOCKLIST = new Set([
30679
- "business",
30680
- "analysis",
30681
- "status",
30682
- "process",
30683
- "access",
30684
- "address",
30685
- "series",
30686
- "species",
30687
- "news",
30688
- "means",
30689
- "cross",
30690
- "class",
30691
- "glass",
30692
- "less",
30693
- "success",
30694
- "progress",
30695
- "express",
30696
- "discuss",
30697
- "focus",
30698
- "bonus",
30699
- "basis",
30700
- "crisis",
30701
- "thesis",
30702
- "atlas",
30703
- "canvas",
30704
- "campus",
30705
- "virus",
30706
- "bus",
30707
- "gas",
30708
- "plus",
30709
- "alias",
30710
- "bias",
30711
- "circus",
30712
- "consensus",
30713
- "census"
30714
- ]);
30715
- function lightStem(token) {
30716
- if (token.length < 4)
30717
- return token;
30718
- if (STEM_BLOCKLIST.has(token))
30719
- return token;
30720
- if (token.endsWith("ies") && token.length > 4) {
30721
- return `${token.slice(0, -3)}y`;
30722
- }
30723
- if (token.endsWith("sses") || token.endsWith("ches") || token.endsWith("shes") || token.endsWith("xes")) {
30724
- return token.slice(0, -2);
30725
- }
30726
- if (token.endsWith("s") && !token.endsWith("ss") && !token.endsWith("us") && !token.endsWith("is")) {
30727
- return token.slice(0, -1);
30728
- }
30729
- return token;
30730
- }
30731
- function contentTokens(text5) {
30732
- return text5.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 1 && !STOP.has(t)).map(lightStem);
30733
- }
30734
- function uniqueContentTokens(text5) {
30735
- return [...new Set(contentTokens(text5))];
30736
- }
30737
- function stripCode2(content3) {
30738
- return content3.replace(/```[\s\S]*?```/g, `
30739
- `).replace(/`[^`\n]+`/g, " ");
30740
- }
30741
- function stripSsotAndMeta(content3) {
30742
- return content3.replace(/<!--\s*source-of-truth:[\s\S]*?-->/gi, `
30743
- `).replace(/^\s*source-of-truth:\s*.+$/gim, `
30744
- `).replace(/^\s*\*\*Source of truth for\*\*\s*.+$/gim, `
30745
- `).replace(/<!--\s*doc-meta:[\s\S]*?-->/gi, `
30746
- `);
30747
- }
30748
- function extractH1(content3) {
30749
- const prose = stripCode2(content3);
30750
- const m = /^#\s+(.+)$/m.exec(prose);
30751
- return m?.[1]?.trim() ?? "";
30752
- }
30753
- function extractLeadParagraph(content3) {
30754
- const prose = stripSsotAndMeta(stripCode2(content3));
30755
- const lines = prose.split(/\n/);
30756
- const chunks = [];
30757
- let buf = [];
30758
- const flush = () => {
30759
- const t = buf.join(" ").trim();
30760
- if (t)
30761
- chunks.push(t);
30762
- buf = [];
30763
- };
30764
- for (const line of lines) {
30765
- const trimmed = line.trim();
30766
- if (!trimmed || trimmed.startsWith("#")) {
30767
- flush();
30768
- continue;
30769
- }
30770
- if (/^[-*|]/.test(trimmed) && chunks.length === 0 && buf.length === 0) {
30771
- buf.push(trimmed.replace(/^[-*|]+\s*/, ""));
30772
- continue;
30773
- }
30774
- buf.push(trimmed);
30775
- }
30776
- flush();
30777
- return chunks[0] ?? "";
30778
- }
30779
- function buildEvidenceText(content3) {
30780
- const h1 = extractH1(content3);
30781
- const lead = extractLeadParagraph(content3);
30782
- const body = stripSsotAndMeta(stripCode2(content3));
30783
- return [h1, lead, body].filter(Boolean).join(`
30784
-
30785
- `);
30786
- }
30787
- function ssotEvidenceOverlap(summary, evidence) {
30788
- const st = uniqueContentTokens(summary);
30789
- if (st.length === 0)
30790
- return 0;
30791
- const ev = new Set(contentTokens(evidence));
30792
- let hit = 0;
30793
- for (const t of st) {
30794
- if (ev.has(t))
30795
- hit++;
30796
- }
30797
- return hit / st.length;
30798
- }
30799
- function longestSummaryPhrase(summary) {
30800
- const toks = contentTokens(summary);
30801
- if (toks.length >= 3)
30802
- return toks.slice(0, 3);
30803
- if (toks.length >= 2)
30804
- return toks.slice(0, 2);
30805
- return null;
30806
- }
30807
- function evidenceHasPhrase(evidence, phrase) {
30808
- if (phrase.length === 0)
30809
- return true;
30810
- const ev = contentTokens(evidence);
30811
- const needle = phrase.join(" ");
30812
- for (let i = 0;i <= ev.length - phrase.length; i++) {
30813
- if (ev.slice(i, i + phrase.length).join(" ") === needle)
30814
- return true;
30815
- }
30816
- return false;
30817
- }
30818
- function evaluateSsotFit(files, options = {}) {
30819
- const overlapMin = options.overlapMin ?? DEFAULT_SSOT_OVERLAP_MIN;
30820
- const margin = options.betterMatchMargin ?? DEFAULT_BETTER_MATCH_MARGIN;
30821
- const phraseCheck = options.phraseCheck !== false;
30822
- const prepared = files.map((f) => {
30823
- const evidence = buildEvidenceText(f.content);
30824
- const overlap = ssotEvidenceOverlap(f.summary, evidence);
30825
- const summaryToks = uniqueContentTokens(f.summary);
30826
- return { ...f, evidence, overlap, summaryToks };
30827
- });
30828
- const issues = [];
30829
- for (const row of prepared) {
30830
- if (row.summaryToks.length < 2) {
30831
- issues.push({
30832
- kind: "short",
30833
- path: row.path,
30834
- message: `source-of-truth summary too short to verify against body ("${row.summary}")`
30835
- });
30836
- continue;
30837
- }
30838
- if (row.overlap < overlapMin) {
30839
- let message = `source-of-truth summary weakly matches this paper (token overlap ${row.overlap.toFixed(2)} < ${overlapMin})`;
30840
- if (phraseCheck) {
30841
- const phrase = longestSummaryPhrase(row.summary);
30842
- if (phrase && !evidenceHasPhrase(row.evidence, phrase)) {
30843
- message += ` — key phrase "${phrase.join(" ")}" not found in H1/lead/body`;
30844
- }
30845
- }
30846
- issues.push({ kind: "weak", path: row.path, message });
30847
- let best = null;
30848
- for (const other of prepared) {
30849
- if (other.path === row.path)
30850
- continue;
30851
- const cross = ssotEvidenceOverlap(row.summary, other.evidence);
30852
- if (cross < overlapMin)
30853
- continue;
30854
- if (cross < row.overlap + margin)
30855
- continue;
30856
- if (!best || cross > best.overlap)
30857
- best = { path: other.path, overlap: cross };
30858
- }
30859
- if (best) {
30860
- issues.push({
30861
- kind: "better-match",
30862
- path: row.path,
30863
- otherPath: best.path,
30864
- message: `source-of-truth fits ${best.path} better (overlap ${best.overlap.toFixed(2)} vs own ${row.overlap.toFixed(2)}). ` + `Try: (1) rewrite this SSOT to match this paper, (2) move/fix the marker onto ${best.path}, ` + `or (3) if these pages are really one topic, consider combining them`
30865
- });
30866
- }
30867
- }
30868
- }
30869
- return issues;
30870
- }
30871
-
30872
- // src/audit/core/code-fit.ts
30873
- var DEFAULT_CODE_FIT_OVERLAP_MIN = 0.03;
30874
- var DEFAULT_CODE_FIT_SURFACE_CAP = 25;
30875
- var CODE_FIT_RE = /<!--\s*code-fit:\s*([^>]*?)-->/gi;
30876
- function parseCodeFitMarkers(content3) {
30647
+ // src/audit/core/review-deps.ts
30648
+ import { existsSync as existsSync12 } from "node:fs";
30649
+ var REVIEW_DEPS_RE = /<!--\s*review-deps:\s*([^>]*?)-->/gi;
30650
+ var GLOB_MAGIC_RE = /[*?{[]/;
30651
+ function parseReviewDepsMarkers(content3) {
30877
30652
  const withoutCode = content3.replace(/```[\s\S]*?```/g, `
30878
30653
  `).replace(/`[^`\n]+`/g, " ");
30879
30654
  const out = [];
30880
- for (const match of withoutCode.matchAll(CODE_FIT_RE)) {
30881
- const body = (match[1] ?? "").trim();
30882
- const parsed = parseMarkerBody(body);
30883
- if (parsed)
30884
- out.push(parsed);
30655
+ for (const match of withoutCode.matchAll(REVIEW_DEPS_RE)) {
30656
+ const raw = (match[1] ?? "").trim();
30657
+ const pathsMatch = /\bpaths\s*=\s*([^\s]+)/i.exec(raw);
30658
+ const paths = pathsMatch?.[1] ? pathsMatch[1].split(",").map((path3) => normalizeRelPath(path3.trim())).filter(Boolean) : [];
30659
+ out.push({ paths, raw });
30885
30660
  }
30886
30661
  return out;
30887
30662
  }
30888
- function parseMarkerBody(body) {
30889
- const targetsMatch = /\btargets\s*=\s*([^\s]+)/i.exec(body);
30890
- if (!targetsMatch?.[1])
30891
- return null;
30892
- const targets = targetsMatch[1].split(",").map((t) => t.trim()).filter(Boolean);
30893
- if (targets.length === 0)
30894
- return null;
30895
- const surfaceMatch = /\bsurface\s*=\s*([^\s]+)/i.exec(body);
30896
- const surface = surfaceMatch?.[1] ? surfaceMatch[1].split(",").map((s) => s.trim()).filter(Boolean) : null;
30897
- return { targets, surface, raw: body };
30663
+ function reviewDependencyPatterns(content3) {
30664
+ return [...new Set(parseReviewDepsMarkers(content3).flatMap((marker) => marker.paths))].sort();
30898
30665
  }
30899
- function stripCodeNoise(source) {
30900
- return source.replace(/\/\*[\s\S]*?\*\//g, `
30901
- `).replace(/\/\/[^\n]*/g, `
30902
- `).replace(/`(?:\\.|[^`\\])*`/g, " ").replace(/'(?:\\.|[^'\\])*'/g, " ").replace(/"(?:\\.|[^"\\])*"/g, " ");
30666
+ function isReviewDependencyGlob(pattern) {
30667
+ return GLOB_MAGIC_RE.test(pattern);
30903
30668
  }
30904
- function extractPublicSurface(source) {
30905
- const names = new Set;
30906
- for (const m of source.matchAll(/\bcase\s+["']([^"']+)["']\s*:/g)) {
30907
- if (m[1])
30908
- names.add(m[1]);
30909
- }
30910
- for (const m of source.matchAll(/\bexport\s+(?:async\s+)?(?:function|class|const|let|var|type|interface|enum)\s+([A-Za-z_$][\w$]*)/g)) {
30911
- if (m[1])
30912
- names.add(m[1]);
30913
- }
30914
- for (const m of source.matchAll(/\bexport\s+default\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/g)) {
30915
- if (m[1])
30916
- names.add(m[1]);
30917
- }
30918
- for (const m of source.matchAll(/\bexport\s+default\s+class\s+([A-Za-z_$][\w$]*)/g)) {
30919
- if (m[1])
30920
- names.add(m[1]);
30921
- }
30922
- collectExportListNames(source, names);
30923
- return [...names].sort();
30669
+ function isSafeReviewDependencyPath(value) {
30670
+ const normalized = normalizeRelPath(value);
30671
+ return value.length > 0 && value === normalized && normalized !== "." && normalized !== ".." && !normalized.startsWith("/") && !normalized.startsWith("../") && !normalized.includes("/../") && !/^[A-Za-z]:\//.test(normalized);
30924
30672
  }
30925
- function collectExportListNames(source, names) {
30926
- for (const m of source.matchAll(/\bexport\s*\{([^}]+)\}/g)) {
30927
- const inner = m[1] ?? "";
30928
- for (const part of inner.split(",")) {
30929
- addExportListPart(part.trim(), names);
30673
+ function resolveReviewDependencies(root2, patterns) {
30674
+ const targets = new Set;
30675
+ for (const pattern of patterns) {
30676
+ if (!isSafeReviewDependencyPath(pattern)) {
30677
+ throw new Error(`Invalid review dependency path: ${pattern}`);
30930
30678
  }
30931
- }
30932
- }
30933
- function addExportListPart(cleaned, names) {
30934
- if (!cleaned || cleaned === "type" || cleaned === "typeof")
30935
- return;
30936
- const asMatch = /^([\w$]+)\s+as\s+([\w$]+)$/.exec(cleaned);
30937
- if (asMatch?.[2]) {
30938
- names.add(asMatch[2]);
30939
- return;
30940
- }
30941
- const typeAs = /^type\s+([\w$]+)(?:\s+as\s+([\w$]+))?$/.exec(cleaned);
30942
- if (typeAs?.[1]) {
30943
- names.add(typeAs[2] ?? typeAs[1]);
30944
- return;
30945
- }
30946
- const id = /^([\w$]+)$/.exec(cleaned);
30947
- if (id?.[1])
30948
- names.add(id[1]);
30949
- }
30950
- function codeIdentifiers(source) {
30951
- const stripped = stripCodeNoise(source);
30952
- return uniqueContentTokens(stripped.replace(/[^A-Za-z0-9_$]+/g, " ").replace(/_/g, " "));
30953
- }
30954
- function identifierOverlap(docContent, codeSource) {
30955
- const codeIds = new Set(codeIdentifiers(codeSource));
30956
- if (codeIds.size === 0)
30957
- return 1;
30958
- const docToks = uniqueContentTokens(docContent);
30959
- if (docToks.length === 0)
30960
- return 0;
30961
- let hit = 0;
30962
- for (const t of docToks) {
30963
- if (codeIds.has(t))
30964
- hit++;
30965
- }
30966
- return hit / docToks.length;
30967
- }
30968
- function nameInDoc(name, docContent) {
30969
- const docToks = new Set(contentTokens(docContent));
30970
- const parts = uniqueContentTokens(name.replace(/_/g, " "));
30971
- if (parts.length === 0) {
30972
- return docToks.has(name.toLowerCase());
30973
- }
30974
- const asToken = contentTokens(name);
30975
- if (asToken.some((t) => docToks.has(t)))
30976
- return true;
30977
- return parts.every((p) => docToks.has(p));
30978
- }
30979
- function nameExistsInTarget(name, autoSurface, source) {
30980
- if (autoSurface.includes(name))
30981
- return true;
30982
- const ids = new Set(extractPublicSurface(source));
30983
- if (ids.has(name))
30984
- return true;
30985
- const re = new RegExp(`\\b${escapeRegExp(name)}\\b`);
30986
- return re.test(source);
30987
- }
30988
- function escapeRegExp(s) {
30989
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30990
- }
30991
- function pushCoverageGaps(issues, input, effective) {
30992
- if (effective.length === 0)
30993
- return;
30994
- const missing = effective.filter((n) => !nameInDoc(n, input.docContent));
30995
- if (missing.length === 0)
30996
- return;
30997
- issues.push({
30998
- path: input.docPath,
30999
- message: `code-fit coverage: doc does not mention ${missing.map((m) => `"${m}"`).join(", ")} (from ${input.target})`,
31000
- link: input.target
31001
- });
31002
- }
31003
- function pushLexicalGap(issues, input, source) {
31004
- const overlapMin = input.options.overlapMin ?? DEFAULT_CODE_FIT_OVERLAP_MIN;
31005
- const overlap = identifierOverlap(input.docContent, source);
31006
- if (overlap + 0.000000001 >= overlapMin)
31007
- return;
31008
- issues.push({
31009
- path: input.docPath,
31010
- message: `code-fit lexical overlap ${(overlap * 100).toFixed(0)}% < ${(overlapMin * 100).toFixed(0)}% vs ${input.target}`,
31011
- link: input.target
31012
- });
31013
- }
31014
- function evaluateTarget(input) {
31015
- const issues = [];
31016
- const abs = join11(input.options.root, input.target);
31017
- if (!existsSync12(abs)) {
31018
- issues.push({
31019
- path: input.docPath,
31020
- message: `code-fit target missing: ${input.target}`,
31021
- link: input.target
31022
- });
31023
- return issues;
31024
- }
31025
- const source = readFileSync11(abs, "utf8");
31026
- const auto = extractPublicSurface(source);
31027
- const cap = input.options.surfaceCap ?? DEFAULT_CODE_FIT_SURFACE_CAP;
31028
- if (input.surfaceOverride === null && auto.length > cap) {
31029
- issues.push({
31030
- path: input.docPath,
31031
- message: `code-fit auto-surface has ${auto.length} names (cap ${cap}); add surface=… to the marker for ${input.target}`,
31032
- link: input.target
31033
- });
31034
- return issues;
31035
- }
31036
- if (input.surfaceOverride !== null) {
31037
- for (const name of input.surfaceOverride) {
31038
- if (!nameExistsInTarget(name, auto, source)) {
31039
- issues.push({
31040
- path: input.docPath,
31041
- message: `code-fit surface name "${name}" not found in ${input.target}`,
31042
- link: input.target
31043
- });
30679
+ if (!isReviewDependencyGlob(pattern)) {
30680
+ if (!existsSync12(`${root2}/${pattern}`)) {
30681
+ throw new Error(`Review dependency path is missing: ${pattern}`);
31044
30682
  }
31045
- }
31046
- }
31047
- const effective = input.surfaceOverride !== null ? input.surfaceOverride : auto;
31048
- pushCoverageGaps(issues, input, effective);
31049
- pushLexicalGap(issues, input, source);
31050
- return issues;
31051
- }
31052
- function evaluateCodeFitDoc(docPath, docContent, options) {
31053
- const markers = parseCodeFitMarkers(docContent);
31054
- if (markers.length === 0)
31055
- return [];
31056
- const issues = [];
31057
- for (const marker of markers) {
31058
- if (marker.targets.length === 0) {
31059
- issues.push({
31060
- path: docPath,
31061
- message: "code-fit marker missing targets="
31062
- });
30683
+ targets.add(pattern);
31063
30684
  continue;
31064
30685
  }
31065
- for (const target of marker.targets) {
31066
- issues.push(...evaluateTarget({
31067
- docPath,
31068
- docContent,
31069
- target,
31070
- surfaceOverride: marker.surface,
31071
- options
31072
- }));
30686
+ for (const match of globSync(pattern, {
30687
+ cwd: root2,
30688
+ onlyFiles: true,
30689
+ dot: true,
30690
+ ignore: [".git/**"]
30691
+ })) {
30692
+ targets.add(normalizeRelPath(match));
31073
30693
  }
31074
30694
  }
31075
- return issues;
30695
+ return { patterns: [...new Set(patterns)].sort(), targets: [...targets].sort() };
30696
+ }
30697
+ function reviewDependencyMatchesPath(pattern, relPath2) {
30698
+ return isReviewDependencyGlob(pattern) ? matchesGlobScope(relPath2, pattern) : normalizeRelPath(relPath2) === pattern;
31076
30699
  }
31077
30700
 
31078
30701
  // src/audit/core/review-proof.ts
@@ -31087,7 +30710,7 @@ function hash(content3) {
31087
30710
  return `sha256:${createHash("sha256").update(content3, "utf8").digest("hex")}`;
31088
30711
  }
31089
30712
  function emptyLock() {
31090
- return { version: 1, documents: {} };
30713
+ return { version: 2, documents: {} };
31091
30714
  }
31092
30715
  function isRecord(value) {
31093
30716
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -31112,17 +30735,17 @@ function parseEntry(value) {
31112
30735
  return null;
31113
30736
  if (!isHash(value.documentHash))
31114
30737
  return null;
31115
- if (!isRecord(value.codeTargets))
30738
+ if (!isRecord(value.reviewDependencies))
31116
30739
  return null;
31117
- const codeTargets = {};
31118
- for (const [target, targetHash] of Object.entries(value.codeTargets)) {
30740
+ const reviewDependencies = {};
30741
+ for (const [target, targetHash] of Object.entries(value.reviewDependencies)) {
31119
30742
  if (!isSafeRepoPath(target))
31120
30743
  return null;
31121
30744
  if (!isHash(targetHash))
31122
30745
  return null;
31123
- codeTargets[target] = targetHash;
30746
+ reviewDependencies[target] = targetHash;
31124
30747
  }
31125
- return { reviewedAt: value.reviewedAt, documentHash: value.documentHash, codeTargets };
30748
+ return { reviewedAt: value.reviewedAt, documentHash: value.documentHash, reviewDependencies };
31126
30749
  }
31127
30750
  function lockPath(ctx) {
31128
30751
  return normalizeRelPath(ctx.config.reviewProof?.lockfile ?? DEFAULT_REVIEW_LOCKFILE);
@@ -31130,7 +30753,7 @@ function lockPath(ctx) {
31130
30753
  function parseLock(content3) {
31131
30754
  try {
31132
30755
  const parsed = JSON.parse(content3);
31133
- if (!isRecord(parsed) || parsed.version !== 1 || !isRecord(parsed.documents))
30756
+ if (!isRecord(parsed) || parsed.version !== 2 || !isRecord(parsed.documents))
31134
30757
  return null;
31135
30758
  const documents = {};
31136
30759
  for (const [path3, value] of Object.entries(parsed.documents)) {
@@ -31141,7 +30764,7 @@ function parseLock(content3) {
31141
30764
  return null;
31142
30765
  documents[path3] = entry;
31143
30766
  }
31144
- return { version: 1, documents };
30767
+ return { version: 2, documents };
31145
30768
  } catch {
31146
30769
  return null;
31147
30770
  }
@@ -31150,18 +30773,15 @@ function loadLock(root2, relPath2) {
31150
30773
  const abs = resolveWritePath(root2, relPath2);
31151
30774
  if (!existsSync13(abs))
31152
30775
  return null;
31153
- return parseLock(readFileSync12(abs, "utf8"));
31154
- }
31155
- function codeTargets(content3) {
31156
- return [...new Set(parseCodeFitMarkers(content3).flatMap((marker) => marker.targets))].sort();
30776
+ return parseLock(readFileSync11(abs, "utf8"));
31157
30777
  }
31158
- function hashTargets(root2, targets) {
30778
+ function hashDependencies(root2, targets) {
31159
30779
  const out = {};
31160
30780
  for (const target of targets) {
31161
30781
  const abs = resolveWritePath(root2, target);
31162
30782
  if (!existsSync13(abs))
31163
- throw new Error(`Cannot attest missing code-fit target: ${target}`);
31164
- out[target] = hash(readFileSync12(abs, "utf8"));
30783
+ throw new Error(`Cannot attest missing review dependency: ${target}`);
30784
+ out[target] = hash(readFileSync11(abs, "utf8"));
31165
30785
  }
31166
30786
  return out;
31167
30787
  }
@@ -31174,10 +30794,10 @@ function sortedLock(lock) {
31174
30794
  documents[path3] = {
31175
30795
  reviewedAt: entry.reviewedAt,
31176
30796
  documentHash: entry.documentHash,
31177
- codeTargets: Object.fromEntries(Object.entries(entry.codeTargets).sort(([a], [b]) => a.localeCompare(b)))
30797
+ reviewDependencies: Object.fromEntries(Object.entries(entry.reviewDependencies).sort(([a], [b]) => a.localeCompare(b)))
31178
30798
  };
31179
30799
  }
31180
- return { version: 1, documents };
30800
+ return { version: 2, documents };
31181
30801
  }
31182
30802
  function validateReviewedAt(reviewedAt) {
31183
30803
  if (!isCalendarDate(reviewedAt)) {
@@ -31190,7 +30810,7 @@ function commitWrites(root2, writes) {
31190
30810
  try {
31191
30811
  for (const [relPath2, content3] of writes) {
31192
30812
  const abs = resolveWritePath(root2, relPath2);
31193
- originals.set(relPath2, existsSync13(abs) ? readFileSync12(abs, "utf8") : null);
30813
+ originals.set(relPath2, existsSync13(abs) ? readFileSync11(abs, "utf8") : null);
31194
30814
  mkdirSync2(dirname8(abs), { recursive: true });
31195
30815
  writeFileSync3(abs, content3, "utf8");
31196
30816
  written.push(relPath2);
@@ -31231,7 +30851,7 @@ function attestDocuments(options) {
31231
30851
  const abs = resolveWritePath(ctx.root, relPath2);
31232
30852
  if (!existsSync13(abs))
31233
30853
  throw new Error(`Cannot attest missing document: ${relPath2}`);
31234
- const original = readFileSync12(abs, "utf8");
30854
+ const original = readFileSync11(abs, "utf8");
31235
30855
  if (!DOC_META_RE.test(original)) {
31236
30856
  throw new Error(`Cannot attest ${relPath2}: missing doc-meta comment`);
31237
30857
  }
@@ -31241,7 +30861,7 @@ function attestDocuments(options) {
31241
30861
  lock.documents[relPath2] = {
31242
30862
  reviewedAt,
31243
30863
  documentHash: hash(updated),
31244
- codeTargets: hashTargets(ctx.root, codeTargets(updated))
30864
+ reviewDependencies: hashDependencies(ctx.root, resolveReviewDependencies(ctx.root, reviewDependencyPatterns(updated)).targets)
31245
30865
  };
31246
30866
  }
31247
30867
  }
@@ -31259,16 +30879,32 @@ function attestDocuments(options) {
31259
30879
  modifiedFiles: options.dryRun ? [] : [...writes.keys()]
31260
30880
  };
31261
30881
  }
31262
- function changedTargetIssue(relPath2, target) {
30882
+ function changedDependencyIssue(relPath2, target) {
31263
30883
  return issue("review-proof", relPath2, {
31264
- code: "review-code-target-changed",
31265
- message: `code-fit target changed after the recorded review: ${target}`,
30884
+ code: "review-dependency-changed",
30885
+ message: `review dependency changed after the recorded review: ${target}`,
31266
30886
  link: target,
31267
- remediation: "Re-read the entire document against the current target, then attest it again."
30887
+ remediation: "Re-read the entire document against the current dependencies, then attest it again."
31268
30888
  });
31269
30889
  }
31270
- function validateEntry(input) {
31271
- const { ctx, relPath: relPath2, content: content3, entry } = input;
30890
+ function validateEntry(input) {
30891
+ const { ctx, relPath: relPath2, content: content3, entry } = input;
30892
+ const issues = documentProofIssues(relPath2, content3, entry);
30893
+ const currentTargets = currentDependencies({ root: ctx.root, relPath: relPath2, content: content3, issues });
30894
+ if (!currentTargets)
30895
+ return issues;
30896
+ const recordedTargets = Object.keys(entry.reviewDependencies).sort();
30897
+ if (JSON.stringify(currentTargets) !== JSON.stringify(recordedTargets)) {
30898
+ issues.push(issue("review-proof", relPath2, {
30899
+ code: "review-dependency-set-changed",
30900
+ message: "review dependency set changed after the recorded review"
30901
+ }));
30902
+ return issues;
30903
+ }
30904
+ issues.push(...dependencyHashIssues({ root: ctx.root, relPath: relPath2, targets: currentTargets, entry }));
30905
+ return issues;
30906
+ }
30907
+ function documentProofIssues(relPath2, content3, entry) {
31272
30908
  const issues = [];
31273
30909
  if (entry.reviewedAt !== docMetaLastReviewed(content3)) {
31274
30910
  issues.push(issue("review-proof", relPath2, {
@@ -31283,27 +30919,33 @@ function validateEntry(input) {
31283
30919
  remediation: "Re-read the entire document, then attest it again."
31284
30920
  }));
31285
30921
  }
31286
- const currentTargets = codeTargets(content3);
31287
- const recordedTargets = Object.keys(entry.codeTargets).sort();
31288
- if (JSON.stringify(currentTargets) !== JSON.stringify(recordedTargets)) {
30922
+ return issues;
30923
+ }
30924
+ function currentDependencies(input) {
30925
+ const { root: root2, relPath: relPath2, content: content3, issues } = input;
30926
+ try {
30927
+ return resolveReviewDependencies(root2, reviewDependencyPatterns(content3)).targets;
30928
+ } catch (error) {
31289
30929
  issues.push(issue("review-proof", relPath2, {
31290
- code: "review-code-target-set-changed",
31291
- message: "code-fit target set changed after the recorded review"
30930
+ code: "review-dependency-invalid",
30931
+ message: error instanceof Error ? error.message : String(error)
31292
30932
  }));
31293
- return issues;
30933
+ return null;
31294
30934
  }
31295
- for (const target of currentTargets) {
31296
- const abs = resolveWritePath(ctx.root, target);
30935
+ }
30936
+ function dependencyHashIssues(input) {
30937
+ const { root: root2, relPath: relPath2, targets, entry } = input;
30938
+ const issues = [];
30939
+ for (const target of targets) {
30940
+ const abs = resolveWritePath(root2, target);
31297
30941
  if (!existsSync13(abs)) {
31298
30942
  issues.push(issue("review-proof", relPath2, {
31299
- code: "review-code-target-missing",
31300
- message: `recorded code-fit target is missing: ${target}`,
30943
+ code: "review-dependency-missing",
30944
+ message: `recorded review dependency is missing: ${target}`,
31301
30945
  link: target
31302
30946
  }));
31303
- continue;
31304
- }
31305
- if (entry.codeTargets[target] !== hash(readFileSync12(abs, "utf8"))) {
31306
- issues.push(changedTargetIssue(relPath2, target));
30947
+ } else if (entry.reviewDependencies[target] !== hash(readFileSync11(abs, "utf8"))) {
30948
+ issues.push(changedDependencyIssue(relPath2, target));
31307
30949
  }
31308
30950
  }
31309
30951
  return issues;
@@ -31321,7 +30963,7 @@ function runReviewProofRule(ctx) {
31321
30963
  })
31322
30964
  ];
31323
30965
  }
31324
- const lock = parseLock(readFileSync12(absLock, "utf8"));
30966
+ const lock = parseLock(readFileSync11(absLock, "utf8"));
31325
30967
  if (!lock) {
31326
30968
  return [
31327
30969
  issue("review-proof", relLock, {
@@ -31335,7 +30977,7 @@ function runReviewProofRule(ctx) {
31335
30977
  const abs = resolveWritePath(ctx.root, relPath2);
31336
30978
  if (!existsSync13(abs))
31337
30979
  continue;
31338
- const content3 = readFileSync12(abs, "utf8");
30980
+ const content3 = readFileSync11(abs, "utf8");
31339
30981
  const entry = lock.documents[normalizeRelPath(relative6(ctx.root, abs))];
31340
30982
  if (!entry) {
31341
30983
  issues.push(issue("review-proof", relPath2, {
@@ -31352,8 +30994,8 @@ function runReviewProofRule(ctx) {
31352
30994
  var reviewProofRule = { id: "review-proof", run: runReviewProofRule };
31353
30995
 
31354
30996
  // src/references/check.ts
31355
- import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync14 } from "node:fs";
31356
- import { join as join13, relative as relative8 } from "node:path";
30997
+ import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync13 } from "node:fs";
30998
+ import { join as join12, relative as relative8 } from "node:path";
31357
30999
 
31358
31000
  // src/references/constants.ts
31359
31001
  var CANONICAL_REFS_DIR = ".skeleton/references";
@@ -31376,8 +31018,8 @@ function isGeneratedReference(content3) {
31376
31018
  }
31377
31019
 
31378
31020
  // src/references/discover.ts
31379
- import { existsSync as existsSync14, readdirSync as readdirSync2, readFileSync as readFileSync13 } from "node:fs";
31380
- import { join as join12, relative as relative7 } from "node:path";
31021
+ import { existsSync as existsSync14, readdirSync as readdirSync2, readFileSync as readFileSync12 } from "node:fs";
31022
+ import { join as join11, relative as relative7 } from "node:path";
31381
31023
  function walkMarkdownFiles(dir, root2) {
31382
31024
  const files = [];
31383
31025
  if (!existsSync14(dir))
@@ -31385,7 +31027,7 @@ function walkMarkdownFiles(dir, root2) {
31385
31027
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
31386
31028
  if (entry.name.startsWith("."))
31387
31029
  continue;
31388
- const fullPath = join12(dir, entry.name);
31030
+ const fullPath = join11(dir, entry.name);
31389
31031
  if (entry.isDirectory()) {
31390
31032
  files.push(...walkMarkdownFiles(fullPath, root2));
31391
31033
  continue;
@@ -31397,7 +31039,7 @@ function walkMarkdownFiles(dir, root2) {
31397
31039
  return files;
31398
31040
  }
31399
31041
  function canonicalExists(root2, refPath) {
31400
- return existsSync14(join12(root2, CANONICAL_REFS_DIR, refPath));
31042
+ return existsSync14(join11(root2, CANONICAL_REFS_DIR, refPath));
31401
31043
  }
31402
31044
  function findSharedRefLinks(content3, sourceFile) {
31403
31045
  const links = [];
@@ -31421,7 +31063,7 @@ function findSiblingRefLinks(root2, content3, sourceFile) {
31421
31063
  const raw = normalizeRelPath(match[1] ?? "");
31422
31064
  if (!raw)
31423
31065
  continue;
31424
- const refPath = withinDir ? normalizeRelPath(join12(withinDir, raw)) : raw;
31066
+ const refPath = withinDir ? normalizeRelPath(join11(withinDir, raw)) : raw;
31425
31067
  if (!canonicalExists(root2, refPath))
31426
31068
  continue;
31427
31069
  links.push({ refPath, sourceFile });
@@ -31441,7 +31083,7 @@ function findLocalCanonicalLinks(root2, content3, sourceFile) {
31441
31083
  return links;
31442
31084
  }
31443
31085
  function collectLinksForFile(root2, relFile) {
31444
- const content3 = readFileSync13(join12(root2, relFile), "utf8");
31086
+ const content3 = readFileSync12(join11(root2, relFile), "utf8");
31445
31087
  if (isGeneratedReference(content3))
31446
31088
  return [];
31447
31089
  return [
@@ -31456,7 +31098,7 @@ function expandTransitiveRefs(input) {
31456
31098
  const refPath = queue.pop();
31457
31099
  if (!(refPath && canonicalExists(root2, refPath)))
31458
31100
  continue;
31459
- const canonicalContent = readFileSync13(join12(root2, CANONICAL_REFS_DIR, refPath), "utf8");
31101
+ const canonicalContent = readFileSync12(join11(root2, CANONICAL_REFS_DIR, refPath), "utf8");
31460
31102
  const syntheticSource = generatedRefPath(skillDir, refPath);
31461
31103
  for (const link2 of findLocalCanonicalLinks(root2, canonicalContent, syntheticSource)) {
31462
31104
  if (refPaths.has(link2.refPath))
@@ -31468,8 +31110,8 @@ function expandTransitiveRefs(input) {
31468
31110
  }
31469
31111
  }
31470
31112
  function planForSkill(root2, slug2, skillDir) {
31471
- const absSkillDir = join12(root2, skillDir);
31472
- if (!existsSync14(join12(absSkillDir, "SKILL.md")))
31113
+ const absSkillDir = join11(root2, skillDir);
31114
+ if (!existsSync14(join11(absSkillDir, "SKILL.md")))
31473
31115
  return null;
31474
31116
  const refPaths = new Set;
31475
31117
  const links = [];
@@ -31485,8 +31127,8 @@ function planForSkill(root2, slug2, skillDir) {
31485
31127
  function concreteSkillDirs(root2, index2, slug2) {
31486
31128
  const dirs = [];
31487
31129
  for (const skillRoot of index2.roots) {
31488
- const rel = normalizeRelPath(skillRoot.kind === "nested" ? join12(skillRoot.relPath, slug2) : slug2);
31489
- if (existsSync14(join12(root2, rel, "SKILL.md")))
31130
+ const rel = normalizeRelPath(skillRoot.kind === "nested" ? join11(skillRoot.relPath, slug2) : slug2);
31131
+ if (existsSync14(join11(root2, rel, "SKILL.md")))
31490
31132
  dirs.push(rel);
31491
31133
  }
31492
31134
  return [...new Set(dirs)];
@@ -31504,7 +31146,7 @@ function discoverSkillReferencePlans(root2, ownership) {
31504
31146
  return plans.sort((a, b) => a.skillDir.localeCompare(b.skillDir));
31505
31147
  }
31506
31148
  function generatedRefPath(skillDir, refPath) {
31507
- return normalizeRelPath(join12(skillDir, "references", refPath));
31149
+ return normalizeRelPath(join11(skillDir, "references", refPath));
31508
31150
  }
31509
31151
  function rewriteSharedRefTarget(sourceFile, skillDir, refPath) {
31510
31152
  const sourceDir = sourceFile.slice(0, sourceFile.lastIndexOf("/"));
@@ -31536,7 +31178,7 @@ function walkMarkdown(dir, onFile) {
31536
31178
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
31537
31179
  if (entry.name.startsWith("."))
31538
31180
  continue;
31539
- const fullPath = join13(dir, entry.name);
31181
+ const fullPath = join12(dir, entry.name);
31540
31182
  if (entry.isDirectory()) {
31541
31183
  walkMarkdown(fullPath, onFile);
31542
31184
  continue;
@@ -31549,7 +31191,7 @@ function walkMarkdown(dir, onFile) {
31549
31191
  function listAllGeneratedFiles(root2) {
31550
31192
  const files = [];
31551
31193
  walkMarkdown(root2, (fullPath) => {
31552
- const content3 = readFileSync14(fullPath, "utf8");
31194
+ const content3 = readFileSync13(fullPath, "utf8");
31553
31195
  if (isGeneratedReference(content3)) {
31554
31196
  files.push(normalizeRelPath(relative8(root2, fullPath)));
31555
31197
  }
@@ -31557,21 +31199,21 @@ function listAllGeneratedFiles(root2) {
31557
31199
  return files;
31558
31200
  }
31559
31201
  function checkNeededCopy(root2, targetRel) {
31560
- const targetPath = join13(root2, targetRel);
31202
+ const targetPath = join12(root2, targetRel);
31561
31203
  if (!existsSync15(targetPath)) {
31562
31204
  return issue("generated-references", targetRel, "missing generated copy — run skeleton references sync");
31563
31205
  }
31564
- const generated = readFileSync14(targetPath, "utf8");
31206
+ const generated = readFileSync13(targetPath, "utf8");
31565
31207
  if (!isGeneratedReference(generated)) {
31566
31208
  return issue("generated-references", targetRel, "expected generated-reference provenance header");
31567
31209
  }
31568
31210
  const body = stripGeneratedHeader(generated);
31569
- const sourceRel = normalizeRelPath(generated.match(/source: ([^\n]+)/)?.[1] ?? join13(CANONICAL_REFS_DIR, targetRel.split("/references/")[1] ?? ""));
31570
- const canonicalPath = join13(root2, sourceRel);
31211
+ const sourceRel = normalizeRelPath(generated.match(/source: ([^\n]+)/)?.[1] ?? join12(CANONICAL_REFS_DIR, targetRel.split("/references/")[1] ?? ""));
31212
+ const canonicalPath = join12(root2, sourceRel);
31571
31213
  if (!existsSync15(canonicalPath)) {
31572
31214
  return issue("generated-references", targetRel, `canonical source missing: ${sourceRel}`);
31573
31215
  }
31574
- const canonical = readFileSync14(canonicalPath, "utf8");
31216
+ const canonical = readFileSync13(canonicalPath, "utf8");
31575
31217
  if (body !== canonical) {
31576
31218
  return issue("generated-references", targetRel, "stale generated copy — run skeleton references sync");
31577
31219
  }
@@ -31592,7 +31234,7 @@ function checkStaleSharedLinks(root2, skillDir) {
31592
31234
  const issues = [];
31593
31235
  walkMarkdown(skillDir, (fullPath) => {
31594
31236
  const relFile = normalizeRelPath(relative8(root2, fullPath));
31595
- const content3 = readFileSync14(fullPath, "utf8");
31237
+ const content3 = readFileSync13(fullPath, "utf8");
31596
31238
  if (!content3.match(SHARED_REF_LINK_RE))
31597
31239
  return;
31598
31240
  issues.push(issue("generated-references", relFile, "still links to shared root references/ — run skeleton references sync"));
@@ -31601,7 +31243,7 @@ function checkStaleSharedLinks(root2, skillDir) {
31601
31243
  }
31602
31244
  function runGeneratedReferencesCheck(root2, ownership) {
31603
31245
  const issues = [];
31604
- const canonicalDir = join13(root2, CANONICAL_REFS_DIR);
31246
+ const canonicalDir = join12(root2, CANONICAL_REFS_DIR);
31605
31247
  if (!existsSync15(canonicalDir))
31606
31248
  return issues;
31607
31249
  const skillIndex = buildSkillIndex(root2, ownership);
@@ -31619,7 +31261,7 @@ function runGeneratedReferencesCheck(root2, ownership) {
31619
31261
  }
31620
31262
  issues.push(...checkOrphanedCopies(root2, needed, skillIndex));
31621
31263
  for (const plan of plans) {
31622
- issues.push(...checkStaleSharedLinks(root2, join13(root2, plan.skillDir)));
31264
+ issues.push(...checkStaleSharedLinks(root2, join12(root2, plan.skillDir)));
31623
31265
  }
31624
31266
  return issues;
31625
31267
  }
@@ -31643,38 +31285,9 @@ function runBannedRule(ctx) {
31643
31285
  }
31644
31286
  var bannedRule = { id: "banned", run: runBannedRule };
31645
31287
 
31646
- // src/audit/rules/code-fit.ts
31647
- import { relative as relative9 } from "node:path";
31648
- function runCodeFitRule(ctx) {
31649
- const overlapMin = ctx.config.docsLint?.codeFitOverlapMin ?? DEFAULT_CODE_FIT_OVERLAP_MIN;
31650
- const surfaceCap = ctx.config.docsLint?.codeFitSurfaceCap ?? DEFAULT_CODE_FIT_SURFACE_CAP;
31651
- const options = { root: ctx.root, overlapMin, surfaceCap };
31652
- const corpus = collectScanFiles(ctx.config, ctx.root, ctx.skillIndex);
31653
- const issues = [];
31654
- for (const abs of corpus) {
31655
- const content3 = readFileContent(abs);
31656
- if (!parseCodeFitMarkers(content3).length)
31657
- continue;
31658
- const rel = normalizeRelPath(relative9(ctx.root, abs));
31659
- for (const fit of evaluateCodeFitDoc(rel, content3, options)) {
31660
- issues.push(issue("code-fit", fit.path, {
31661
- message: fit.message,
31662
- link: fit.link,
31663
- severity: "error"
31664
- }));
31665
- }
31666
- }
31667
- return issues;
31668
- }
31669
- var codeFitRule = {
31670
- id: "code-fit",
31671
- alwaysRun: true,
31672
- run: runCodeFitRule
31673
- };
31674
-
31675
31288
  // src/audit/rules/doc-meta.ts
31676
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "node:fs";
31677
- import { join as join14 } from "node:path";
31289
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "node:fs";
31290
+ import { join as join13 } from "node:path";
31678
31291
  function checkDocMetaBanner(relPath2, content3) {
31679
31292
  if (DOC_META_RE.test(content3))
31680
31293
  return null;
@@ -31725,136 +31338,359 @@ function checkGitFreshness(input) {
31725
31338
  remediation: "Re-read the entire document, then use explicit review attestation for that path."
31726
31339
  });
31727
31340
  }
31728
- function runDocMetaRule(ctx) {
31729
- const issues = [];
31730
- const today = new Date;
31731
- for (const relPath2 of ctx.docMetaPaths) {
31732
- const abs = join14(ctx.root, relPath2);
31733
- if (!existsSync16(abs))
31341
+ function runDocMetaRule(ctx) {
31342
+ const issues = [];
31343
+ const today = new Date;
31344
+ for (const relPath2 of ctx.docMetaPaths) {
31345
+ const abs = join13(ctx.root, relPath2);
31346
+ if (!existsSync16(abs))
31347
+ continue;
31348
+ const content3 = readFileSync14(abs, "utf8");
31349
+ const banner = checkDocMetaBanner(relPath2, content3);
31350
+ if (banner) {
31351
+ issues.push(banner);
31352
+ continue;
31353
+ }
31354
+ const stale = checkStaleReview({
31355
+ relPath: relPath2,
31356
+ content: content3,
31357
+ today,
31358
+ staleDays: ctx.config.daysUntilStale
31359
+ });
31360
+ if (stale)
31361
+ issues.push(stale);
31362
+ const git = checkGitFreshness({
31363
+ relPath: relPath2,
31364
+ content: content3,
31365
+ root: ctx.root,
31366
+ lockedSkillSlugs: ctx.lockedSkillSlugs
31367
+ });
31368
+ if (git)
31369
+ issues.push(git);
31370
+ }
31371
+ return issues;
31372
+ }
31373
+ var docMetaRule = { id: "doc-meta", run: runDocMetaRule };
31374
+
31375
+ // src/audit/rules/links.ts
31376
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "node:fs";
31377
+ import { dirname as dirname9, resolve as resolve5 } from "node:path";
31378
+ function resolveLink2(sourceFile, target) {
31379
+ const withoutAnchor = target.split("#")[0]?.split("?")[0] ?? "";
31380
+ if (!withoutAnchor)
31381
+ return sourceFile;
31382
+ return resolve5(dirname9(sourceFile), withoutAnchor);
31383
+ }
31384
+ function checkMissingSkill(input, relSource) {
31385
+ if (!input.target.includes("/SKILL.md"))
31386
+ return null;
31387
+ const slug2 = SKILL_LINK_IN_TARGET_RE.exec(input.target)?.[1];
31388
+ if (!(slug2 && !resolveSkillPath(input.ctx.skillIndex, input.ctx.root, slug2)))
31389
+ return null;
31390
+ return issue("links", relSource, {
31391
+ message: `missing skill "${slug2}/SKILL.md"`,
31392
+ link: input.linkLabel
31393
+ });
31394
+ }
31395
+ function checkAgentFile(input, resolved, relSource) {
31396
+ if (!((input.target.includes(".claude/agents/") || input.target.includes(".cursor/agents/")) && input.target.endsWith(".md"))) {
31397
+ return null;
31398
+ }
31399
+ const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
31400
+ if (existsSync17(agentPath))
31401
+ return null;
31402
+ return issue("links", relSource, { message: "missing agent file", link: input.linkLabel });
31403
+ }
31404
+ function checkBrokenPath(ctx) {
31405
+ const { input, pathPart, resolved, relSource, relTarget } = ctx;
31406
+ if (!(pathPart && !existsSync17(resolved)))
31407
+ return null;
31408
+ return issue("links", relSource, {
31409
+ message: `broken link → ${relTarget}`,
31410
+ link: input.linkLabel
31411
+ });
31412
+ }
31413
+ function checkBrokenAnchor(ctx) {
31414
+ const { input, anchor, resolved, relSource, relTarget } = ctx;
31415
+ if (!(anchor && existsSync17(resolved)))
31416
+ return null;
31417
+ const targetContent = readFileSync15(resolved, "utf8");
31418
+ const slugs = extractHeadingSlugs(targetContent, resolved);
31419
+ const anchorSlug = slugifyAnchor(anchor);
31420
+ if (slugs.has(anchorSlug))
31421
+ return null;
31422
+ return issue("links", relSource, {
31423
+ message: `broken anchor → #${anchor} in ${relTarget}`,
31424
+ link: input.linkLabel
31425
+ });
31426
+ }
31427
+ function resolveTargetParts(sourceFile, target, root2) {
31428
+ const relSource = relPath(sourceFile, root2);
31429
+ const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
31430
+ const pathPart = target.split("#")[0]?.split("?")[0] ?? "";
31431
+ const resolved = resolveLink2(sourceFile, target);
31432
+ const relTarget = relPath(resolved, root2);
31433
+ return { relSource, anchor, pathPart, resolved, relTarget };
31434
+ }
31435
+ function validateTarget(input) {
31436
+ const { ctx, sourceFile, target } = input;
31437
+ if (isExternalLink(target) && !target.startsWith("#"))
31438
+ return [];
31439
+ if (isPlaceholderLink(target))
31440
+ return [];
31441
+ const parts = resolveTargetParts(sourceFile, target, ctx.root);
31442
+ const missingSkill = checkMissingSkill(input, parts.relSource);
31443
+ if (missingSkill)
31444
+ return [missingSkill];
31445
+ const agent = checkAgentFile(input, parts.resolved, parts.relSource);
31446
+ if (agent)
31447
+ return [agent];
31448
+ const brokenPath = checkBrokenPath({ input, ...parts });
31449
+ if (brokenPath)
31450
+ return [brokenPath];
31451
+ const brokenAnchor = checkBrokenAnchor({ input, ...parts });
31452
+ return brokenAnchor ? [brokenAnchor] : [];
31453
+ }
31454
+ function runLinksRule(ctx) {
31455
+ const issues = [];
31456
+ for (const filePath of ctx.files) {
31457
+ const content3 = readFileContent(filePath);
31458
+ const links = extractLinksFromMarkdown(content3, filePath);
31459
+ for (const { target, line } of links) {
31460
+ const linkLabel = line ? `line ${line}` : target;
31461
+ issues.push(...validateTarget({ ctx, sourceFile: filePath, target, linkLabel }));
31462
+ }
31463
+ }
31464
+ return issues;
31465
+ }
31466
+ var linksRule = { id: "links", run: runLinksRule };
31467
+
31468
+ // src/audit/rules/near-duplicate.ts
31469
+ import { readFileSync as readFileSync16 } from "node:fs";
31470
+ import { join as join14 } from "node:path";
31471
+
31472
+ // src/audit/core/ssot-fit.ts
31473
+ var DEFAULT_SSOT_OVERLAP_MIN = 0.35;
31474
+ var DEFAULT_BETTER_MATCH_MARGIN = 0.15;
31475
+ var STOP = new Set([
31476
+ "a",
31477
+ "an",
31478
+ "the",
31479
+ "and",
31480
+ "or",
31481
+ "of",
31482
+ "for",
31483
+ "to",
31484
+ "in",
31485
+ "on",
31486
+ "with",
31487
+ "this",
31488
+ "that",
31489
+ "is",
31490
+ "are",
31491
+ "be",
31492
+ "as",
31493
+ "by",
31494
+ "from",
31495
+ "at",
31496
+ "it",
31497
+ "its"
31498
+ ]);
31499
+ var STEM_BLOCKLIST = new Set([
31500
+ "business",
31501
+ "analysis",
31502
+ "status",
31503
+ "process",
31504
+ "access",
31505
+ "address",
31506
+ "series",
31507
+ "species",
31508
+ "news",
31509
+ "means",
31510
+ "cross",
31511
+ "class",
31512
+ "glass",
31513
+ "less",
31514
+ "success",
31515
+ "progress",
31516
+ "express",
31517
+ "discuss",
31518
+ "focus",
31519
+ "bonus",
31520
+ "basis",
31521
+ "crisis",
31522
+ "thesis",
31523
+ "atlas",
31524
+ "canvas",
31525
+ "campus",
31526
+ "virus",
31527
+ "bus",
31528
+ "gas",
31529
+ "plus",
31530
+ "alias",
31531
+ "bias",
31532
+ "circus",
31533
+ "consensus",
31534
+ "census"
31535
+ ]);
31536
+ function lightStem(token) {
31537
+ if (token.length < 4)
31538
+ return token;
31539
+ if (STEM_BLOCKLIST.has(token))
31540
+ return token;
31541
+ if (token.endsWith("ies") && token.length > 4) {
31542
+ return `${token.slice(0, -3)}y`;
31543
+ }
31544
+ if (token.endsWith("sses") || token.endsWith("ches") || token.endsWith("shes") || token.endsWith("xes")) {
31545
+ return token.slice(0, -2);
31546
+ }
31547
+ if (token.endsWith("s") && !token.endsWith("ss") && !token.endsWith("us") && !token.endsWith("is")) {
31548
+ return token.slice(0, -1);
31549
+ }
31550
+ return token;
31551
+ }
31552
+ function contentTokens(text5) {
31553
+ return text5.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 1 && !STOP.has(t)).map(lightStem);
31554
+ }
31555
+ function uniqueContentTokens(text5) {
31556
+ return [...new Set(contentTokens(text5))];
31557
+ }
31558
+ function stripCode2(content3) {
31559
+ return content3.replace(/```[\s\S]*?```/g, `
31560
+ `).replace(/`[^`\n]+`/g, " ");
31561
+ }
31562
+ function stripSsotAndMeta(content3) {
31563
+ return content3.replace(/<!--\s*source-of-truth:[\s\S]*?-->/gi, `
31564
+ `).replace(/^\s*source-of-truth:\s*.+$/gim, `
31565
+ `).replace(/^\s*\*\*Source of truth for\*\*\s*.+$/gim, `
31566
+ `).replace(/<!--\s*doc-meta:[\s\S]*?-->/gi, `
31567
+ `);
31568
+ }
31569
+ function extractH1(content3) {
31570
+ const prose = stripCode2(content3);
31571
+ const m = /^#\s+(.+)$/m.exec(prose);
31572
+ return m?.[1]?.trim() ?? "";
31573
+ }
31574
+ function extractLeadParagraph(content3) {
31575
+ const prose = stripSsotAndMeta(stripCode2(content3));
31576
+ const lines = prose.split(/\n/);
31577
+ const chunks = [];
31578
+ let buf = [];
31579
+ const flush = () => {
31580
+ const t = buf.join(" ").trim();
31581
+ if (t)
31582
+ chunks.push(t);
31583
+ buf = [];
31584
+ };
31585
+ for (const line of lines) {
31586
+ const trimmed = line.trim();
31587
+ if (!trimmed || trimmed.startsWith("#")) {
31588
+ flush();
31734
31589
  continue;
31735
- const content3 = readFileSync15(abs, "utf8");
31736
- const banner = checkDocMetaBanner(relPath2, content3);
31737
- if (banner) {
31738
- issues.push(banner);
31590
+ }
31591
+ if (/^[-*|]/.test(trimmed) && chunks.length === 0 && buf.length === 0) {
31592
+ buf.push(trimmed.replace(/^[-*|]+\s*/, ""));
31739
31593
  continue;
31740
31594
  }
31741
- const stale = checkStaleReview({
31742
- relPath: relPath2,
31743
- content: content3,
31744
- today,
31745
- staleDays: ctx.config.daysUntilStale
31746
- });
31747
- if (stale)
31748
- issues.push(stale);
31749
- const git = checkGitFreshness({
31750
- relPath: relPath2,
31751
- content: content3,
31752
- root: ctx.root,
31753
- lockedSkillSlugs: ctx.lockedSkillSlugs
31754
- });
31755
- if (git)
31756
- issues.push(git);
31595
+ buf.push(trimmed);
31757
31596
  }
31758
- return issues;
31597
+ flush();
31598
+ return chunks[0] ?? "";
31759
31599
  }
31760
- var docMetaRule = { id: "doc-meta", run: runDocMetaRule };
31600
+ function buildEvidenceText(content3) {
31601
+ const h1 = extractH1(content3);
31602
+ const lead = extractLeadParagraph(content3);
31603
+ const body = stripSsotAndMeta(stripCode2(content3));
31604
+ return [h1, lead, body].filter(Boolean).join(`
31761
31605
 
31762
- // src/audit/rules/links.ts
31763
- import { existsSync as existsSync17, readFileSync as readFileSync16 } from "node:fs";
31764
- import { dirname as dirname9, resolve as resolve5 } from "node:path";
31765
- function resolveLink2(sourceFile, target) {
31766
- const withoutAnchor = target.split("#")[0]?.split("?")[0] ?? "";
31767
- if (!withoutAnchor)
31768
- return sourceFile;
31769
- return resolve5(dirname9(sourceFile), withoutAnchor);
31770
- }
31771
- function checkMissingSkill(input, relSource) {
31772
- if (!input.target.includes("/SKILL.md"))
31773
- return null;
31774
- const slug2 = SKILL_LINK_IN_TARGET_RE.exec(input.target)?.[1];
31775
- if (!(slug2 && !resolveSkillPath(input.ctx.skillIndex, input.ctx.root, slug2)))
31776
- return null;
31777
- return issue("links", relSource, {
31778
- message: `missing skill "${slug2}/SKILL.md"`,
31779
- link: input.linkLabel
31780
- });
31606
+ `);
31781
31607
  }
31782
- function checkAgentFile(input, resolved, relSource) {
31783
- if (!((input.target.includes(".claude/agents/") || input.target.includes(".cursor/agents/")) && input.target.endsWith(".md"))) {
31784
- return null;
31608
+ function ssotEvidenceOverlap(summary, evidence) {
31609
+ const st = uniqueContentTokens(summary);
31610
+ if (st.length === 0)
31611
+ return 0;
31612
+ const ev = new Set(contentTokens(evidence));
31613
+ let hit = 0;
31614
+ for (const t of st) {
31615
+ if (ev.has(t))
31616
+ hit++;
31785
31617
  }
31786
- const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
31787
- if (existsSync17(agentPath))
31788
- return null;
31789
- return issue("links", relSource, { message: "missing agent file", link: input.linkLabel });
31790
- }
31791
- function checkBrokenPath(ctx) {
31792
- const { input, pathPart, resolved, relSource, relTarget } = ctx;
31793
- if (!(pathPart && !existsSync17(resolved)))
31794
- return null;
31795
- return issue("links", relSource, {
31796
- message: `broken link → ${relTarget}`,
31797
- link: input.linkLabel
31798
- });
31799
- }
31800
- function checkBrokenAnchor(ctx) {
31801
- const { input, anchor, resolved, relSource, relTarget } = ctx;
31802
- if (!(anchor && existsSync17(resolved)))
31803
- return null;
31804
- const targetContent = readFileSync16(resolved, "utf8");
31805
- const slugs = extractHeadingSlugs(targetContent, resolved);
31806
- const anchorSlug = slugifyAnchor(anchor);
31807
- if (slugs.has(anchorSlug))
31808
- return null;
31809
- return issue("links", relSource, {
31810
- message: `broken anchor → #${anchor} in ${relTarget}`,
31811
- link: input.linkLabel
31812
- });
31618
+ return hit / st.length;
31813
31619
  }
31814
- function resolveTargetParts(sourceFile, target, root2) {
31815
- const relSource = relPath(sourceFile, root2);
31816
- const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
31817
- const pathPart = target.split("#")[0]?.split("?")[0] ?? "";
31818
- const resolved = resolveLink2(sourceFile, target);
31819
- const relTarget = relPath(resolved, root2);
31820
- return { relSource, anchor, pathPart, resolved, relTarget };
31620
+ function longestSummaryPhrase(summary) {
31621
+ const toks = contentTokens(summary);
31622
+ if (toks.length >= 3)
31623
+ return toks.slice(0, 3);
31624
+ if (toks.length >= 2)
31625
+ return toks.slice(0, 2);
31626
+ return null;
31821
31627
  }
31822
- function validateTarget(input) {
31823
- const { ctx, sourceFile, target } = input;
31824
- if (isExternalLink(target) && !target.startsWith("#"))
31825
- return [];
31826
- if (isPlaceholderLink(target))
31827
- return [];
31828
- const parts = resolveTargetParts(sourceFile, target, ctx.root);
31829
- const missingSkill = checkMissingSkill(input, parts.relSource);
31830
- if (missingSkill)
31831
- return [missingSkill];
31832
- const agent = checkAgentFile(input, parts.resolved, parts.relSource);
31833
- if (agent)
31834
- return [agent];
31835
- const brokenPath = checkBrokenPath({ input, ...parts });
31836
- if (brokenPath)
31837
- return [brokenPath];
31838
- const brokenAnchor = checkBrokenAnchor({ input, ...parts });
31839
- return brokenAnchor ? [brokenAnchor] : [];
31628
+ function evidenceHasPhrase(evidence, phrase) {
31629
+ if (phrase.length === 0)
31630
+ return true;
31631
+ const ev = contentTokens(evidence);
31632
+ const needle = phrase.join(" ");
31633
+ for (let i = 0;i <= ev.length - phrase.length; i++) {
31634
+ if (ev.slice(i, i + phrase.length).join(" ") === needle)
31635
+ return true;
31636
+ }
31637
+ return false;
31840
31638
  }
31841
- function runLinksRule(ctx) {
31639
+ function evaluateSsotFit(files, options = {}) {
31640
+ const overlapMin = options.overlapMin ?? DEFAULT_SSOT_OVERLAP_MIN;
31641
+ const margin = options.betterMatchMargin ?? DEFAULT_BETTER_MATCH_MARGIN;
31642
+ const phraseCheck = options.phraseCheck !== false;
31643
+ const prepared = files.map((f) => {
31644
+ const evidence = buildEvidenceText(f.content);
31645
+ const overlap = ssotEvidenceOverlap(f.summary, evidence);
31646
+ const summaryToks = uniqueContentTokens(f.summary);
31647
+ return { ...f, evidence, overlap, summaryToks };
31648
+ });
31842
31649
  const issues = [];
31843
- for (const filePath of ctx.files) {
31844
- const content3 = readFileContent(filePath);
31845
- const links = extractLinksFromMarkdown(content3, filePath);
31846
- for (const { target, line } of links) {
31847
- const linkLabel = line ? `line ${line}` : target;
31848
- issues.push(...validateTarget({ ctx, sourceFile: filePath, target, linkLabel }));
31650
+ for (const row of prepared) {
31651
+ if (row.summaryToks.length < 2) {
31652
+ issues.push({
31653
+ kind: "short",
31654
+ path: row.path,
31655
+ message: `source-of-truth summary too short to verify against body ("${row.summary}")`
31656
+ });
31657
+ continue;
31658
+ }
31659
+ if (row.overlap < overlapMin) {
31660
+ let message = `source-of-truth summary weakly matches this paper (token overlap ${row.overlap.toFixed(2)} < ${overlapMin})`;
31661
+ if (phraseCheck) {
31662
+ const phrase = longestSummaryPhrase(row.summary);
31663
+ if (phrase && !evidenceHasPhrase(row.evidence, phrase)) {
31664
+ message += ` — key phrase "${phrase.join(" ")}" not found in H1/lead/body`;
31665
+ }
31666
+ }
31667
+ issues.push({ kind: "weak", path: row.path, message });
31668
+ let best = null;
31669
+ for (const other of prepared) {
31670
+ if (other.path === row.path)
31671
+ continue;
31672
+ const cross = ssotEvidenceOverlap(row.summary, other.evidence);
31673
+ if (cross < overlapMin)
31674
+ continue;
31675
+ if (cross < row.overlap + margin)
31676
+ continue;
31677
+ if (!best || cross > best.overlap)
31678
+ best = { path: other.path, overlap: cross };
31679
+ }
31680
+ if (best) {
31681
+ issues.push({
31682
+ kind: "better-match",
31683
+ path: row.path,
31684
+ otherPath: best.path,
31685
+ message: `source-of-truth fits ${best.path} better (overlap ${best.overlap.toFixed(2)} vs own ${row.overlap.toFixed(2)}). ` + `Try: (1) rewrite this SSOT to match this paper, (2) move/fix the marker onto ${best.path}, ` + `or (3) if these pages are really one topic, consider combining them`
31686
+ });
31687
+ }
31849
31688
  }
31850
31689
  }
31851
31690
  return issues;
31852
31691
  }
31853
- var linksRule = { id: "links", run: runLinksRule };
31854
31692
 
31855
31693
  // src/audit/rules/near-duplicate.ts
31856
- import { readFileSync as readFileSync17 } from "node:fs";
31857
- import { join as join15 } from "node:path";
31858
31694
  var DEFAULT_THRESHOLD = 0.72;
31859
31695
  var SHINGLE_N = 3;
31860
31696
  function tokenize2(text5) {
@@ -31919,7 +31755,7 @@ function runNearDuplicateRule(ctx) {
31919
31755
  const ignored = ignoredPairSet(ctx);
31920
31756
  const entries = eligibleEntries(ctx);
31921
31757
  const fingerprints = entries.map((e) => {
31922
- const content3 = readFileSync17(join15(ctx.root, e.path), "utf8");
31758
+ const content3 = readFileSync16(join14(ctx.root, e.path), "utf8");
31923
31759
  const tokens = tokenize2(bodyWithoutSsotNoise(content3));
31924
31760
  return {
31925
31761
  path: e.path,
@@ -32013,6 +31849,44 @@ function runProsePolicyRule(ctx) {
32013
31849
  return issues;
32014
31850
  }
32015
31851
  var prosePolicyRule = { id: "prose-policy", run: runProsePolicyRule };
31852
+
31853
+ // src/audit/rules/review-deps.ts
31854
+ import { relative as relative9 } from "node:path";
31855
+ function runReviewDepsRule(ctx) {
31856
+ const issues = [];
31857
+ for (const abs of collectScanFiles(ctx.config, ctx.root, ctx.skillIndex)) {
31858
+ const path3 = normalizeRelPath(relative9(ctx.root, abs));
31859
+ for (const marker of parseReviewDepsMarkers(readFileContent(abs))) {
31860
+ issues.push(...validateMarker(ctx.root, path3, marker.paths));
31861
+ }
31862
+ }
31863
+ return issues;
31864
+ }
31865
+ function validateMarker(root2, path3, dependencies) {
31866
+ if (dependencies.length === 0)
31867
+ return [issue("review-deps", path3, "review-deps marker missing paths=")];
31868
+ return dependencies.flatMap((dependency) => validateDependency(root2, path3, dependency));
31869
+ }
31870
+ function validateDependency(root2, path3, dependency) {
31871
+ if (!isSafeReviewDependencyPath(dependency)) {
31872
+ return [issue("review-deps", path3, `invalid review dependency path: ${dependency}`)];
31873
+ }
31874
+ try {
31875
+ const resolved = resolveReviewDependencies(root2, [dependency]);
31876
+ if (!isReviewDependencyGlob(dependency) || resolved.targets.length > 0)
31877
+ return [];
31878
+ return [
31879
+ issue("review-deps", path3, {
31880
+ code: "review-dependency-glob-empty",
31881
+ message: `review dependency glob matches no files: ${dependency}`,
31882
+ severity: "warning"
31883
+ })
31884
+ ];
31885
+ } catch (error) {
31886
+ return [issue("review-deps", path3, error instanceof Error ? error.message : String(error))];
31887
+ }
31888
+ }
31889
+ var reviewDepsRule = { id: "review-deps", alwaysRun: true, run: runReviewDepsRule };
32016
31890
  // src/audit/rules/scan-gaps.ts
32017
31891
  function runCoverageGapsRule(ctx) {
32018
31892
  const exclude = [...COVERAGE_BUILTIN_EXCLUDES, ...ctx.config.scan.exclude];
@@ -32042,8 +31916,8 @@ function runScanRootsRule(ctx) {
32042
31916
  var scanRootsRule = { id: "scan-roots", run: runScanRootsRule };
32043
31917
 
32044
31918
  // src/audit/rules/skill-index.ts
32045
- import { existsSync as existsSync18, readdirSync as readdirSync4, readFileSync as readFileSync18 } from "node:fs";
32046
- import { join as join16, relative as relative10 } from "node:path";
31919
+ import { existsSync as existsSync18, readdirSync as readdirSync4, readFileSync as readFileSync17 } from "node:fs";
31920
+ import { join as join15, relative as relative10 } from "node:path";
32047
31921
  function walkSkillMarkdown(dir) {
32048
31922
  const files = [];
32049
31923
  if (!existsSync18(dir))
@@ -32051,7 +31925,7 @@ function walkSkillMarkdown(dir) {
32051
31925
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
32052
31926
  if (entry.name.startsWith("."))
32053
31927
  continue;
32054
- const fullPath = join16(dir, entry.name);
31928
+ const fullPath = join15(dir, entry.name);
32055
31929
  if (entry.isDirectory()) {
32056
31930
  files.push(...walkSkillMarkdown(fullPath));
32057
31931
  continue;
@@ -32078,7 +31952,7 @@ function parseReadmeTaxonomySlugs(content3) {
32078
31952
  function scanFileForSkillLinks(ctx, filePath, index2) {
32079
31953
  const issues = [];
32080
31954
  const rel = relative10(ctx.root, filePath).replace(/\\/g, "/");
32081
- const content3 = readFileSync18(filePath, "utf8");
31955
+ const content3 = readFileSync17(filePath, "utf8");
32082
31956
  if (isGeneratedReference(content3))
32083
31957
  return issues;
32084
31958
  for (const match of content3.matchAll(SKILL_LINK_RE)) {
@@ -32093,14 +31967,14 @@ function scanFileForSkillLinks(ctx, filePath, index2) {
32093
31967
  }
32094
31968
  function taxonomyIssuesForReadme(input) {
32095
31969
  const { ctx, index: index2, skillRoot, diskSlugs, nonPublic } = input;
32096
- const readmePath = join16(ctx.root, skillRoot.relPath, "README.md");
31970
+ const readmePath = join15(ctx.root, skillRoot.relPath, "README.md");
32097
31971
  if (!existsSync18(readmePath))
32098
31972
  return [];
32099
- const readme = readFileSync18(readmePath, "utf8");
31973
+ const readme = readFileSync17(readmePath, "utf8");
32100
31974
  if (!readme.includes("## Taxonomy"))
32101
31975
  return [];
32102
31976
  const taxonomySlugs = parseReadmeTaxonomySlugs(readme);
32103
- const nestedSlugs = diskSlugs.filter((slug2) => existsSync18(join16(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
31977
+ const nestedSlugs = diskSlugs.filter((slug2) => existsSync18(join15(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
32104
31978
  const foreign = new Set(index2.foreignSlugs);
32105
31979
  const publicSlugs = nestedSlugs.filter((slug2) => !(nonPublic.has(slug2) || foreign.has(slug2)));
32106
31980
  const relReadme = `${skillRoot.relPath}/README.md`;
@@ -32133,9 +32007,9 @@ function slugsForRoot(skillRoot, index2, owned) {
32133
32007
  function auditSkillRoot(input) {
32134
32008
  const { ctx, index: index2, skillRoot, owned } = input;
32135
32009
  const issues = [];
32136
- const base = skillRoot.kind === "nested" ? join16(ctx.root, skillRoot.relPath) : ctx.root;
32010
+ const base = skillRoot.kind === "nested" ? join15(ctx.root, skillRoot.relPath) : ctx.root;
32137
32011
  for (const slug2 of slugsForRoot(skillRoot, index2, owned)) {
32138
- const skillDir = join16(base, slug2);
32012
+ const skillDir = join15(base, slug2);
32139
32013
  if (!existsSync18(skillDir))
32140
32014
  continue;
32141
32015
  for (const skillMd of walkSkillMarkdown(skillDir)) {
@@ -32186,8 +32060,8 @@ function runSsotRule(ctx) {
32186
32060
  var ssotRule = { id: "ssot", run: runSsotRule };
32187
32061
 
32188
32062
  // src/audit/rules/ssot-summary.ts
32189
- import { readFileSync as readFileSync19 } from "node:fs";
32190
- import { join as join17 } from "node:path";
32063
+ import { readFileSync as readFileSync18 } from "node:fs";
32064
+ import { join as join16 } from "node:path";
32191
32065
  function runSsotSummaryRule(ctx) {
32192
32066
  const overlapMin = ctx.config.docsLint?.ssotOverlapMin ?? DEFAULT_SSOT_OVERLAP_MIN;
32193
32067
  const margin = ctx.config.docsLint?.ssotBetterMatchMargin ?? DEFAULT_BETTER_MATCH_MARGIN;
@@ -32195,7 +32069,7 @@ function runSsotSummaryRule(ctx) {
32195
32069
  const files = ctx.ssotEntries.map((entry) => ({
32196
32070
  path: entry.path,
32197
32071
  summary: entry.summary,
32198
- content: readFileSync19(join17(ctx.root, entry.path), "utf8")
32072
+ content: readFileSync18(join16(ctx.root, entry.path), "utf8")
32199
32073
  }));
32200
32074
  return evaluateSsotFit(files, {
32201
32075
  overlapMin,
@@ -32219,7 +32093,7 @@ var docsRules = [
32219
32093
  linksRule,
32220
32094
  docMetaRule,
32221
32095
  reviewProofRule,
32222
- codeFitRule,
32096
+ reviewDepsRule,
32223
32097
  { ...bannedRule, global: true },
32224
32098
  prosePolicyRule
32225
32099
  ];
@@ -32562,19 +32436,19 @@ async function runAudit(options) {
32562
32436
  }
32563
32437
 
32564
32438
  // src/customize/resolve.ts
32565
- import { existsSync as existsSync19, readFileSync as readFileSync20 } from "node:fs";
32566
- import { basename as basename3, join as join18, relative as relative11 } from "node:path";
32439
+ import { existsSync as existsSync19, readFileSync as readFileSync19 } from "node:fs";
32440
+ import { basename as basename3, join as join17, relative as relative11 } from "node:path";
32567
32441
  function customizeDir(root2) {
32568
- return join18(root2, REGISTRY_DIR_REL, "customize");
32442
+ return join17(root2, REGISTRY_DIR_REL, "customize");
32569
32443
  }
32570
32444
  function customizePathForSlug(root2, slug2) {
32571
- return join18(customizeDir(root2), `${slug2}.md`);
32445
+ return join17(customizeDir(root2), `${slug2}.md`);
32572
32446
  }
32573
32447
  function resolveSlugFile(root2, slug2) {
32574
32448
  const direct = customizePathForSlug(root2, slug2);
32575
32449
  if (existsSync19(direct)) {
32576
32450
  return {
32577
- content: readFileSync20(direct, "utf8"),
32451
+ content: readFileSync19(direct, "utf8"),
32578
32452
  path: normalizeRelPath(relative11(root2, direct))
32579
32453
  };
32580
32454
  }
@@ -32596,10 +32470,10 @@ function readAlwaysInclude(root2, basenames, skipBasename) {
32596
32470
  const file = basename3(name);
32597
32471
  if (skipBasename && file === skipBasename)
32598
32472
  continue;
32599
- const abs = join18(dir, file);
32473
+ const abs = join17(dir, file);
32600
32474
  if (!existsSync19(abs))
32601
32475
  continue;
32602
- parts.push(readFileSync20(abs, "utf8").trimEnd());
32476
+ parts.push(readFileSync19(abs, "utf8").trimEnd());
32603
32477
  paths.push(normalizeRelPath(relative11(root2, abs)));
32604
32478
  }
32605
32479
  return { parts, paths };
@@ -32723,29 +32597,29 @@ Customize override for /${slug2} (from ${from}):
32723
32597
 
32724
32598
  // src/init/init.ts
32725
32599
  import { spawnSync as spawnSync2 } from "node:child_process";
32726
- import { copyFileSync, existsSync as existsSync23, mkdirSync as mkdirSync4, readFileSync as readFileSync22 } from "node:fs";
32727
- import { join as join22 } from "node:path";
32600
+ import { copyFileSync, existsSync as existsSync23, mkdirSync as mkdirSync4, readFileSync as readFileSync21 } from "node:fs";
32601
+ import { join as join21 } from "node:path";
32728
32602
  import process6 from "node:process";
32729
32603
 
32730
32604
  // src/init/merge-hooks.ts
32731
- import { existsSync as existsSync22, mkdirSync as mkdirSync3, readFileSync as readFileSync21, writeFileSync as writeFileSync4 } from "node:fs";
32732
- import { dirname as dirname12, join as join21 } from "node:path";
32605
+ import { existsSync as existsSync22, mkdirSync as mkdirSync3, readFileSync as readFileSync20, writeFileSync as writeFileSync4 } from "node:fs";
32606
+ import { dirname as dirname12, join as join20 } from "node:path";
32733
32607
 
32734
32608
  // src/init/package-paths.ts
32735
32609
  import { existsSync as existsSync20 } from "node:fs";
32736
- import { dirname as dirname10, join as join19 } from "node:path";
32610
+ import { dirname as dirname10, join as join18 } from "node:path";
32737
32611
  import { fileURLToPath as fileURLToPath4 } from "node:url";
32738
32612
  var MODULE_DIR = dirname10(fileURLToPath4(import.meta.url));
32739
- var PACKAGE_ROOT_CANDIDATES = [join19(MODULE_DIR, "../.."), join19(MODULE_DIR, "..")];
32613
+ var PACKAGE_ROOT_CANDIDATES = [join18(MODULE_DIR, "../.."), join18(MODULE_DIR, "..")];
32740
32614
  function resolvePackageRoot() {
32741
32615
  for (const candidate of PACKAGE_ROOT_CANDIDATES) {
32742
- if (existsSync20(join19(candidate, "package.json")))
32616
+ if (existsSync20(join18(candidate, "package.json")))
32743
32617
  return candidate;
32744
32618
  }
32745
32619
  throw new Error("Could not resolve @csark0812/skeleton package root");
32746
32620
  }
32747
32621
  function resolveTemplatesDir() {
32748
- const dir = join19(resolvePackageRoot(), "templates/skeleton-init");
32622
+ const dir = join18(resolvePackageRoot(), "templates/skeleton-init");
32749
32623
  if (!existsSync20(dir)) {
32750
32624
  throw new Error("Missing templates/skeleton-init in package");
32751
32625
  }
@@ -32755,7 +32629,7 @@ function resolveTemplatesDir() {
32755
32629
  // src/init/resolve-hook-command.ts
32756
32630
  import { existsSync as existsSync21, realpathSync as realpathSync5 } from "node:fs";
32757
32631
  import { createRequire as createRequire3 } from "node:module";
32758
- import { dirname as dirname11, join as join20, relative as relative12, resolve as resolve6 } from "node:path";
32632
+ import { dirname as dirname11, join as join19, relative as relative12, resolve as resolve6 } from "node:path";
32759
32633
  var PACKAGE_NAME = "@csark0812/skeleton";
32760
32634
  var CLI_DIST = "dist/cli.js";
32761
32635
  var PACKAGE_ROOT = resolvePackageRoot();
@@ -32774,7 +32648,7 @@ function toRepoRelative(cwd, absPath) {
32774
32648
  }
32775
32649
  function tryResolvePublishedCli(cwd) {
32776
32650
  try {
32777
- const req = createRequire3(join20(cwd, "package.json"));
32651
+ const req = createRequire3(join19(cwd, "package.json"));
32778
32652
  return req.resolve(`${PACKAGE_NAME}/${CLI_DIST}`);
32779
32653
  } catch {
32780
32654
  return null;
@@ -32783,7 +32657,7 @@ function tryResolvePublishedCli(cwd) {
32783
32657
  function walkNodeModulesCli(cwd) {
32784
32658
  let dir = cwd;
32785
32659
  while (true) {
32786
- const candidate = join20(dir, "node_modules", PACKAGE_NAME, CLI_DIST);
32660
+ const candidate = join19(dir, "node_modules", PACKAGE_NAME, CLI_DIST);
32787
32661
  if (existsSync21(candidate))
32788
32662
  return candidate;
32789
32663
  const parent = dirname11(dir);
@@ -32825,14 +32699,14 @@ function identityKey(platform, event, matcher) {
32825
32699
  return `skeleton:customize:${platform}:${event}:${matcher}`;
32826
32700
  }
32827
32701
  function loadFragment(name, hookCommand) {
32828
- const raw = readFileSync21(join21(TEMPLATES_DIR, name), "utf8");
32702
+ const raw = readFileSync20(join20(TEMPLATES_DIR, name), "utf8");
32829
32703
  return JSON.parse(raw.replaceAll("{{HOOK_COMMAND}}", hookCommand));
32830
32704
  }
32831
32705
  function readJson(path3) {
32832
32706
  if (!existsSync22(path3))
32833
32707
  return null;
32834
32708
  try {
32835
- return JSON.parse(readFileSync21(path3, "utf8"));
32709
+ return JSON.parse(readFileSync20(path3, "utf8"));
32836
32710
  } catch (error) {
32837
32711
  throw new Error(`Invalid JSON in ${path3}: ${error}`);
32838
32712
  }
@@ -32992,10 +32866,10 @@ function mergeNestedHooks(args) {
32992
32866
  }
32993
32867
  function mergeHookConfigs(opts) {
32994
32868
  const results = [];
32995
- const cursorPath = join21(opts.cwd, ".cursor/hooks.json");
32869
+ const cursorPath = join20(opts.cwd, ".cursor/hooks.json");
32996
32870
  const cursorFragment = loadFragment("cursor-hooks.fragment.json", opts.hookCommand);
32997
32871
  results.push(mergeCursorHooks(cursorPath, cursorFragment, opts));
32998
- const claudePath = join21(opts.cwd, ".claude/settings.json");
32872
+ const claudePath = join20(opts.cwd, ".claude/settings.json");
32999
32873
  const claudeFragment = loadFragment("claude-settings.fragment.json", opts.hookCommand);
33000
32874
  results.push(mergeNestedHooks({
33001
32875
  platform: "claude",
@@ -33004,8 +32878,8 @@ function mergeHookConfigs(opts) {
33004
32878
  eventName: "PostToolUse",
33005
32879
  opts
33006
32880
  }));
33007
- const codexPath = join21(opts.cwd, ".codex/hooks.json");
33008
- if (existsSync22(join21(opts.cwd, ".codex"))) {
32881
+ const codexPath = join20(opts.cwd, ".codex/hooks.json");
32882
+ if (existsSync22(join20(opts.cwd, ".codex"))) {
33009
32883
  const codexFragment = loadFragment("codex-hooks.fragment.json", opts.hookCommand);
33010
32884
  results.push(mergeNestedHooks({
33011
32885
  platform: "codex",
@@ -33020,11 +32894,11 @@ function mergeHookConfigs(opts) {
33020
32894
  return results;
33021
32895
  }
33022
32896
  function mergePackageJsonScripts(cwd) {
33023
- const pkgPath = join21(cwd, "package.json");
32897
+ const pkgPath = join20(cwd, "package.json");
33024
32898
  if (!existsSync22(pkgPath))
33025
32899
  return "skipped";
33026
- const fragment = JSON.parse(readFileSync21(join21(TEMPLATES_DIR, "package.json.scripts.fragment.json"), "utf8"));
33027
- const pkg = JSON.parse(readFileSync21(pkgPath, "utf8"));
32900
+ const fragment = JSON.parse(readFileSync20(join20(TEMPLATES_DIR, "package.json.scripts.fragment.json"), "utf8"));
32901
+ const pkg = JSON.parse(readFileSync20(pkgPath, "utf8"));
33028
32902
  pkg.scripts ??= {};
33029
32903
  let changed = false;
33030
32904
  for (const [key, value] of Object.entries(fragment)) {
@@ -33080,23 +32954,23 @@ function skillsAddArgs(options = {}) {
33080
32954
  // src/init/init.ts
33081
32955
  var TEMPLATES_DIR2 = resolveTemplatesDir();
33082
32956
  function writeScaffold(cwd) {
33083
- const skeletonDir2 = join22(cwd, ".skeleton");
32957
+ const skeletonDir2 = join21(cwd, ".skeleton");
33084
32958
  mkdirSync4(skeletonDir2, { recursive: true });
33085
32959
  let created = false;
33086
- const tomlPath = join22(cwd, "skeleton.toml");
33087
- const legacyYaml = join22(skeletonDir2, "config.yaml");
32960
+ const tomlPath = join21(cwd, "skeleton.toml");
32961
+ const legacyYaml = join21(skeletonDir2, "config.yaml");
33088
32962
  if (!(existsSync23(tomlPath) || existsSync23(legacyYaml))) {
33089
- copyFileSync(join22(TEMPLATES_DIR2, "skeleton.toml"), tomlPath);
32963
+ copyFileSync(join21(TEMPLATES_DIR2, "skeleton.toml"), tomlPath);
33090
32964
  created = true;
33091
32965
  }
33092
- mkdirSync4(join22(skeletonDir2, "customize"), { recursive: true });
32966
+ mkdirSync4(join21(skeletonDir2, "customize"), { recursive: true });
33093
32967
  return created ? "created" : "skipped";
33094
32968
  }
33095
32969
  function assertPackageResolvable(cwd) {
33096
- const pkgPath = join22(cwd, "package.json");
32970
+ const pkgPath = join21(cwd, "package.json");
33097
32971
  if (!existsSync23(pkgPath))
33098
32972
  return;
33099
- const pkg = JSON.parse(readFileSync22(pkgPath, "utf8"));
32973
+ const pkg = JSON.parse(readFileSync21(pkgPath, "utf8"));
33100
32974
  const hasDep = pkg.devDependencies?.["@csark0812/skeleton"] || pkg.dependencies?.["@csark0812/skeleton"];
33101
32975
  if (!hasDep) {
33102
32976
  try {
@@ -33180,7 +33054,7 @@ function parseInitArgs(argv) {
33180
33054
  // src/plugins/build.ts
33181
33055
  import { spawnSync as spawnSync3 } from "node:child_process";
33182
33056
  import { createHash as createHash2 } from "node:crypto";
33183
- import { existsSync as existsSync24, readFileSync as readFileSync23, writeFileSync as writeFileSync5 } from "node:fs";
33057
+ import { existsSync as existsSync24, readFileSync as readFileSync22, writeFileSync as writeFileSync5 } from "node:fs";
33184
33058
  import { basename as basename4, dirname as dirname13, resolve as resolve7 } from "node:path";
33185
33059
  function parseBuildPluginArgs(argv) {
33186
33060
  let check = false;
@@ -33238,7 +33112,7 @@ function sourceFingerprint(tsAbs, seen = new Set) {
33238
33112
  if (seen.has(abs))
33239
33113
  return;
33240
33114
  seen.add(abs);
33241
- const content3 = readFileSync23(abs, "utf8");
33115
+ const content3 = readFileSync22(abs, "utf8");
33242
33116
  hash2.update(basename4(abs));
33243
33117
  hash2.update("\x00");
33244
33118
  hash2.update(content3);
@@ -33286,7 +33160,7 @@ function checkOne(tsAbs) {
33286
33160
  if (!existsSync24(stampAbs)) {
33287
33161
  throw new Error(`Plugin stale: ${mjsAbs} has no fingerprint stamp. Run: skeleton build-plugin`);
33288
33162
  }
33289
- const expected = readFileSync23(stampAbs, "utf8").trim();
33163
+ const expected = readFileSync22(stampAbs, "utf8").trim();
33290
33164
  const actual = sourceFingerprint(tsAbs);
33291
33165
  if (expected !== actual) {
33292
33166
  throw new Error(`Plugin stale: ${mjsAbs} does not match ${tsAbs} (or local imports). Run: skeleton build-plugin`);
@@ -33322,11 +33196,11 @@ import {
33322
33196
  existsSync as existsSync25,
33323
33197
  mkdirSync as mkdirSync5,
33324
33198
  readdirSync as readdirSync5,
33325
- readFileSync as readFileSync24,
33199
+ readFileSync as readFileSync23,
33326
33200
  unlinkSync as unlinkSync2,
33327
33201
  writeFileSync as writeFileSync6
33328
33202
  } from "node:fs";
33329
- import { dirname as dirname14, join as join23, relative as relative13 } from "node:path";
33203
+ import { dirname as dirname14, join as join22, relative as relative13 } from "node:path";
33330
33204
  import process7 from "node:process";
33331
33205
  function resolveOwnership(root2, override) {
33332
33206
  if (override !== undefined)
@@ -33342,7 +33216,7 @@ function walkMarkdownFiles2(dir, root2) {
33342
33216
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
33343
33217
  if (entry.name.startsWith("."))
33344
33218
  continue;
33345
- const fullPath = join23(dir, entry.name);
33219
+ const fullPath = join22(dir, entry.name);
33346
33220
  if (entry.isDirectory()) {
33347
33221
  files.push(...walkMarkdownFiles2(fullPath, root2));
33348
33222
  continue;
@@ -33356,14 +33230,14 @@ function walkMarkdownFiles2(dir, root2) {
33356
33230
  function collectGeneratedInDir(input) {
33357
33231
  const { dir, refsDir, skillDir, files } = input;
33358
33232
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
33359
- const fullPath = join23(dir, entry.name);
33233
+ const fullPath = join22(dir, entry.name);
33360
33234
  if (entry.isDirectory()) {
33361
33235
  collectGeneratedInDir({ dir: fullPath, refsDir, skillDir, files });
33362
33236
  continue;
33363
33237
  }
33364
33238
  if (!entry.name.endsWith(".md"))
33365
33239
  continue;
33366
- const content3 = readFileSync24(fullPath, "utf8");
33240
+ const content3 = readFileSync23(fullPath, "utf8");
33367
33241
  if (!isGeneratedReference(content3))
33368
33242
  continue;
33369
33243
  const refPath = normalizeRelPath(relative13(refsDir, fullPath));
@@ -33371,7 +33245,7 @@ function collectGeneratedInDir(input) {
33371
33245
  }
33372
33246
  }
33373
33247
  function listGeneratedReferenceFiles(root2, skillDir) {
33374
- const refsDir = join23(root2, skillDir, "references");
33248
+ const refsDir = join22(root2, skillDir, "references");
33375
33249
  if (!existsSync25(refsDir))
33376
33250
  return [];
33377
33251
  const files = [];
@@ -33380,18 +33254,18 @@ function listGeneratedReferenceFiles(root2, skillDir) {
33380
33254
  }
33381
33255
  function syncGeneratedCopy(ctx, refPath) {
33382
33256
  const { root: root2, plan, options, result } = ctx;
33383
- const sourceRel = normalizeRelPath(join23(CANONICAL_REFS_DIR, refPath));
33384
- const canonicalPath = join23(root2, sourceRel);
33257
+ const sourceRel = normalizeRelPath(join22(CANONICAL_REFS_DIR, refPath));
33258
+ const canonicalPath = join22(root2, sourceRel);
33385
33259
  if (!existsSync25(canonicalPath)) {
33386
33260
  throw new Error(`canonical reference missing: ${sourceRel}`);
33387
33261
  }
33388
33262
  const targetRel = generatedRefPath(plan.skillDir, refPath);
33389
- const targetPath = join23(root2, targetRel);
33390
- const canonicalContent = readFileSync24(canonicalPath, "utf8");
33263
+ const targetPath = join22(root2, targetRel);
33264
+ const canonicalContent = readFileSync23(canonicalPath, "utf8");
33391
33265
  const nextContent = formatGeneratedHeader(sourceRel) + canonicalContent;
33392
33266
  if (!options.dryRun)
33393
33267
  mkdirSync5(dirname14(targetPath), { recursive: true });
33394
- const existing = existsSync25(targetPath) ? readFileSync24(targetPath, "utf8") : null;
33268
+ const existing = existsSync25(targetPath) ? readFileSync23(targetPath, "utf8") : null;
33395
33269
  if (existing !== nextContent) {
33396
33270
  if (!options.dryRun)
33397
33271
  writeFileSync6(targetPath, nextContent, "utf8");
@@ -33405,8 +33279,8 @@ function rewritePlanLinks(ctx, skillDir) {
33405
33279
  if (options.rewriteLinks === false)
33406
33280
  return;
33407
33281
  for (const relFile of walkMarkdownFiles2(skillDir, root2)) {
33408
- const filePath = join23(root2, relFile);
33409
- const content3 = readFileSync24(filePath, "utf8");
33282
+ const filePath = join22(root2, relFile);
33283
+ const content3 = readFileSync23(filePath, "utf8");
33410
33284
  const next = rewriteSharedRefLinks(content3, relFile, plan.skillDir);
33411
33285
  if (next === content3)
33412
33286
  continue;
@@ -33422,12 +33296,12 @@ function removeStaleGenerated(ctx) {
33422
33296
  if (plan.refPaths.has(refPath))
33423
33297
  continue;
33424
33298
  if (!options.dryRun)
33425
- unlinkSync2(join23(root2, generatedRel));
33299
+ unlinkSync2(join22(root2, generatedRel));
33426
33300
  result.removed.push(generatedRel);
33427
33301
  }
33428
33302
  }
33429
33303
  function syncPlan(ctx) {
33430
- const skillDir = join23(ctx.root, ctx.plan.skillDir);
33304
+ const skillDir = join22(ctx.root, ctx.plan.skillDir);
33431
33305
  for (const refPath of ctx.plan.refPaths) {
33432
33306
  syncGeneratedCopy(ctx, refPath);
33433
33307
  }
@@ -33436,7 +33310,7 @@ function syncPlan(ctx) {
33436
33310
  }
33437
33311
  function syncReferences(options = {}) {
33438
33312
  const root2 = options.root ?? process7.cwd();
33439
- const canonicalDir = join23(root2, CANONICAL_REFS_DIR);
33313
+ const canonicalDir = join22(root2, CANONICAL_REFS_DIR);
33440
33314
  if (!existsSync25(canonicalDir)) {
33441
33315
  throw new Error(`canonical references dir not found: ${CANONICAL_REFS_DIR}`);
33442
33316
  }
@@ -33484,27 +33358,47 @@ function printSyncResult(result) {
33484
33358
 
33485
33359
  // src/validate/changed.ts
33486
33360
  import { spawnSync as spawnSync5 } from "node:child_process";
33487
- import { existsSync as existsSync26, readFileSync as readFileSync25 } from "node:fs";
33488
- import { basename as basename5, extname as extname2, join as join24 } from "node:path";
33361
+ import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
33362
+ import { basename as basename5, extname as extname2, join as join23 } from "node:path";
33489
33363
 
33490
33364
  // src/validate/git-diff.ts
33491
33365
  import { spawnSync as spawnSync4 } from "node:child_process";
33492
33366
  function gitDiffChangedFiles(options = {}) {
33493
33367
  const root2 = options.root ?? findRepoRoot();
33494
- let args;
33495
- if (options.staged) {
33496
- args = ["diff", "--cached", "--name-only", "--diff-filter=ACMR"];
33497
- } else if (options.base) {
33498
- args = ["diff", `${options.base}...HEAD`, "--name-only", "--diff-filter=ACMR"];
33499
- } else {
33500
- args = ["diff", "HEAD", "--name-only", "--diff-filter=ACMR"];
33501
- }
33502
- const proc = spawnSync4("git", args, { cwd: root2, encoding: "utf8" });
33368
+ const proc = spawnSync4("git", gitDiffArgs(options), { cwd: root2, encoding: "utf8" });
33503
33369
  if (proc.status !== 0) {
33504
33370
  throw new Error(proc.stderr?.trim() || "git diff failed");
33505
33371
  }
33506
- return proc.stdout.split(`
33507
- `).map((line) => normalizeRelPath(line.trim())).filter(Boolean);
33372
+ return parseChangedPaths(proc.stdout);
33373
+ }
33374
+ function gitDiffArgs(options) {
33375
+ const prefix = options.staged ? ["diff", "--cached"] : ["diff", options.base ? `${options.base}...HEAD` : "HEAD"];
33376
+ return [...prefix, "--name-status", "--find-renames", "--diff-filter=ACMRD"];
33377
+ }
33378
+ function parseChangedPaths(output) {
33379
+ const changed = new Map;
33380
+ for (const line of output.split(`
33381
+ `))
33382
+ addChangedLine(changed, line);
33383
+ return [...changed.values()];
33384
+ }
33385
+ function addChangedLine(changed, line) {
33386
+ const [status = "", first = "", second] = line.split("\t");
33387
+ if (!first)
33388
+ return;
33389
+ const paths = status.startsWith("R") || status.startsWith("C") ? [first, second] : [first];
33390
+ for (const raw of paths)
33391
+ addChangedPath({ changed, status, first, raw });
33392
+ }
33393
+ function addChangedPath(input) {
33394
+ const { changed, status, first, raw } = input;
33395
+ if (!raw)
33396
+ return;
33397
+ const path3 = normalizeRelPath(raw);
33398
+ changed.set(path3, {
33399
+ path: path3,
33400
+ deleted: status.startsWith("D") || status.startsWith("R") && raw === first
33401
+ });
33508
33402
  }
33509
33403
 
33510
33404
  // src/validate/changed.ts
@@ -33583,25 +33477,25 @@ function validationIssue(code3, file, message) {
33583
33477
  return issue("validate-changed", file, { code: code3, message, severity: "error" });
33584
33478
  }
33585
33479
  function validateJson(relPath2, root2) {
33586
- const abs = join24(root2, relPath2);
33480
+ const abs = join23(root2, relPath2);
33587
33481
  try {
33588
- parseJsonContent(readFileSync25(abs, "utf8"));
33482
+ parseJsonContent(readFileSync24(abs, "utf8"));
33589
33483
  return null;
33590
33484
  } catch (error) {
33591
33485
  return validationIssue("invalid-json", relPath2, `invalid JSON: ${error}`);
33592
33486
  }
33593
33487
  }
33594
33488
  function validatePolicy(relPath2, root2) {
33595
- const abs = join24(root2, relPath2);
33489
+ const abs = join23(root2, relPath2);
33596
33490
  try {
33597
- loadPolicyFile(abs, readFileSync25(abs, "utf8"));
33491
+ loadPolicyFile(abs, readFileSync24(abs, "utf8"));
33598
33492
  return null;
33599
33493
  } catch (error) {
33600
33494
  return validationIssue("invalid-policy", relPath2, `invalid policy: ${error}`);
33601
33495
  }
33602
33496
  }
33603
33497
  function validateShell(relPath2, root2) {
33604
- const abs = join24(root2, relPath2);
33498
+ const abs = join23(root2, relPath2);
33605
33499
  const shellcheck = spawnSync5("shellcheck", [abs], { encoding: "utf8" });
33606
33500
  if (shellcheck.status === 0)
33607
33501
  return null;
@@ -33612,20 +33506,24 @@ function validateShell(relPath2, root2) {
33612
33506
  }
33613
33507
  function resolvePaths(options) {
33614
33508
  if (options.paths && options.paths.length > 0) {
33615
- return options.paths.map((p) => normalizeRelPath(p));
33509
+ return { paths: options.paths.map((p) => normalizeRelPath(p)), deleted: new Set };
33616
33510
  }
33617
- return gitDiffChangedFiles({
33511
+ const changed = gitDiffChangedFiles({
33618
33512
  staged: options.staged,
33619
33513
  base: options.base,
33620
33514
  root: options.root
33621
33515
  });
33516
+ return {
33517
+ paths: changed.map((entry) => entry.path),
33518
+ deleted: new Set(changed.filter((entry) => entry.deleted).map((entry) => entry.path))
33519
+ };
33622
33520
  }
33623
33521
  function packageManagerFromPackageJson(root2) {
33624
- const pkgPath = join24(root2, "package.json");
33522
+ const pkgPath = join23(root2, "package.json");
33625
33523
  if (!existsSync26(pkgPath))
33626
33524
  return null;
33627
33525
  try {
33628
- const pkg = JSON.parse(readFileSync25(pkgPath, "utf8"));
33526
+ const pkg = JSON.parse(readFileSync24(pkgPath, "utf8"));
33629
33527
  const raw = pkg.packageManager?.split("@")[0];
33630
33528
  if (raw === "bun" || raw === "npm" || raw === "pnpm" || raw === "yarn")
33631
33529
  return raw;
@@ -33633,13 +33531,13 @@ function packageManagerFromPackageJson(root2) {
33633
33531
  return null;
33634
33532
  }
33635
33533
  function packageManagerFromLockfiles(root2) {
33636
- if (existsSync26(join24(root2, "bun.lock")) || existsSync26(join24(root2, "bun.lockb")))
33534
+ if (existsSync26(join23(root2, "bun.lock")) || existsSync26(join23(root2, "bun.lockb")))
33637
33535
  return "bun";
33638
- if (existsSync26(join24(root2, "pnpm-lock.yaml")))
33536
+ if (existsSync26(join23(root2, "pnpm-lock.yaml")))
33639
33537
  return "pnpm";
33640
- if (existsSync26(join24(root2, "yarn.lock")))
33538
+ if (existsSync26(join23(root2, "yarn.lock")))
33641
33539
  return "yarn";
33642
- if (existsSync26(join24(root2, "package-lock.json")))
33540
+ if (existsSync26(join23(root2, "package-lock.json")))
33643
33541
  return "npm";
33644
33542
  return null;
33645
33543
  }
@@ -33665,7 +33563,7 @@ function emptyBuckets() {
33665
33563
  function classifySinglePath(input) {
33666
33564
  const { relPath: relPath2, ctx, state, bucketCtx } = input;
33667
33565
  const normalized = normalizeRelPath(relPath2);
33668
- const abs = join24(ctx.root, normalized);
33566
+ const abs = join23(ctx.root, normalized);
33669
33567
  if (!existsSync26(abs)) {
33670
33568
  state.missing.push(normalized);
33671
33569
  return;
@@ -33730,23 +33628,29 @@ function discoverImpactedDocuments(input) {
33730
33628
  const changed = new Set(input.relPaths.map(normalizeRelPath));
33731
33629
  const impacted = [];
33732
33630
  for (const abs of collectScanFiles(input.config, input.root, input.skillIndex)) {
33733
- const path3 = relPath(abs, input.root);
33734
- const content3 = readFileSync25(abs, "utf8");
33735
- const codeTargets2 = [
33736
- ...new Set(parseCodeFitMarkers(content3).flatMap((marker) => marker.targets.map(normalizeRelPath)))
33737
- ].sort();
33738
- const reasons = [];
33739
- if (changed.has(path3))
33740
- reasons.push({ kind: "changed-document" });
33741
- for (const target of codeTargets2) {
33742
- if (changed.has(target))
33743
- reasons.push({ kind: "changed-code-target", target });
33744
- }
33745
- if (reasons.length > 0)
33746
- impacted.push({ path: path3, codeTargets: codeTargets2, reasons });
33631
+ const document4 = impactedDocumentForPath(abs, input.root, changed);
33632
+ if (document4)
33633
+ impacted.push(document4);
33747
33634
  }
33748
33635
  return impacted.sort((a, b) => a.path.localeCompare(b.path));
33749
33636
  }
33637
+ function impactedDocumentForPath(abs, root2, changed) {
33638
+ const path3 = relPath(abs, root2);
33639
+ const reviewDependencies = reviewDependencyPatterns(readFileSync24(abs, "utf8"));
33640
+ const reasons = impactReasons(path3, reviewDependencies, changed);
33641
+ return reasons.length > 0 ? { path: path3, reviewDependencies, reasons } : null;
33642
+ }
33643
+ function impactReasons(path3, reviewDependencies, changed) {
33644
+ const reasons = changed.has(path3) ? [{ kind: "changed-document" }] : [];
33645
+ for (const dependency of reviewDependencies) {
33646
+ for (const target of changed) {
33647
+ if (reviewDependencyMatchesPath(dependency, target)) {
33648
+ reasons.push({ kind: "changed-review-dependency", dependency, target });
33649
+ }
33650
+ }
33651
+ }
33652
+ return reasons;
33653
+ }
33750
33654
  function dateModeImpactDiagnostics(input) {
33751
33655
  if (input.config.reviewProof)
33752
33656
  return [];
@@ -33754,12 +33658,12 @@ function dateModeImpactDiagnostics(input) {
33754
33658
  const today = formatLocalReviewDate(new Date);
33755
33659
  const diagnostics = [];
33756
33660
  for (const impacted of input.impactedDocuments) {
33757
- if (!impacted.reasons.some((reason) => reason.kind === "changed-code-target"))
33661
+ if (!impacted.reasons.some((reason) => reason.kind === "changed-review-dependency"))
33758
33662
  continue;
33759
- const content3 = readFileSync25(join24(input.root, impacted.path), "utf8");
33663
+ const content3 = readFileSync24(join23(input.root, impacted.path), "utf8");
33760
33664
  if (changed.has(impacted.path) && docMetaLastReviewed(content3) === today)
33761
33665
  continue;
33762
- diagnostics.push(validationIssue("impacted-document-review-required", impacted.path, "a linked code-fit target changed; re-read the entire document, then attest it with --fix=doc-meta --confirm-reviewed and include the document in validation"));
33666
+ diagnostics.push(validationIssue("impacted-document-review-required", impacted.path, "a linked review dependency changed; re-read the entire document, then attest it with --fix=doc-meta --confirm-reviewed and include the document in validation"));
33763
33667
  }
33764
33668
  return diagnostics;
33765
33669
  }
@@ -33840,8 +33744,6 @@ function resultFor(input) {
33840
33744
  const diagnostics = input.diagnostics ?? [];
33841
33745
  const failed = diagnostics.some((item) => item.severity === "error") || audits.some((audit) => audit.exitCode !== 0);
33842
33746
  return {
33843
- schemaVersion: 1,
33844
- command: "validate-changed",
33845
33747
  ok: !failed,
33846
33748
  exitCode: failed ? 1 : 0,
33847
33749
  input: {
@@ -33871,7 +33773,8 @@ function emptyClassification() {
33871
33773
  }
33872
33774
  async function evaluateValidateChanged(options = {}) {
33873
33775
  const root2 = options.root ?? findRepoRoot();
33874
- const relPaths = resolvePaths(options);
33776
+ const resolvedPaths = resolvePaths(options);
33777
+ const relPaths = resolvedPaths.paths;
33875
33778
  if (relPaths.length === 0) {
33876
33779
  return resultFor({ options, relPaths, classification: emptyClassification() });
33877
33780
  }
@@ -33896,6 +33799,7 @@ async function evaluateValidateChanged(options = {}) {
33896
33799
  wiredPolicies,
33897
33800
  skillIndex
33898
33801
  });
33802
+ classification.missing = classification.missing.filter((path3) => !resolvedPaths.deleted.has(path3));
33899
33803
  const impactedDocuments = discoverImpactedDocuments({ relPaths, root: root2, config, skillIndex });
33900
33804
  for (const impacted of impactedDocuments) {
33901
33805
  if (!classification.buckets.docs.includes(impacted.path)) {
@@ -33929,11 +33833,7 @@ async function evaluateValidateChanged(options = {}) {
33929
33833
  diagnostics: evaluated.diagnostics
33930
33834
  });
33931
33835
  }
33932
- function printValidateChangedResult(result, json) {
33933
- if (json) {
33934
- console.log(JSON.stringify(result, null, 2));
33935
- return result.exitCode;
33936
- }
33836
+ function printValidateChangedResult(result) {
33937
33837
  if (result.input.paths.length === 0) {
33938
33838
  console.log("validate changed: no changed files.");
33939
33839
  return result.exitCode;
@@ -33943,6 +33843,13 @@ function printValidateChangedResult(result, json) {
33943
33843
  }
33944
33844
  for (const audit of result.audits)
33945
33845
  printAuditResult(audit, false);
33846
+ for (const impacted of result.impactedDocuments) {
33847
+ for (const reason of impacted.reasons) {
33848
+ if (reason.kind !== "changed-review-dependency")
33849
+ continue;
33850
+ console.log(`validate changed: ${impacted.path} requires review (dependency ${reason.dependency} matched ${reason.target})`);
33851
+ }
33852
+ }
33946
33853
  for (const diagnostic of result.diagnostics) {
33947
33854
  const path3 = diagnostic.file === "." ? "" : `${diagnostic.file}: `;
33948
33855
  console.error(`validate changed: ${path3}${diagnostic.message}`);
@@ -33963,7 +33870,7 @@ function printValidateChangedResult(result, json) {
33963
33870
  return result.exitCode;
33964
33871
  }
33965
33872
  async function runValidateChanged(options = {}) {
33966
- return printValidateChangedResult(await evaluateValidateChanged(options), options.json ?? false);
33873
+ return printValidateChangedResult(await evaluateValidateChanged(options));
33967
33874
  }
33968
33875
 
33969
33876
  // src/cli.ts
@@ -33976,7 +33883,7 @@ Commands:
33976
33883
  [--fix[=doc-meta|anchors|ssot]] [--dry-run]
33977
33884
  [--confirm-reviewed (doc-meta only; requires --paths)]
33978
33885
  build-plugin [path] [--check]
33979
- validate changed [paths…] [--staged] [--base <ref>] [--json]
33886
+ validate changed [paths…] [--staged] [--base <ref>]
33980
33887
  catalog [--check] [--strict] write or check .skeleton/catalog.md (gitignored)
33981
33888
  customize resolve <slug> [--json]
33982
33889
  hook customize (reads a host hook payload on stdin)
@@ -33989,13 +33896,10 @@ function parseValidateChangedArgs(rest) {
33989
33896
  const paths = [];
33990
33897
  let staged = false;
33991
33898
  let base;
33992
- let json = false;
33993
33899
  for (let i = 0;i < rest.length; i++) {
33994
33900
  const arg = rest[i];
33995
33901
  if (arg === "--staged")
33996
33902
  staged = true;
33997
- else if (arg === "--json")
33998
- json = true;
33999
33903
  else if (arg === "--base") {
34000
33904
  const value = rest[++i];
34001
33905
  if (!value || value.startsWith("-"))
@@ -34010,7 +33914,7 @@ function parseValidateChangedArgs(rest) {
34010
33914
  else if (arg && !arg.startsWith("-"))
34011
33915
  paths.push(arg);
34012
33916
  }
34013
- return { paths, staged, base, json };
33917
+ return { paths, staged, base };
34014
33918
  }
34015
33919
  async function handleAudit(argv) {
34016
33920
  const sub = argv[0];
@@ -34034,8 +33938,8 @@ async function handleBuildPlugin(argv) {
34034
33938
  return 0;
34035
33939
  }
34036
33940
  async function handleValidateChanged(argv) {
34037
- const { paths, staged, base, json } = parseValidateChangedArgs(argv);
34038
- return runValidateChanged({ paths, staged, base, json });
33941
+ const { paths, staged, base } = parseValidateChangedArgs(argv);
33942
+ return runValidateChanged({ paths, staged, base });
34039
33943
  }
34040
33944
  function handleRegister() {
34041
33945
  console.error("register: removed — add `<!-- source-of-truth: … -->` (or visible `source-of-truth:`) to the file, then run `skeleton catalog`.");
@@ -34069,7 +33973,7 @@ function handleHook(argv) {
34069
33973
  usage();
34070
33974
  return 1;
34071
33975
  }
34072
- process9.stdout.write(runCustomizeHook(readFileSync26(0, "utf8")));
33976
+ process9.stdout.write(runCustomizeHook(readFileSync25(0, "utf8")));
34073
33977
  return 0;
34074
33978
  }
34075
33979
  function handleInit(argv) {