@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.
- package/LICENSE +201 -201
- package/README.md +29 -30
- package/dist/action-pinning.d.ts +37 -0
- package/dist/action-pinning.d.ts.map +1 -0
- package/dist/action-pinning.js +277 -0
- package/dist/action-pinning.js.map +1 -0
- package/dist/agent-practices.d.ts +3 -1
- package/dist/agent-practices.d.ts.map +1 -1
- package/dist/agent-practices.js +198 -94
- package/dist/agent-practices.js.map +1 -1
- package/dist/ai-config.d.ts +178 -0
- package/dist/ai-config.d.ts.map +1 -0
- package/dist/ai-config.js +721 -0
- package/dist/ai-config.js.map +1 -0
- package/dist/alias-files.d.ts +33 -0
- package/dist/alias-files.d.ts.map +1 -0
- package/dist/alias-files.js +60 -0
- package/dist/alias-files.js.map +1 -0
- package/dist/baseline.d.ts +17 -1
- package/dist/baseline.d.ts.map +1 -1
- package/dist/baseline.js +5 -1
- package/dist/baseline.js.map +1 -1
- package/dist/buried-critical.d.ts +17 -0
- package/dist/buried-critical.d.ts.map +1 -0
- package/dist/buried-critical.js +107 -0
- package/dist/buried-critical.js.map +1 -0
- package/dist/canonical-owner.d.ts.map +1 -1
- package/dist/canonical-owner.js +60 -30
- package/dist/canonical-owner.js.map +1 -1
- package/dist/ci-alignment.d.ts.map +1 -1
- package/dist/ci-alignment.js +3 -2
- package/dist/ci-alignment.js.map +1 -1
- package/dist/command-validity.d.ts +16 -0
- package/dist/command-validity.d.ts.map +1 -1
- package/dist/command-validity.js +387 -134
- package/dist/command-validity.js.map +1 -1
- package/dist/conflicts.d.ts.map +1 -1
- package/dist/conflicts.js +4 -44
- package/dist/conflicts.js.map +1 -1
- package/dist/context-analysis.d.ts.map +1 -1
- package/dist/context-analysis.js +13 -2
- package/dist/context-analysis.js.map +1 -1
- package/dist/context-bloat.d.ts +20 -1
- package/dist/context-bloat.d.ts.map +1 -1
- package/dist/context-bloat.js +99 -15
- package/dist/context-bloat.js.map +1 -1
- package/dist/context-optimizer.d.ts.map +1 -1
- package/dist/context-optimizer.js +18 -54
- package/dist/context-optimizer.js.map +1 -1
- package/dist/copilot-instructions-detector.d.ts +24 -0
- package/dist/copilot-instructions-detector.d.ts.map +1 -0
- package/dist/copilot-instructions-detector.js +145 -0
- package/dist/copilot-instructions-detector.js.map +1 -0
- package/dist/cursor-rules-detector.d.ts +17 -0
- package/dist/cursor-rules-detector.d.ts.map +1 -0
- package/dist/cursor-rules-detector.js +110 -0
- package/dist/cursor-rules-detector.js.map +1 -0
- package/dist/custom-rules.d.ts +52 -0
- package/dist/custom-rules.d.ts.map +1 -0
- package/dist/custom-rules.js +303 -0
- package/dist/custom-rules.js.map +1 -0
- package/dist/dead-references.d.ts +8 -2
- package/dist/dead-references.d.ts.map +1 -1
- package/dist/dead-references.js +315 -68
- package/dist/dead-references.js.map +1 -1
- package/dist/dependency-staleness.d.ts +35 -0
- package/dist/dependency-staleness.d.ts.map +1 -0
- package/dist/dependency-staleness.js +1001 -0
- package/dist/dependency-staleness.js.map +1 -0
- package/dist/detectors.d.ts +1 -3
- package/dist/detectors.d.ts.map +1 -1
- package/dist/detectors.js +74 -56
- package/dist/detectors.js.map +1 -1
- package/dist/duplicates.d.ts.map +1 -1
- package/dist/duplicates.js +15 -9
- package/dist/duplicates.js.map +1 -1
- package/dist/evidence.d.ts +39 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +78 -0
- package/dist/evidence.js.map +1 -0
- package/dist/finding-id.d.ts +21 -0
- package/dist/finding-id.d.ts.map +1 -0
- package/dist/finding-id.js +31 -0
- package/dist/finding-id.js.map +1 -0
- package/dist/fix-engine.d.ts +11 -0
- package/dist/fix-engine.d.ts.map +1 -1
- package/dist/fix-engine.js +88 -71
- package/dist/fix-engine.js.map +1 -1
- package/dist/frameworks/dotnet.js +2 -2
- package/dist/frameworks/dotnet.js.map +1 -1
- package/dist/frameworks/python.d.ts.map +1 -1
- package/dist/frameworks/python.js +5 -2
- package/dist/frameworks/python.js.map +1 -1
- package/dist/frameworks/rust.d.ts.map +1 -1
- package/dist/frameworks/rust.js +2 -1
- package/dist/frameworks/rust.js.map +1 -1
- package/dist/git-symlinks.d.ts +30 -0
- package/dist/git-symlinks.d.ts.map +1 -0
- package/dist/git-symlinks.js +161 -0
- package/dist/git-symlinks.js.map +1 -0
- package/dist/gitignore.d.ts +22 -0
- package/dist/gitignore.d.ts.map +1 -0
- package/dist/gitignore.js +157 -0
- package/dist/gitignore.js.map +1 -0
- package/dist/health-score.d.ts.map +1 -1
- package/dist/health-score.js +35 -5
- package/dist/health-score.js.map +1 -1
- package/dist/hooks-settings-detector.d.ts +17 -0
- package/dist/hooks-settings-detector.d.ts.map +1 -0
- package/dist/hooks-settings-detector.js +228 -0
- package/dist/hooks-settings-detector.js.map +1 -0
- package/dist/index.d.ts +29 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -6
- package/dist/index.js.map +1 -1
- package/dist/makefile.d.ts +37 -0
- package/dist/makefile.d.ts.map +1 -0
- package/dist/makefile.js +156 -0
- package/dist/makefile.js.map +1 -0
- package/dist/manifest-consistency.d.ts +6 -0
- package/dist/manifest-consistency.d.ts.map +1 -1
- package/dist/manifest-consistency.js +75 -10
- package/dist/manifest-consistency.js.map +1 -1
- package/dist/markdown-fences.d.ts +112 -0
- package/dist/markdown-fences.d.ts.map +1 -0
- package/dist/markdown-fences.js +174 -0
- package/dist/markdown-fences.js.map +1 -0
- package/dist/mcp-config-detector.d.ts +20 -0
- package/dist/mcp-config-detector.d.ts.map +1 -0
- package/dist/mcp-config-detector.js +237 -0
- package/dist/mcp-config-detector.js.map +1 -0
- package/dist/missing-context.d.ts.map +1 -1
- package/dist/missing-context.js +2 -52
- package/dist/missing-context.js.map +1 -1
- package/dist/model-budgets.d.ts +69 -0
- package/dist/model-budgets.d.ts.map +1 -0
- package/dist/model-budgets.js +110 -0
- package/dist/model-budgets.js.map +1 -0
- package/dist/negative-instructions.d.ts +26 -0
- package/dist/negative-instructions.d.ts.map +1 -0
- package/dist/negative-instructions.js +169 -0
- package/dist/negative-instructions.js.map +1 -0
- package/dist/path-containment.d.ts +41 -0
- package/dist/path-containment.d.ts.map +1 -0
- package/dist/path-containment.js +123 -0
- package/dist/path-containment.js.map +1 -0
- package/dist/project-type.d.ts +12 -2
- package/dist/project-type.d.ts.map +1 -1
- package/dist/project-type.js +25 -12
- package/dist/project-type.js.map +1 -1
- package/dist/prompt-cache.d.ts.map +1 -1
- package/dist/prompt-cache.js +33 -24
- package/dist/prompt-cache.js.map +1 -1
- package/dist/promptci-gitignore.d.ts +34 -0
- package/dist/promptci-gitignore.d.ts.map +1 -0
- package/dist/promptci-gitignore.js +42 -0
- package/dist/promptci-gitignore.js.map +1 -0
- package/dist/repo-context.d.ts +80 -0
- package/dist/repo-context.d.ts.map +1 -1
- package/dist/repo-context.js +170 -16
- package/dist/repo-context.js.map +1 -1
- package/dist/report.d.ts +8 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +163 -63
- package/dist/report.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +47 -4
- package/dist/scan.js.map +1 -1
- package/dist/scanner.d.ts +61 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +367 -70
- package/dist/scanner.js.map +1 -1
- package/dist/security-pack.d.ts +20 -0
- package/dist/security-pack.d.ts.map +1 -1
- package/dist/security-pack.js +33 -14
- package/dist/security-pack.js.map +1 -1
- package/dist/skill-supply-chain.d.ts +132 -0
- package/dist/skill-supply-chain.d.ts.map +1 -0
- package/dist/skill-supply-chain.js +2954 -0
- package/dist/skill-supply-chain.js.map +1 -0
- package/dist/skills-detector.d.ts +23 -0
- package/dist/skills-detector.d.ts.map +1 -0
- package/dist/skills-detector.js +279 -0
- package/dist/skills-detector.js.map +1 -0
- package/dist/stale-instructions.d.ts +4 -1
- package/dist/stale-instructions.d.ts.map +1 -1
- package/dist/stale-instructions.js +53 -55
- package/dist/stale-instructions.js.map +1 -1
- package/dist/subagents-detector.d.ts +19 -0
- package/dist/subagents-detector.d.ts.map +1 -0
- package/dist/subagents-detector.js +155 -0
- package/dist/subagents-detector.js.map +1 -0
- package/dist/suppression.d.ts +2 -0
- package/dist/suppression.d.ts.map +1 -1
- package/dist/suppression.js +16 -47
- package/dist/suppression.js.map +1 -1
- package/dist/types.d.ts +62 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +47 -1
- package/dist/types.js.map +1 -1
- package/dist/vague-guidance.d.ts +15 -2
- package/dist/vague-guidance.d.ts.map +1 -1
- package/dist/vague-guidance.js +44 -41
- package/dist/vague-guidance.js.map +1 -1
- package/dist/windsurf-detector.d.ts +22 -0
- package/dist/windsurf-detector.d.ts.map +1 -0
- package/dist/windsurf-detector.js +96 -0
- package/dist/windsurf-detector.js.map +1 -0
- package/dist/within-section-dedup.d.ts +16 -0
- package/dist/within-section-dedup.d.ts.map +1 -0
- package/dist/within-section-dedup.js +148 -0
- package/dist/within-section-dedup.js.map +1 -0
- package/dist/workspace-manifests.d.ts +80 -0
- package/dist/workspace-manifests.d.ts.map +1 -0
- package/dist/workspace-manifests.js +333 -0
- package/dist/workspace-manifests.js.map +1 -0
- package/package.json +11 -8
- package/dist/cheap-model-routing.d.ts +0 -4
- package/dist/cheap-model-routing.d.ts.map +0 -1
- package/dist/cheap-model-routing.js +0 -43
- package/dist/cheap-model-routing.js.map +0 -1
- package/dist/cli-output-filter.d.ts +0 -16
- package/dist/cli-output-filter.d.ts.map +0 -1
- package/dist/cli-output-filter.js +0 -88
- package/dist/cli-output-filter.js.map +0 -1
- package/dist/context-cost.d.ts +0 -4
- package/dist/context-cost.d.ts.map +0 -1
- package/dist/context-cost.js +0 -70
- package/dist/context-cost.js.map +0 -1
- package/dist/context-recommender.d.ts +0 -3
- package/dist/context-recommender.d.ts.map +0 -1
- package/dist/context-recommender.js +0 -329
- package/dist/context-recommender.js.map +0 -1
- package/dist/llm.d.ts +0 -6
- package/dist/llm.d.ts.map +0 -1
- package/dist/llm.js +0 -67
- package/dist/llm.js.map +0 -1
- package/dist/mcp-detector.d.ts +0 -4
- package/dist/mcp-detector.d.ts.map +0 -1
- package/dist/mcp-detector.js +0 -131
- package/dist/mcp-detector.js.map +0 -1
- package/dist/native-tool-preference.d.ts +0 -20
- package/dist/native-tool-preference.d.ts.map +0 -1
- package/dist/native-tool-preference.js +0 -94
- package/dist/native-tool-preference.js.map +0 -1
- package/dist/output-verbosity.d.ts +0 -3
- package/dist/output-verbosity.d.ts.map +0 -1
- package/dist/output-verbosity.js +0 -83
- package/dist/output-verbosity.js.map +0 -1
- package/dist/product-boundary.d.ts +0 -11
- package/dist/product-boundary.d.ts.map +0 -1
- package/dist/product-boundary.js +0 -301
- 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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"}
|
package/dist/project-type.d.ts
CHANGED
|
@@ -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 →
|
|
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 →
|
|
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
|
|
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"}
|
package/dist/project-type.js
CHANGED
|
@@ -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 →
|
|
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 →
|
|
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
|
}
|
package/dist/project-type.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-type.js","sourceRoot":"","sources":["../src/project-type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,MAAM,WAAW,CAAC;AAG3B,KAAK,UAAU,MAAM,CAAC,CAAS;IAC7B,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,CAAS;IAChC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC9B,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED
|
|
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;
|
|
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"}
|
package/dist/prompt-cache.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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) =>
|
|
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
|
|
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: [
|
|
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
|
|
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: [
|
|
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
|
|
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: [
|
|
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
|
|
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
|
|
144
|
+
id: promptCacheIssueId('large-table', fileIdPath(file)),
|
|
136
145
|
severity: 'warning',
|
|
137
146
|
category: 'context_bloat',
|
|
138
147
|
title: 'Large table in instruction file',
|