@promptci/core 0.0.1 → 0.2.0

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.
Files changed (253) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +29 -30
  3. package/dist/action-pinning.d.ts +37 -0
  4. package/dist/action-pinning.d.ts.map +1 -0
  5. package/dist/action-pinning.js +277 -0
  6. package/dist/action-pinning.js.map +1 -0
  7. package/dist/agent-practices.d.ts +3 -1
  8. package/dist/agent-practices.d.ts.map +1 -1
  9. package/dist/agent-practices.js +198 -94
  10. package/dist/agent-practices.js.map +1 -1
  11. package/dist/ai-config.d.ts +178 -0
  12. package/dist/ai-config.d.ts.map +1 -0
  13. package/dist/ai-config.js +721 -0
  14. package/dist/ai-config.js.map +1 -0
  15. package/dist/alias-files.d.ts +33 -0
  16. package/dist/alias-files.d.ts.map +1 -0
  17. package/dist/alias-files.js +60 -0
  18. package/dist/alias-files.js.map +1 -0
  19. package/dist/baseline.d.ts +17 -1
  20. package/dist/baseline.d.ts.map +1 -1
  21. package/dist/baseline.js +5 -1
  22. package/dist/baseline.js.map +1 -1
  23. package/dist/buried-critical.d.ts +17 -0
  24. package/dist/buried-critical.d.ts.map +1 -0
  25. package/dist/buried-critical.js +107 -0
  26. package/dist/buried-critical.js.map +1 -0
  27. package/dist/canonical-owner.d.ts.map +1 -1
  28. package/dist/canonical-owner.js +60 -30
  29. package/dist/canonical-owner.js.map +1 -1
  30. package/dist/ci-alignment.d.ts.map +1 -1
  31. package/dist/ci-alignment.js +3 -2
  32. package/dist/ci-alignment.js.map +1 -1
  33. package/dist/command-validity.d.ts +16 -0
  34. package/dist/command-validity.d.ts.map +1 -1
  35. package/dist/command-validity.js +387 -134
  36. package/dist/command-validity.js.map +1 -1
  37. package/dist/conflicts.d.ts.map +1 -1
  38. package/dist/conflicts.js +4 -44
  39. package/dist/conflicts.js.map +1 -1
  40. package/dist/context-analysis.d.ts.map +1 -1
  41. package/dist/context-analysis.js +13 -2
  42. package/dist/context-analysis.js.map +1 -1
  43. package/dist/context-bloat.d.ts +20 -1
  44. package/dist/context-bloat.d.ts.map +1 -1
  45. package/dist/context-bloat.js +99 -15
  46. package/dist/context-bloat.js.map +1 -1
  47. package/dist/context-optimizer.d.ts.map +1 -1
  48. package/dist/context-optimizer.js +18 -54
  49. package/dist/context-optimizer.js.map +1 -1
  50. package/dist/copilot-instructions-detector.d.ts +24 -0
  51. package/dist/copilot-instructions-detector.d.ts.map +1 -0
  52. package/dist/copilot-instructions-detector.js +145 -0
  53. package/dist/copilot-instructions-detector.js.map +1 -0
  54. package/dist/cursor-rules-detector.d.ts +17 -0
  55. package/dist/cursor-rules-detector.d.ts.map +1 -0
  56. package/dist/cursor-rules-detector.js +110 -0
  57. package/dist/cursor-rules-detector.js.map +1 -0
  58. package/dist/custom-rules.d.ts +52 -0
  59. package/dist/custom-rules.d.ts.map +1 -0
  60. package/dist/custom-rules.js +303 -0
  61. package/dist/custom-rules.js.map +1 -0
  62. package/dist/dead-references.d.ts +8 -2
  63. package/dist/dead-references.d.ts.map +1 -1
  64. package/dist/dead-references.js +315 -68
  65. package/dist/dead-references.js.map +1 -1
  66. package/dist/dependency-staleness.d.ts +35 -0
  67. package/dist/dependency-staleness.d.ts.map +1 -0
  68. package/dist/dependency-staleness.js +1001 -0
  69. package/dist/dependency-staleness.js.map +1 -0
  70. package/dist/detectors.d.ts +1 -3
  71. package/dist/detectors.d.ts.map +1 -1
  72. package/dist/detectors.js +74 -56
  73. package/dist/detectors.js.map +1 -1
  74. package/dist/duplicates.d.ts.map +1 -1
  75. package/dist/duplicates.js +15 -9
  76. package/dist/duplicates.js.map +1 -1
  77. package/dist/evidence.d.ts +39 -0
  78. package/dist/evidence.d.ts.map +1 -0
  79. package/dist/evidence.js +78 -0
  80. package/dist/evidence.js.map +1 -0
  81. package/dist/finding-id.d.ts +21 -0
  82. package/dist/finding-id.d.ts.map +1 -0
  83. package/dist/finding-id.js +31 -0
  84. package/dist/finding-id.js.map +1 -0
  85. package/dist/fix-engine.d.ts +11 -0
  86. package/dist/fix-engine.d.ts.map +1 -1
  87. package/dist/fix-engine.js +88 -71
  88. package/dist/fix-engine.js.map +1 -1
  89. package/dist/frameworks/dotnet.js +2 -2
  90. package/dist/frameworks/dotnet.js.map +1 -1
  91. package/dist/frameworks/python.d.ts.map +1 -1
  92. package/dist/frameworks/python.js +5 -2
  93. package/dist/frameworks/python.js.map +1 -1
  94. package/dist/frameworks/rust.d.ts.map +1 -1
  95. package/dist/frameworks/rust.js +2 -1
  96. package/dist/frameworks/rust.js.map +1 -1
  97. package/dist/git-symlinks.d.ts +30 -0
  98. package/dist/git-symlinks.d.ts.map +1 -0
  99. package/dist/git-symlinks.js +161 -0
  100. package/dist/git-symlinks.js.map +1 -0
  101. package/dist/gitignore.d.ts +22 -0
  102. package/dist/gitignore.d.ts.map +1 -0
  103. package/dist/gitignore.js +157 -0
  104. package/dist/gitignore.js.map +1 -0
  105. package/dist/health-score.d.ts.map +1 -1
  106. package/dist/health-score.js +35 -5
  107. package/dist/health-score.js.map +1 -1
  108. package/dist/hooks-settings-detector.d.ts +17 -0
  109. package/dist/hooks-settings-detector.d.ts.map +1 -0
  110. package/dist/hooks-settings-detector.js +228 -0
  111. package/dist/hooks-settings-detector.js.map +1 -0
  112. package/dist/index.d.ts +29 -8
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +21 -6
  115. package/dist/index.js.map +1 -1
  116. package/dist/makefile.d.ts +37 -0
  117. package/dist/makefile.d.ts.map +1 -0
  118. package/dist/makefile.js +156 -0
  119. package/dist/makefile.js.map +1 -0
  120. package/dist/manifest-consistency.d.ts +6 -0
  121. package/dist/manifest-consistency.d.ts.map +1 -1
  122. package/dist/manifest-consistency.js +75 -10
  123. package/dist/manifest-consistency.js.map +1 -1
  124. package/dist/markdown-fences.d.ts +112 -0
  125. package/dist/markdown-fences.d.ts.map +1 -0
  126. package/dist/markdown-fences.js +174 -0
  127. package/dist/markdown-fences.js.map +1 -0
  128. package/dist/mcp-config-detector.d.ts +20 -0
  129. package/dist/mcp-config-detector.d.ts.map +1 -0
  130. package/dist/mcp-config-detector.js +237 -0
  131. package/dist/mcp-config-detector.js.map +1 -0
  132. package/dist/missing-context.d.ts.map +1 -1
  133. package/dist/missing-context.js +2 -52
  134. package/dist/missing-context.js.map +1 -1
  135. package/dist/model-budgets.d.ts +69 -0
  136. package/dist/model-budgets.d.ts.map +1 -0
  137. package/dist/model-budgets.js +110 -0
  138. package/dist/model-budgets.js.map +1 -0
  139. package/dist/negative-instructions.d.ts +26 -0
  140. package/dist/negative-instructions.d.ts.map +1 -0
  141. package/dist/negative-instructions.js +169 -0
  142. package/dist/negative-instructions.js.map +1 -0
  143. package/dist/path-containment.d.ts +41 -0
  144. package/dist/path-containment.d.ts.map +1 -0
  145. package/dist/path-containment.js +123 -0
  146. package/dist/path-containment.js.map +1 -0
  147. package/dist/project-type.d.ts +12 -2
  148. package/dist/project-type.d.ts.map +1 -1
  149. package/dist/project-type.js +25 -12
  150. package/dist/project-type.js.map +1 -1
  151. package/dist/prompt-cache.d.ts.map +1 -1
  152. package/dist/prompt-cache.js +33 -24
  153. package/dist/prompt-cache.js.map +1 -1
  154. package/dist/promptci-gitignore.d.ts +34 -0
  155. package/dist/promptci-gitignore.d.ts.map +1 -0
  156. package/dist/promptci-gitignore.js +42 -0
  157. package/dist/promptci-gitignore.js.map +1 -0
  158. package/dist/repo-context.d.ts +80 -0
  159. package/dist/repo-context.d.ts.map +1 -1
  160. package/dist/repo-context.js +170 -16
  161. package/dist/repo-context.js.map +1 -1
  162. package/dist/report.d.ts +8 -0
  163. package/dist/report.d.ts.map +1 -1
  164. package/dist/report.js +163 -63
  165. package/dist/report.js.map +1 -1
  166. package/dist/scan.d.ts.map +1 -1
  167. package/dist/scan.js +47 -4
  168. package/dist/scan.js.map +1 -1
  169. package/dist/scanner.d.ts +61 -1
  170. package/dist/scanner.d.ts.map +1 -1
  171. package/dist/scanner.js +367 -70
  172. package/dist/scanner.js.map +1 -1
  173. package/dist/security-pack.d.ts +20 -0
  174. package/dist/security-pack.d.ts.map +1 -1
  175. package/dist/security-pack.js +33 -14
  176. package/dist/security-pack.js.map +1 -1
  177. package/dist/skill-supply-chain.d.ts +132 -0
  178. package/dist/skill-supply-chain.d.ts.map +1 -0
  179. package/dist/skill-supply-chain.js +2954 -0
  180. package/dist/skill-supply-chain.js.map +1 -0
  181. package/dist/skills-detector.d.ts +23 -0
  182. package/dist/skills-detector.d.ts.map +1 -0
  183. package/dist/skills-detector.js +279 -0
  184. package/dist/skills-detector.js.map +1 -0
  185. package/dist/stale-instructions.d.ts +4 -1
  186. package/dist/stale-instructions.d.ts.map +1 -1
  187. package/dist/stale-instructions.js +53 -55
  188. package/dist/stale-instructions.js.map +1 -1
  189. package/dist/subagents-detector.d.ts +19 -0
  190. package/dist/subagents-detector.d.ts.map +1 -0
  191. package/dist/subagents-detector.js +155 -0
  192. package/dist/subagents-detector.js.map +1 -0
  193. package/dist/suppression.d.ts +2 -0
  194. package/dist/suppression.d.ts.map +1 -1
  195. package/dist/suppression.js +16 -47
  196. package/dist/suppression.js.map +1 -1
  197. package/dist/types.d.ts +62 -4
  198. package/dist/types.d.ts.map +1 -1
  199. package/dist/types.js +47 -1
  200. package/dist/types.js.map +1 -1
  201. package/dist/vague-guidance.d.ts +15 -2
  202. package/dist/vague-guidance.d.ts.map +1 -1
  203. package/dist/vague-guidance.js +44 -41
  204. package/dist/vague-guidance.js.map +1 -1
  205. package/dist/windsurf-detector.d.ts +22 -0
  206. package/dist/windsurf-detector.d.ts.map +1 -0
  207. package/dist/windsurf-detector.js +96 -0
  208. package/dist/windsurf-detector.js.map +1 -0
  209. package/dist/within-section-dedup.d.ts +16 -0
  210. package/dist/within-section-dedup.d.ts.map +1 -0
  211. package/dist/within-section-dedup.js +148 -0
  212. package/dist/within-section-dedup.js.map +1 -0
  213. package/dist/workspace-manifests.d.ts +80 -0
  214. package/dist/workspace-manifests.d.ts.map +1 -0
  215. package/dist/workspace-manifests.js +333 -0
  216. package/dist/workspace-manifests.js.map +1 -0
  217. package/package.json +11 -8
  218. package/dist/cheap-model-routing.d.ts +0 -4
  219. package/dist/cheap-model-routing.d.ts.map +0 -1
  220. package/dist/cheap-model-routing.js +0 -43
  221. package/dist/cheap-model-routing.js.map +0 -1
  222. package/dist/cli-output-filter.d.ts +0 -16
  223. package/dist/cli-output-filter.d.ts.map +0 -1
  224. package/dist/cli-output-filter.js +0 -88
  225. package/dist/cli-output-filter.js.map +0 -1
  226. package/dist/context-cost.d.ts +0 -4
  227. package/dist/context-cost.d.ts.map +0 -1
  228. package/dist/context-cost.js +0 -70
  229. package/dist/context-cost.js.map +0 -1
  230. package/dist/context-recommender.d.ts +0 -3
  231. package/dist/context-recommender.d.ts.map +0 -1
  232. package/dist/context-recommender.js +0 -329
  233. package/dist/context-recommender.js.map +0 -1
  234. package/dist/llm.d.ts +0 -6
  235. package/dist/llm.d.ts.map +0 -1
  236. package/dist/llm.js +0 -67
  237. package/dist/llm.js.map +0 -1
  238. package/dist/mcp-detector.d.ts +0 -4
  239. package/dist/mcp-detector.d.ts.map +0 -1
  240. package/dist/mcp-detector.js +0 -131
  241. package/dist/mcp-detector.js.map +0 -1
  242. package/dist/native-tool-preference.d.ts +0 -20
  243. package/dist/native-tool-preference.d.ts.map +0 -1
  244. package/dist/native-tool-preference.js +0 -94
  245. package/dist/native-tool-preference.js.map +0 -1
  246. package/dist/output-verbosity.d.ts +0 -3
  247. package/dist/output-verbosity.d.ts.map +0 -1
  248. package/dist/output-verbosity.js +0 -83
  249. package/dist/output-verbosity.js.map +0 -1
  250. package/dist/product-boundary.d.ts +0 -11
  251. package/dist/product-boundary.d.ts.map +0 -1
  252. package/dist/product-boundary.js +0 -301
  253. package/dist/product-boundary.js.map +0 -1
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Negative-instruction overload detector (FEAT-010).
3
+ *
4
+ * Models follow positive directives ("use const") more reliably than
5
+ * prohibitions ("don't use var"). A file whose directive sentences are
6
+ * overwhelmingly phrased as prohibitions is harder to follow than the same
7
+ * guidance framed positively.
8
+ *
9
+ * Heuristic: split the file into candidate sentences, classify each as a
10
+ * positive directive, a negative directive, or neither, and — when there are
11
+ * enough directives to be meaningful — flag files where more than 40% of the
12
+ * directives are negative. Info severity only; the wording is deliberately
13
+ * cautious and the recommendation offers a positive-rewrite hint.
14
+ *
15
+ * Deterministic and rule-based: no clock, network, or randomness.
16
+ */
17
+ import * as crypto from 'node:crypto';
18
+ import * as path from 'node:path';
19
+ import { INSTRUCTION_FILE_TYPES } from './types.js';
20
+ import { snippet } from './evidence.js';
21
+ import { fileIdPath } from './finding-id.js';
22
+ /** Persistent, always-loaded instruction files this detector applies to. */
23
+ const TARGET_FILE_TYPES = INSTRUCTION_FILE_TYPES;
24
+ /**
25
+ * A sentence is a NEGATIVE directive when it contains one of these prohibition
26
+ * markers. Negative markers take precedence over positive ones, so "always
27
+ * avoid mutation" counts as negative — its operative instruction is a
28
+ * prohibition.
29
+ */
30
+ const NEGATIVE_MARKERS = [
31
+ // Second-person imperative prohibitions only. Third-person "does not" /
32
+ // "doesn't" is descriptive narration ("this file does not repeat it"), not a
33
+ // directive, so it is deliberately excluded to avoid inflating the count.
34
+ /\bdo\s+not\b/i,
35
+ /\bdon['’]?t\b/i,
36
+ /\bnever\b/i,
37
+ /\bavoid\b/i,
38
+ /\brefrain\s+from\b/i,
39
+ /\bmust\s+not\b/i,
40
+ /\bmustn['’]?t\b/i,
41
+ /\bshould\s+not\b/i,
42
+ /\bshouldn['’]?t\b/i,
43
+ /\bcannot\b/i,
44
+ /\bcan['’]?t\b/i,
45
+ /\bwon['’]?t\b/i,
46
+ /\bno\s+longer\b/i,
47
+ ];
48
+ /**
49
+ * A sentence is a POSITIVE directive when it starts with an imperative verb or
50
+ * carries a positive modal ("must"/"should"/"always" not already caught as
51
+ * negative above). This intentionally under-counts rather than over-counts:
52
+ * only clearly directive sentences enter the denominator.
53
+ */
54
+ const POSITIVE_IMPERATIVE_START = /^(use|always|prefer|ensure|make\s+sure|keep|run|write|add|follow|call|set|return|check|verify|read|ask|include|place|put|store|document|test|update|create|define|import|export|name|group|split|choose|pick|apply|treat|surface|confirm|report|explain|favor|favour|default\s+to|stick\s+to|remember\s+to|be\s+sure\s+to)\b/i;
55
+ const POSITIVE_MODAL = /\b(must|should|always|shall)\b/i;
56
+ const MIN_DIRECTIVES = 8; // below this the ratio is too noisy to be meaningful
57
+ const NEGATIVE_RATIO_THRESHOLD = 0.4; // > 40% negative → finding
58
+ /** Strip a leading markdown list bullet / ordered-list marker / heading hash. */
59
+ function stripLeadingMarkers(line) {
60
+ return line
61
+ .replace(/^\s*#{1,6}\s+/, '')
62
+ .replace(/^\s*[-*+]\s+/, '')
63
+ .replace(/^\s*\d+[.)]\s+/, '')
64
+ .replace(/^\s*>\s+/, '')
65
+ .trim();
66
+ }
67
+ /**
68
+ * Strip leading markdown emphasis (`**bold**`, `*italic*`, `` `code` ``) so the
69
+ * operative verb is visible to the imperative test — otherwise "**Report** ..."
70
+ * hides the "Report" that makes the sentence a positive directive.
71
+ */
72
+ function stripLeadingEmphasis(text) {
73
+ return text.replace(/^[*_`]+/, '').trimStart();
74
+ }
75
+ /**
76
+ * Split file content into candidate directive sentences. Fenced code blocks are
77
+ * dropped (a bash comment is not an instruction sentence). Each non-blank line
78
+ * is stripped of list/heading markers, then split on sentence terminators so a
79
+ * line holding two clauses is counted as two.
80
+ */
81
+ export function splitSentences(content) {
82
+ const lines = content.split(/\r?\n/);
83
+ const sentences = [];
84
+ let inFence = false;
85
+ for (const raw of lines) {
86
+ const fenceToggle = /^\s*(```|~~~)/.test(raw);
87
+ if (fenceToggle) {
88
+ inFence = !inFence;
89
+ continue;
90
+ }
91
+ if (inFence)
92
+ continue;
93
+ const cleaned = stripLeadingMarkers(raw);
94
+ if (!cleaned)
95
+ continue;
96
+ for (const part of cleaned.split(/(?<=[.!?])\s+/)) {
97
+ const trimmed = part.trim();
98
+ // Require a few words so table cells / fragments don't count as sentences.
99
+ if (trimmed.split(/\s+/).length >= 3)
100
+ sentences.push(trimmed);
101
+ }
102
+ }
103
+ return sentences;
104
+ }
105
+ function classify(sentence) {
106
+ const lead = stripLeadingEmphasis(sentence);
107
+ // The LEADING imperative governs: a positively-framed instruction that also
108
+ // mentions a prohibition ("Keep diffs focused; do not refactor unrelated
109
+ // code") is a positive directive, not a negative one. Only sentences whose
110
+ // primary verb is not a positive imperative fall through to the prohibition
111
+ // markers.
112
+ if (POSITIVE_IMPERATIVE_START.test(lead))
113
+ return 'positive';
114
+ for (const re of NEGATIVE_MARKERS) {
115
+ if (re.test(sentence))
116
+ return 'negative';
117
+ }
118
+ if (POSITIVE_MODAL.test(sentence))
119
+ return 'positive';
120
+ return 'none';
121
+ }
122
+ function issueId(filePath) {
123
+ const hash = crypto.createHash('sha1').update(`negative-overload:${filePath}`).digest('hex').slice(0, 12);
124
+ return `negative-overload-${hash}`;
125
+ }
126
+ export function detectNegativeInstructionOverload(files) {
127
+ const issues = [];
128
+ for (const file of files) {
129
+ if (!TARGET_FILE_TYPES.has(file.fileType))
130
+ continue;
131
+ const sentences = splitSentences(file.content);
132
+ const negatives = [];
133
+ let positiveCount = 0;
134
+ for (const sentence of sentences) {
135
+ const kind = classify(sentence);
136
+ if (kind === 'negative')
137
+ negatives.push(sentence);
138
+ else if (kind === 'positive')
139
+ positiveCount++;
140
+ }
141
+ const total = negatives.length + positiveCount;
142
+ if (total < MIN_DIRECTIVES)
143
+ continue;
144
+ const ratio = negatives.length / total;
145
+ if (ratio <= NEGATIVE_RATIO_THRESHOLD)
146
+ continue;
147
+ const fileName = path.basename(file.path);
148
+ const pct = Math.round(ratio * 100);
149
+ const examples = negatives.slice(0, 3).map((s) => snippet(s, 100));
150
+ issues.push({
151
+ id: issueId(fileIdPath(file)),
152
+ severity: 'info',
153
+ category: 'agent_practices',
154
+ title: 'High proportion of negative instructions',
155
+ summary: `About ${pct}% of the directive sentences in "${fileName}" are phrased as prohibitions ` +
156
+ `(${negatives.length} of ${total}). Models tend to follow positive directives more ` +
157
+ `reliably than prohibitions, so heavily negative guidance can be harder to apply.`,
158
+ filePaths: [file.path],
159
+ locations: [],
160
+ evidence: examples.map((e) => `Negative directive: "${e}"`),
161
+ recommendation: 'Where practical, rephrase prohibitions as positive directives — e.g. instead of ' +
162
+ '"don\'t use var", write "always use const or let". Keep prohibitions for the cases ' +
163
+ 'where no positive form is possible.',
164
+ confidence: 0.5,
165
+ });
166
+ }
167
+ return issues;
168
+ }
169
+ //# sourceMappingURL=negative-instructions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"negative-instructions.js","sourceRoot":"","sources":["../src/negative-instructions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * True when `candidatePath` resolves to `root` itself or a descendant of it.
3
+ * Both arguments are resolved to absolute form first, so either may be relative.
4
+ */
5
+ export declare function isWithinRoot(root: string, candidatePath: string): boolean;
6
+ /**
7
+ * Resolve `candidatePath` — treated as relative to `root` unless it is itself
8
+ * absolute — and return the absolute path when it stays within `root`, or
9
+ * `null` when it escapes (via `..`, an absolute path, or a different drive).
10
+ */
11
+ export declare function resolveWithinRoot(root: string, candidatePath: string): string | null;
12
+ /** The canonical real path (symlinks resolved), or null when it does not exist. */
13
+ export declare function realPath(p: string): string | null;
14
+ /**
15
+ * True unless `absPath` exists and resolves — through a symlinked file or any
16
+ * symlinked directory on its path — to a location outside `root`. The lexical
17
+ * checks above cannot see a symlink: a committed `CLAUDE.md -> /etc/passwd`
18
+ * or `package.json -> ~/.npmrc` passes them and is then read through. In-repo
19
+ * symlinks stay readable. A path that does not exist (or a dangling link) has
20
+ * nothing to read through, so it is left to the caller's own read to fail.
21
+ */
22
+ export declare function realPathWithinRoot(root: string, absPath: string): boolean;
23
+ /**
24
+ * {@link resolveWithinRoot} for paths about to be READ: additionally `null`
25
+ * when the target is a symlink (or sits under a symlinked directory) whose
26
+ * real path escapes `root`.
27
+ */
28
+ export declare function resolveReadableWithinRoot(root: string, candidatePath: string): string | null;
29
+ /**
30
+ * True when WRITING `target` stays inside `root`. The mirror of the read
31
+ * policy: a path inside the root must not reach outside through a symlink on
32
+ * any component that already exists (a `.promptci` committed as a link to
33
+ * `/etc`, a `latest.md` linked to `~/.bashrc`), including a dangling link whose
34
+ * target a write would create outside. The nearest existing ancestor is
35
+ * checked, so a not-yet-created file is judged by the directory it would land in.
36
+ *
37
+ * A target that is lexically OUTSIDE the root is the caller's explicit choice
38
+ * (`--output /tmp/report.md`) and is allowed.
39
+ */
40
+ export declare function writeTargetWithinRoot(root: string, target: string): boolean;
41
+ //# sourceMappingURL=path-containment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-containment.d.ts","sourceRoot":"","sources":["../src/path-containment.ts"],"names":[],"mappings":"AAsBA;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAYzE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUpF;AAED,mFAAmF;AACnF,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOjD;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAKzE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI5F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAgB3E"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The one path-containment check for the whole toolchain.
3
+ *
4
+ * Every place that resolves an externally-influenced path (a config value, an
5
+ * LLM-proposed edit target, a fix's file path) must confirm the result stays
6
+ * inside the repo/target root before touching the filesystem. This module is
7
+ * the single, tested implementation those checks share.
8
+ *
9
+ * The naive guard this replaces — `resolved.startsWith(root)` — is wrong two
10
+ * ways:
11
+ * - **Sibling prefix.** `/repo-evil` starts with `/repo`, so a sibling
12
+ * directory whose name merely shares a prefix reads as "contained".
13
+ * - **Separator / drive blind spots.** On Windows a raw string prefix ignores
14
+ * the path separator boundary and cross-drive absolutes.
15
+ *
16
+ * Comparing the *relative* path from the root sidesteps both: a contained path
17
+ * yields `''` (the root itself) or a forward walk with no leading `..`; any
18
+ * escape yields a leading `..` segment or an absolute path (a different drive).
19
+ */
20
+ import * as fs from 'node:fs';
21
+ import * as path from 'node:path';
22
+ /**
23
+ * True when `candidatePath` resolves to `root` itself or a descendant of it.
24
+ * Both arguments are resolved to absolute form first, so either may be relative.
25
+ */
26
+ export function isWithinRoot(root, candidatePath) {
27
+ const resolvedRoot = path.resolve(root);
28
+ const resolved = path.resolve(candidatePath);
29
+ const rel = path.relative(resolvedRoot, resolved);
30
+ // '' is the root itself. A genuine escape is exactly '..' or begins with
31
+ // '..<sep>' (a child literally named e.g. '..foo' is NOT an escape), or is
32
+ // absolute — which path.relative only returns when the target is on a
33
+ // different Windows drive and cannot be reached by walking up.
34
+ return (rel === '' ||
35
+ (rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel)));
36
+ }
37
+ /**
38
+ * Resolve `candidatePath` — treated as relative to `root` unless it is itself
39
+ * absolute — and return the absolute path when it stays within `root`, or
40
+ * `null` when it escapes (via `..`, an absolute path, or a different drive).
41
+ */
42
+ export function resolveWithinRoot(root, candidatePath) {
43
+ try {
44
+ const resolvedRoot = path.resolve(root);
45
+ const resolved = path.isAbsolute(candidatePath)
46
+ ? path.resolve(candidatePath)
47
+ : path.resolve(resolvedRoot, candidatePath);
48
+ return isWithinRoot(resolvedRoot, resolved) ? resolved : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ /** The canonical real path (symlinks resolved), or null when it does not exist. */
55
+ export function realPath(p) {
56
+ try {
57
+ const real = fs.realpathSync.native(p);
58
+ return typeof real === 'string' ? real : null;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * True unless `absPath` exists and resolves — through a symlinked file or any
66
+ * symlinked directory on its path — to a location outside `root`. The lexical
67
+ * checks above cannot see a symlink: a committed `CLAUDE.md -> /etc/passwd`
68
+ * or `package.json -> ~/.npmrc` passes them and is then read through. In-repo
69
+ * symlinks stay readable. A path that does not exist (or a dangling link) has
70
+ * nothing to read through, so it is left to the caller's own read to fail.
71
+ */
72
+ export function realPathWithinRoot(root, absPath) {
73
+ const real = realPath(absPath);
74
+ if (real === null)
75
+ return true;
76
+ const realRoot = realPath(root) ?? path.resolve(root);
77
+ return isWithinRoot(realRoot, real);
78
+ }
79
+ /**
80
+ * {@link resolveWithinRoot} for paths about to be READ: additionally `null`
81
+ * when the target is a symlink (or sits under a symlinked directory) whose
82
+ * real path escapes `root`.
83
+ */
84
+ export function resolveReadableWithinRoot(root, candidatePath) {
85
+ const resolved = resolveWithinRoot(root, candidatePath);
86
+ if (resolved === null)
87
+ return null;
88
+ return realPathWithinRoot(root, resolved) ? resolved : null;
89
+ }
90
+ /**
91
+ * True when WRITING `target` stays inside `root`. The mirror of the read
92
+ * policy: a path inside the root must not reach outside through a symlink on
93
+ * any component that already exists (a `.promptci` committed as a link to
94
+ * `/etc`, a `latest.md` linked to `~/.bashrc`), including a dangling link whose
95
+ * target a write would create outside. The nearest existing ancestor is
96
+ * checked, so a not-yet-created file is judged by the directory it would land in.
97
+ *
98
+ * A target that is lexically OUTSIDE the root is the caller's explicit choice
99
+ * (`--output /tmp/report.md`) and is allowed.
100
+ */
101
+ export function writeTargetWithinRoot(root, target) {
102
+ const resolved = path.resolve(target);
103
+ if (!isWithinRoot(root, resolved))
104
+ return true;
105
+ let probe = resolved;
106
+ for (;;) {
107
+ if (realPath(probe) !== null)
108
+ return realPathWithinRoot(root, probe);
109
+ try {
110
+ // lstat succeeding while realpath failed: a dangling symlink, whose target a write would create.
111
+ if (fs.lstatSync(probe).isSymbolicLink())
112
+ return false;
113
+ }
114
+ catch {
115
+ // does not exist: look at its parent
116
+ }
117
+ const parent = path.dirname(probe);
118
+ if (parent === probe)
119
+ return true;
120
+ probe = parent;
121
+ }
122
+ }
123
+ //# sourceMappingURL=path-containment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-containment.js","sourceRoot":"","sources":["../src/path-containment.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,12 @@
1
1
  import type { InstructionFile, ProjectType } from './types.js';
2
2
  /**
3
3
  * Detect the primary project type for a repo at repoRoot.
4
- * Detection order: nextjs → typescript → unity → dotnet → unknown
4
+ * Detection order: nextjs → unity → dotnet → typescript → python → go → rust → unknown
5
+ *
6
+ * Unity/.NET probes run before the package.json→typescript branch: Unity and .NET
7
+ * repos commonly carry a root package.json for JS tooling (husky, prettier), so the
8
+ * bare-package.json check must not shadow their more specific markers. This matches
9
+ * the unity-before-typescript ordering in detectProjectTypeFromContent.
5
10
  */
6
11
  export declare function detectProjectType(repoRoot: string): Promise<ProjectType>;
7
12
  /**
@@ -9,7 +14,12 @@ export declare function detectProjectType(repoRoot: string): Promise<ProjectType
9
14
  * scan returns 'unknown' (e.g. repos that only contain instruction files, no source).
10
15
  * Checks the combined content of all scanned instruction files for strong signals.
11
16
  *
12
- * Detection order: nextjs → unity → typescript → dotnet → python → go → rust → unknown
17
+ * Detection order: nextjs → unity → dotnet → typescript → python → go → rust → unknown
18
+ *
19
+ * .NET is checked before typescript for the same reason as in the file-system
20
+ * probe above: .NET repos commonly mention JS tooling (pnpm, npm run, jest) in
21
+ * their instructions, so the broader typescript signals must not shadow the
22
+ * more specific .NET ones.
13
23
  */
14
24
  export declare function detectProjectTypeFromContent(files: InstructionFile[]): ProjectType;
15
25
  //# sourceMappingURL=project-type.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-type.d.ts","sourceRoot":"","sources":["../src/project-type.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAoB/D;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA4D9E;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,WAAW,CA0BlF"}
1
+ {"version":3,"file":"project-type.d.ts","sourceRoot":"","sources":["../src/project-type.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAoB/D;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA+D9E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,WAAW,CA0BlF"}
@@ -21,7 +21,12 @@ async function dirExists(p) {
21
21
  }
22
22
  /**
23
23
  * Detect the primary project type for a repo at repoRoot.
24
- * Detection order: nextjs → typescript → unity → dotnet → unknown
24
+ * Detection order: nextjs → unity → dotnet → typescript → python → go → rust → unknown
25
+ *
26
+ * Unity/.NET probes run before the package.json→typescript branch: Unity and .NET
27
+ * repos commonly carry a root package.json for JS tooling (husky, prettier), so the
28
+ * bare-package.json check must not shadow their more specific markers. This matches
29
+ * the unity-before-typescript ordering in detectProjectTypeFromContent.
25
30
  */
26
31
  export async function detectProjectType(repoRoot) {
27
32
  const root = path.resolve(repoRoot);
@@ -42,19 +47,17 @@ export async function detectProjectType(repoRoot) {
42
47
  if (hasApp || hasPages)
43
48
  return 'nextjs';
44
49
  }
45
- // ── TypeScript ───────────────────────────────────────────────────────────
46
- // package.json or tsconfig.json present
47
- const hasTsConfig = await exists(path.join(root, 'tsconfig.json'));
48
- if (hasPkgJson || hasTsConfig)
49
- return 'typescript';
50
50
  // ── Unity ────────────────────────────────────────────────────────────────
51
- // Assets/ directory AND ProjectSettings/ProjectVersion.txt both exist
51
+ // Assets/ directory AND ProjectSettings/ProjectVersion.txt both exist.
52
+ // Checked before the package.json→typescript branch so a Unity repo with a
53
+ // root package.json is not misclassified as typescript.
52
54
  const hasAssets = await dirExists(path.join(root, 'Assets'));
53
55
  const hasProjectVersion = await exists(path.join(root, 'ProjectSettings', 'ProjectVersion.txt'));
54
56
  if (hasAssets && hasProjectVersion)
55
57
  return 'unity';
56
58
  // ── .NET ─────────────────────────────────────────────────────────────────
57
- // any .sln or .csproj file at repo root level
59
+ // any .sln or .csproj file at repo root level. Also checked before the
60
+ // typescript branch for the same reason (JS tooling drops a package.json).
58
61
  const dotnetFiles = await fg(['*.sln', '*.csproj'], {
59
62
  cwd: root,
60
63
  dot: false,
@@ -63,6 +66,11 @@ export async function detectProjectType(repoRoot) {
63
66
  });
64
67
  if (dotnetFiles.length > 0)
65
68
  return 'dotnet';
69
+ // ── TypeScript ───────────────────────────────────────────────────────────
70
+ // package.json or tsconfig.json present
71
+ const hasTsConfig = await exists(path.join(root, 'tsconfig.json'));
72
+ if (hasPkgJson || hasTsConfig)
73
+ return 'typescript';
66
74
  // ── Python ───────────────────────────────────────────────────────────────
67
75
  const hasPyProject = await exists(path.join(root, 'pyproject.toml'));
68
76
  const hasRequirements = await exists(path.join(root, 'requirements.txt'));
@@ -86,7 +94,12 @@ export async function detectProjectType(repoRoot) {
86
94
  * scan returns 'unknown' (e.g. repos that only contain instruction files, no source).
87
95
  * Checks the combined content of all scanned instruction files for strong signals.
88
96
  *
89
- * Detection order: nextjs → unity → typescript → dotnet → python → go → rust → unknown
97
+ * Detection order: nextjs → unity → dotnet → typescript → python → go → rust → unknown
98
+ *
99
+ * .NET is checked before typescript for the same reason as in the file-system
100
+ * probe above: .NET repos commonly mention JS tooling (pnpm, npm run, jest) in
101
+ * their instructions, so the broader typescript signals must not shadow the
102
+ * more specific .NET ones.
90
103
  */
91
104
  export function detectProjectTypeFromContent(files) {
92
105
  const combined = files.map((f) => f.content).join('\n');
@@ -96,12 +109,12 @@ export function detectProjectTypeFromContent(files) {
96
109
  if ((/\bunity\b/i.test(combined) && /\b(lts|editor|hub|engine)\b/i.test(combined)) || /\bmonobehaviour\b|\bunityengine\b|\bgameobject\b|\bprefab\b|\bscriptableobject\b/i.test(combined)) {
97
110
  return 'unity';
98
111
  }
99
- if (/\btypescript\b|\btsconfig\b|\btsc\s|\bpnpm\b|\bnpm\s+run\b|\bvitest\b|\bjest\b/i.test(combined)) {
100
- return 'typescript';
101
- }
102
112
  if (/\bdotnet\b|\.sln\b|\.csproj\b|\bxunit\b|\bnunit\b|\bmstest\b/i.test(combined)) {
103
113
  return 'dotnet';
104
114
  }
115
+ if (/\btypescript\b|\btsconfig\b|\btsc\s|\bpnpm\b|\bnpm\s+run\b|\bvitest\b|\bjest\b/i.test(combined)) {
116
+ return 'typescript';
117
+ }
105
118
  if (/\bpython\b|\bpip\b|\bpoetry\b|\bvenv\b|\bpytest\b|\bpyproject\.toml\b/i.test(combined)) {
106
119
  return 'python';
107
120
  }
@@ -1 +1 @@
1
- {"version":3,"file":"project-type.js","sourceRoot":"","sources":["../src/project-type.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"project-type.js","sourceRoot":"","sources":["../src/project-type.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"prompt-cache.d.ts","sourceRoot":"","sources":["../src/prompt-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAqCjE,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE,CAiIvF"}
1
+ {"version":3,"file":"prompt-cache.d.ts","sourceRoot":"","sources":["../src/prompt-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAqDjE,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE,CAiIvF"}
@@ -1,5 +1,8 @@
1
+ import { INSTRUCTION_FILE_TYPES } from './types.js';
2
+ import { matchEvidence } from './evidence.js';
1
3
  import * as path from 'node:path';
2
4
  import * as crypto from 'node:crypto';
5
+ import { fileIdPath } from './finding-id.js';
3
6
  /**
4
7
  * B4: every id in this file was keyed by `${file.fileType}` — two files
5
8
  * sharing a fileType (e.g. CLAUDE.md and .claude/notes.md, both 'claude')
@@ -10,42 +13,48 @@ function promptCacheIssueId(kind, filePath) {
10
13
  return `prompt-cache-${kind}-${hash}`;
11
14
  }
12
15
  const DATE_PATTERNS = [
13
- /\b(?:last\s+updated|last\s+modified|updated\s+at|modified\s+at|date)\b\s*:\s*\b\d{4}-\d{2}-\d{2}\b/i,
14
- /\b\d{4}-\d{2}-\d{2}t\d{2}:\d{2}:\d{2}\b/i,
15
- /\b(?:january|february|march|april|may|june|july|august|september|october|november|december)\s+\d{1,2}(?:st|nd|rd|th)?,\s+\d{4}\b/i,
16
+ {
17
+ re: /\b(?:last\s+updated|last\s+modified|updated\s+at|modified\s+at|date)\b\s*:\s*\b\d{4}-\d{2}-\d{2}\b/i,
18
+ label: 'Dated "last updated"/"modified" line',
19
+ },
20
+ { re: /\b\d{4}-\d{2}-\d{2}t\d{2}:\d{2}:\d{2}\b/i, label: 'ISO 8601 timestamp' },
21
+ {
22
+ re: /\b(?:january|february|march|april|may|june|july|august|september|october|november|december)\s+\d{1,2}(?:st|nd|rd|th)?,\s+\d{4}\b/i,
23
+ label: 'Long-form calendar date',
24
+ },
16
25
  ];
17
26
  const SCAN_REPORT_PATTERNS = [
18
- /health\s+score\s*:\s*\d+/i,
19
- /files\s+scanned\s*:\s*\d+/i,
20
- /promptci\s+report/i,
21
- /\bgenerated\s+by\s+promptci\b/i,
22
- /latest\s+scan\s+results/i,
23
- /scan\s+history/i,
27
+ { re: /health\s+score\s*:\s*\d+/i, label: 'Health-score line from a scan report' },
28
+ { re: /files\s+scanned\s*:\s*\d+/i, label: 'File-count line from a scan report' },
29
+ { re: /promptci\s+report/i, label: 'PromptCI report heading' },
30
+ { re: /\bgenerated\s+by\s+promptci\b/i, label: 'PromptCI generated-by marker' },
31
+ { re: /latest\s+scan\s+results/i, label: 'Pasted "latest scan results" section' },
32
+ { re: /scan\s+history/i, label: 'Pasted "scan history" section' },
24
33
  ];
25
34
  const BRANCH_TASK_PATTERNS = [
26
- /current\s+branch\s*:\s*\S+/i,
27
- /git\s+branch\s*:\s*\S+/i,
28
- /current\s+task\s*:\s*\S+/i,
35
+ { re: /current\s+branch\s*:\s*\S+/i, label: 'Current-branch line' },
36
+ { re: /git\s+branch\s*:\s*\S+/i, label: 'Git-branch line' },
37
+ { re: /current\s+task\s*:\s*\S+/i, label: 'Current-task line' },
29
38
  ];
30
39
  const LOCAL_PATH_PATTERN = /(?:[a-zA-Z]:\\users\\[a-zA-Z0-9_-]+)|(?:\/users\/[a-zA-Z0-9_-]+\/)|(?:\/home\/(?!runner)[a-zA-Z0-9_-]+\/)/i;
31
40
  export function detectPromptCacheFriendliness(files) {
32
41
  const issues = [];
33
42
  // Focus only on persistent, always-loaded instruction files
34
- const targetFiles = files.filter((f) => ['claude', 'agents', 'cursor', 'copilot', 'prompt'].includes(f.fileType));
43
+ const targetFiles = files.filter((f) => INSTRUCTION_FILE_TYPES.has(f.fileType));
35
44
  for (const file of targetFiles) {
36
45
  const fileName = path.basename(file.path);
37
46
  // 1. Timestamps and Dates
38
47
  for (const pat of DATE_PATTERNS) {
39
- if (pat.test(file.content)) {
48
+ if (pat.re.test(file.content)) {
40
49
  issues.push({
41
- id: promptCacheIssueId('volatile-timestamp', file.path),
50
+ id: promptCacheIssueId('volatile-timestamp', fileIdPath(file)),
42
51
  severity: 'warning',
43
52
  category: 'context_bloat',
44
53
  title: 'Volatile timestamp in instruction file',
45
54
  summary: `The instruction file "${fileName}" contains a volatile date or timestamp. Frequently changing dates in persistent instruction files invalidate prompt caches.`,
46
55
  filePaths: [file.path],
47
56
  locations: [],
48
- evidence: [`Matched pattern: ${pat.toString()}`],
57
+ evidence: [matchEvidence(file.content, pat.re, pat.label)],
49
58
  recommendation: 'Move volatile dates to a separate task-specific prompt or scratch file, or remove them from canonical instructions.',
50
59
  fixRecipe: 'Remove the "Last updated: <date>" or timestamp line from the file.',
51
60
  confidence: 0.85,
@@ -55,16 +64,16 @@ export function detectPromptCacheFriendliness(files) {
55
64
  }
56
65
  // 2. Pasted Scan Outputs/Reports
57
66
  for (const pat of SCAN_REPORT_PATTERNS) {
58
- if (pat.test(file.content)) {
67
+ if (pat.re.test(file.content)) {
59
68
  issues.push({
60
- id: promptCacheIssueId('pasted-report', file.path),
69
+ id: promptCacheIssueId('pasted-report', fileIdPath(file)),
61
70
  severity: 'warning',
62
71
  category: 'context_bloat',
63
72
  title: 'Pasted scan report or summary in instructions',
64
73
  summary: `The instruction file "${fileName}" contains pasted scan results or reports. Committing generated reports into canonical instruction files breaks prompt caching and bloats context.`,
65
74
  filePaths: [file.path],
66
75
  locations: [],
67
- evidence: [`Matched pattern: ${pat.toString()}`],
76
+ evidence: [matchEvidence(file.content, pat.re, pat.label)],
68
77
  recommendation: 'Link to the generated report file or read it on-demand instead of pasting its contents into canonical instructions.',
69
78
  fixRecipe: 'Delete the pasted report summary block and reference the report file path instead.',
70
79
  confidence: 0.9,
@@ -74,16 +83,16 @@ export function detectPromptCacheFriendliness(files) {
74
83
  }
75
84
  // 3. Current Branch or Task status
76
85
  for (const pat of BRANCH_TASK_PATTERNS) {
77
- if (pat.test(file.content)) {
86
+ if (pat.re.test(file.content)) {
78
87
  issues.push({
79
- id: promptCacheIssueId('volatile-branch-task', file.path),
88
+ id: promptCacheIssueId('volatile-branch-task', fileIdPath(file)),
80
89
  severity: 'warning',
81
90
  category: 'context_bloat',
82
91
  title: 'Volatile branch or task status in instructions',
83
92
  summary: `The instruction file "${fileName}" contains active git branch or task details. Frequently updated task status or branch names break prompt caching.`,
84
93
  filePaths: [file.path],
85
94
  locations: [],
86
- evidence: [`Matched pattern: ${pat.toString()}`],
95
+ evidence: [matchEvidence(file.content, pat.re, pat.label)],
87
96
  recommendation: 'Move branch-specific notes and active task details to git commit messages, PR descriptions, or a temporary scratch file.',
88
97
  fixRecipe: 'Remove active branch and task notes from the persistent instructions.',
89
98
  confidence: 0.85,
@@ -95,7 +104,7 @@ export function detectPromptCacheFriendliness(files) {
95
104
  if (LOCAL_PATH_PATTERN.test(file.content)) {
96
105
  const match = file.content.match(LOCAL_PATH_PATTERN)?.[0] ?? '';
97
106
  issues.push({
98
- id: promptCacheIssueId('local-paths', file.path),
107
+ id: promptCacheIssueId('local-paths', fileIdPath(file)),
99
108
  severity: 'warning',
100
109
  category: 'context_bloat',
101
110
  title: 'Local machine paths in instructions',
@@ -132,7 +141,7 @@ export function detectPromptCacheFriendliness(files) {
132
141
  }
133
142
  if (maxTableRows > 15) {
134
143
  issues.push({
135
- id: promptCacheIssueId('large-table', file.path),
144
+ id: promptCacheIssueId('large-table', fileIdPath(file)),
136
145
  severity: 'warning',
137
146
  category: 'context_bloat',
138
147
  title: 'Large table in instruction file',