@getripple/core 1.0.4 → 1.0.6
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/CHANGELOG.md +13 -0
- package/README.md +306 -23
- package/dist/audit.d.ts +4 -0
- package/dist/audit.js +5 -0
- package/dist/audit.js.map +1 -1
- package/dist/change-intent.d.ts +1 -0
- package/dist/change-intent.js +22 -6
- package/dist/change-intent.js.map +1 -1
- package/dist/git.d.ts +6 -0
- package/dist/git.js +57 -0
- package/dist/git.js.map +1 -0
- package/dist/graph.d.ts +22 -2
- package/dist/graph.js +119 -31
- package/dist/graph.js.map +1 -1
- package/dist/readiness.d.ts +10 -0
- package/dist/readiness.js +117 -19
- package/dist/readiness.js.map +1 -1
- package/dist/staged-check.js +160 -98
- package/dist/staged-check.js.map +1 -1
- package/package.json +2 -2
package/dist/graph.js
CHANGED
|
@@ -57,15 +57,12 @@ const ts_morph_1 = require("ts-morph");
|
|
|
57
57
|
const types_1 = require("./types");
|
|
58
58
|
const normalizer_1 = require("./normalizer");
|
|
59
59
|
const adapters_1 = require("./adapters");
|
|
60
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
61
|
-
// PERSISTENCE AND GENERATED CONTEXT
|
|
62
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
63
60
|
/**
|
|
64
61
|
* Handles durable project state:
|
|
65
62
|
* - .ripple/history.json for human-auditable change history
|
|
66
63
|
* - .ripple/.cache/graph.cache.json for fast startup
|
|
67
|
-
* - .ripple/.cache/context*.json and focus files for
|
|
68
|
-
* - .ripple/WORKFLOW.md for agent operating rules
|
|
64
|
+
* - optional .ripple/.cache/context*.json and focus files for file-based agents
|
|
65
|
+
* - optional .ripple/WORKFLOW.md for agent operating rules
|
|
69
66
|
*/
|
|
70
67
|
class GraphPersistence {
|
|
71
68
|
/**
|
|
@@ -76,7 +73,7 @@ class GraphPersistence {
|
|
|
76
73
|
this.patternCache = null;
|
|
77
74
|
}
|
|
78
75
|
constructor(workspaceRoot) {
|
|
79
|
-
this.
|
|
76
|
+
this.contextGenerationMode = "full";
|
|
80
77
|
this.patternCache = null;
|
|
81
78
|
this.workspaceRoot = workspaceRoot;
|
|
82
79
|
const rippleDir = path.join(workspaceRoot, ".ripple");
|
|
@@ -189,34 +186,20 @@ class GraphPersistence {
|
|
|
189
186
|
* Restores graph nodes from cache and returns files whose content hash changed
|
|
190
187
|
* since the cache was written. Stale files are repaired by the scan phase.
|
|
191
188
|
*/
|
|
192
|
-
loadCache(graph) {
|
|
189
|
+
loadCache(graph, options = {}) {
|
|
193
190
|
try {
|
|
194
191
|
if (!fs.existsSync(this.cachePath)) {
|
|
195
192
|
return [];
|
|
196
193
|
}
|
|
197
194
|
const raw = fs.readFileSync(this.cachePath, "utf8");
|
|
198
195
|
const data = JSON.parse(raw);
|
|
196
|
+
const checkHashes = options.checkHashes ?? true;
|
|
199
197
|
const staleFiles = [];
|
|
200
198
|
const loadedFilePaths = new Set();
|
|
201
199
|
Object.entries(data.files).forEach(([filePath, node]) => {
|
|
202
200
|
if (!fs.existsSync(filePath)) {
|
|
203
201
|
return;
|
|
204
202
|
}
|
|
205
|
-
const currentContent = (() => {
|
|
206
|
-
try {
|
|
207
|
-
return fs.readFileSync(filePath, "utf8");
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
})();
|
|
213
|
-
if (currentContent === null) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
const currentHash = crypto
|
|
217
|
-
.createHash("sha1")
|
|
218
|
-
.update(currentContent)
|
|
219
|
-
.digest("hex");
|
|
220
203
|
const fileNode = {
|
|
221
204
|
path: node.path,
|
|
222
205
|
imports: new Set(node.imports),
|
|
@@ -230,7 +213,7 @@ class GraphPersistence {
|
|
|
230
213
|
};
|
|
231
214
|
graph.files.set(filePath, fileNode);
|
|
232
215
|
loadedFilePaths.add(filePath);
|
|
233
|
-
if (
|
|
216
|
+
if (checkHashes && this.cachedFileIsStale(filePath, node.hash)) {
|
|
234
217
|
staleFiles.push(filePath);
|
|
235
218
|
}
|
|
236
219
|
});
|
|
@@ -263,6 +246,18 @@ class GraphPersistence {
|
|
|
263
246
|
return [];
|
|
264
247
|
}
|
|
265
248
|
}
|
|
249
|
+
cachedFileIsStale(filePath, cachedHash) {
|
|
250
|
+
try {
|
|
251
|
+
const currentHash = crypto
|
|
252
|
+
.createHash("sha1")
|
|
253
|
+
.update(fs.readFileSync(filePath, "utf8"))
|
|
254
|
+
.digest("hex");
|
|
255
|
+
return currentHash !== cachedHash;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
266
261
|
toProjectPath(filePath) {
|
|
267
262
|
return path.relative(this.workspaceRoot, filePath).split(path.sep).join("/");
|
|
268
263
|
}
|
|
@@ -582,7 +577,7 @@ class GraphPersistence {
|
|
|
582
577
|
* - focus files and WORKFLOW.md for targeted agent workflows
|
|
583
578
|
*/
|
|
584
579
|
generateContext(graph, history) {
|
|
585
|
-
if (
|
|
580
|
+
if (this.contextGenerationMode !== "full") {
|
|
586
581
|
return;
|
|
587
582
|
}
|
|
588
583
|
try {
|
|
@@ -1509,7 +1504,7 @@ ${ambiguousBasenameExamples.map(e => `- ${e}`).join("\n")}` : ""}
|
|
|
1509
1504
|
* debounced separately, but the target focus file should be fresh immediately.
|
|
1510
1505
|
*/
|
|
1511
1506
|
generateSingleFocus(graph, filePath) {
|
|
1512
|
-
if (
|
|
1507
|
+
if (this.contextGenerationMode === "lean") {
|
|
1513
1508
|
return;
|
|
1514
1509
|
}
|
|
1515
1510
|
try {
|
|
@@ -1617,11 +1612,20 @@ function isPythonIdentifier(value) {
|
|
|
1617
1612
|
// ────────────────────────────────────────────────────────────────────────────
|
|
1618
1613
|
class GraphEngine {
|
|
1619
1614
|
/**
|
|
1620
|
-
*
|
|
1621
|
-
*
|
|
1615
|
+
* Chooses how much file-based context Ripple writes for the active interface.
|
|
1616
|
+
*
|
|
1617
|
+
* full — VS Code/file-based agent mode; writes WORKFLOW.md and focus files.
|
|
1618
|
+
* lean — CLI gate/check mode; writes only durable history and graph cache.
|
|
1619
|
+
* on-demand — MCP mode; skips broad dumps but allows targeted focus writes.
|
|
1620
|
+
*/
|
|
1621
|
+
setContextGenerationMode(mode) {
|
|
1622
|
+
this.persistence.contextGenerationMode = mode;
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Backwards-compatible switch used by older callers and tests.
|
|
1622
1626
|
*/
|
|
1623
1627
|
setContextGeneration(enabled) {
|
|
1624
|
-
this.
|
|
1628
|
+
this.setContextGenerationMode(enabled ? "full" : "lean");
|
|
1625
1629
|
}
|
|
1626
1630
|
constructor(workspaceRoot) {
|
|
1627
1631
|
// Scans rebuild a large part of the graph. File-system events that arrive
|
|
@@ -1632,6 +1636,7 @@ class GraphEngine {
|
|
|
1632
1636
|
this.pendingDeletes = new Set();
|
|
1633
1637
|
this.pendingFullRescan = false;
|
|
1634
1638
|
this.sessionNewFiles = new Set();
|
|
1639
|
+
this.focusKeyCache = new Map();
|
|
1635
1640
|
this.workspaceRoot = workspaceRoot;
|
|
1636
1641
|
this.graph = new types_1.SystemGraph();
|
|
1637
1642
|
this.history = new types_1.HistoryLog();
|
|
@@ -1648,6 +1653,29 @@ class GraphEngine {
|
|
|
1648
1653
|
invalidateAdapterSupport() {
|
|
1649
1654
|
this.adapterSupportCache = undefined;
|
|
1650
1655
|
}
|
|
1656
|
+
invalidateFocusKeyCache() {
|
|
1657
|
+
this.focusKeyCache.clear();
|
|
1658
|
+
this.focusBaseKeyCounts = undefined;
|
|
1659
|
+
}
|
|
1660
|
+
focusKeyForPath(filePath) {
|
|
1661
|
+
const cached = this.focusKeyCache.get(filePath);
|
|
1662
|
+
if (cached) {
|
|
1663
|
+
return cached;
|
|
1664
|
+
}
|
|
1665
|
+
if (!this.focusBaseKeyCounts) {
|
|
1666
|
+
this.focusBaseKeyCounts = new Map();
|
|
1667
|
+
this.graph.files.forEach((_, graphFilePath) => {
|
|
1668
|
+
const baseKey = getBaseKey(graphFilePath);
|
|
1669
|
+
this.focusBaseKeyCounts.set(baseKey, (this.focusBaseKeyCounts.get(baseKey) ?? 0) + 1);
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
const baseKey = getBaseKey(filePath);
|
|
1673
|
+
const focusKey = (this.focusBaseKeyCounts.get(baseKey) ?? 0) > 1
|
|
1674
|
+
? `${baseKey}-${crypto.createHash("sha1").update(filePath).digest("hex").slice(0, 6)}`
|
|
1675
|
+
: baseKey;
|
|
1676
|
+
this.focusKeyCache.set(filePath, focusKey);
|
|
1677
|
+
return focusKey;
|
|
1678
|
+
}
|
|
1651
1679
|
createProject() {
|
|
1652
1680
|
return new ts_morph_1.Project({
|
|
1653
1681
|
compilerOptions: {
|
|
@@ -1745,6 +1773,7 @@ class GraphEngine {
|
|
|
1745
1773
|
this.graph.files.clear();
|
|
1746
1774
|
this.graph.symbols.clear();
|
|
1747
1775
|
this.project = this.createProject();
|
|
1776
|
+
this.invalidateFocusKeyCache();
|
|
1748
1777
|
this.sessionNewFiles.clear();
|
|
1749
1778
|
let scanned = 0;
|
|
1750
1779
|
const total = validFiles.length * 2;
|
|
@@ -1794,6 +1823,61 @@ class GraphEngine {
|
|
|
1794
1823
|
await this.rebuildFromDisk(onProgress, priorityFile);
|
|
1795
1824
|
}
|
|
1796
1825
|
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Fast path for live agent gates. It trusts the existing graph cache instead
|
|
1828
|
+
* of hash-checking every source file, then only parses candidate files that
|
|
1829
|
+
* are missing from the cache. If no cache exists, it falls back to the normal
|
|
1830
|
+
* scan so first-time use remains correct.
|
|
1831
|
+
*/
|
|
1832
|
+
async fastCheckScan(candidateFiles = [], onProgress) {
|
|
1833
|
+
this.isScanning = true;
|
|
1834
|
+
this.invalidateAdapterSupport();
|
|
1835
|
+
this.graph.files.clear();
|
|
1836
|
+
this.graph.symbols.clear();
|
|
1837
|
+
this.project = this.createProject();
|
|
1838
|
+
this.invalidateFocusKeyCache();
|
|
1839
|
+
const normalizedCandidates = candidateFiles
|
|
1840
|
+
.map((filePath) => this.resolveFilePath(filePath))
|
|
1841
|
+
.filter((filePath, index, values) => values.indexOf(filePath) === index &&
|
|
1842
|
+
!shouldIgnore(filePath) &&
|
|
1843
|
+
fs.existsSync(filePath) &&
|
|
1844
|
+
/\.(ts|tsx|js|jsx|py)$/i.test(filePath) &&
|
|
1845
|
+
!filePath.endsWith(".d.ts"));
|
|
1846
|
+
this.persistence.loadCache(this.graph, { checkHashes: false });
|
|
1847
|
+
this.isScanning = false;
|
|
1848
|
+
if (this.graph.files.size === 0) {
|
|
1849
|
+
await this.initialScan(onProgress, normalizedCandidates[0]);
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
let scanned = 0;
|
|
1853
|
+
const total = normalizedCandidates.length * 2;
|
|
1854
|
+
normalizedCandidates
|
|
1855
|
+
.filter((filePath) => !this.graph.files.has(filePath))
|
|
1856
|
+
.forEach((filePath) => {
|
|
1857
|
+
try {
|
|
1858
|
+
this.ensureFileNode(filePath);
|
|
1859
|
+
this.parseImportsAndExports(filePath, false);
|
|
1860
|
+
}
|
|
1861
|
+
catch {
|
|
1862
|
+
console.warn("[Ripple] Fast check parse error:", filePath);
|
|
1863
|
+
}
|
|
1864
|
+
scanned++;
|
|
1865
|
+
onProgress?.(scanned, total);
|
|
1866
|
+
});
|
|
1867
|
+
normalizedCandidates
|
|
1868
|
+
.filter((filePath) => this.graph.files.has(filePath))
|
|
1869
|
+
.forEach((filePath) => {
|
|
1870
|
+
try {
|
|
1871
|
+
this.parseCallsOnly(filePath);
|
|
1872
|
+
}
|
|
1873
|
+
catch {
|
|
1874
|
+
console.warn("[Ripple] Fast check call parse error:", filePath);
|
|
1875
|
+
}
|
|
1876
|
+
scanned++;
|
|
1877
|
+
onProgress?.(scanned, total);
|
|
1878
|
+
});
|
|
1879
|
+
this.processPendingChanges();
|
|
1880
|
+
}
|
|
1797
1881
|
// ── INITIAL SCAN ──────────────────────────────────────────────────────────
|
|
1798
1882
|
/**
|
|
1799
1883
|
* Starts from cache when possible and repairs only stale or newly discovered
|
|
@@ -3067,6 +3151,7 @@ class GraphEngine {
|
|
|
3067
3151
|
changeCount: 0,
|
|
3068
3152
|
hash: "",
|
|
3069
3153
|
});
|
|
3154
|
+
this.invalidateFocusKeyCache();
|
|
3070
3155
|
}
|
|
3071
3156
|
return this.graph.files.get(filePath);
|
|
3072
3157
|
}
|
|
@@ -3118,7 +3203,7 @@ class GraphEngine {
|
|
|
3118
3203
|
return "safe";
|
|
3119
3204
|
}
|
|
3120
3205
|
focusPathFor(filePath) {
|
|
3121
|
-
return `.ripple/.cache/focus/${
|
|
3206
|
+
return `.ripple/.cache/focus/${this.focusKeyForPath(filePath)}.json`;
|
|
3122
3207
|
}
|
|
3123
3208
|
dependencyLinkFor(filePath) {
|
|
3124
3209
|
const node = this.graph.files.get(filePath);
|
|
@@ -3241,7 +3326,10 @@ class GraphEngine {
|
|
|
3241
3326
|
return Array.from(this.graph.files.get(filePath)?.imports ?? []);
|
|
3242
3327
|
}
|
|
3243
3328
|
focusKeyForFile(filePath) {
|
|
3244
|
-
return
|
|
3329
|
+
return this.focusKeyForPath(filePath);
|
|
3330
|
+
}
|
|
3331
|
+
writeFileFocus(filePath) {
|
|
3332
|
+
this.persistence.generateSingleFocus(this.graph, this.resolveFilePath(filePath));
|
|
3245
3333
|
}
|
|
3246
3334
|
symbolImpact(symbolId) {
|
|
3247
3335
|
return Array.from(this.graph.symbols.get(symbolId)?.calledBy ?? []);
|
|
@@ -4059,7 +4147,7 @@ class GraphEngine {
|
|
|
4059
4147
|
return {
|
|
4060
4148
|
filePath: resolvedPath,
|
|
4061
4149
|
projectPath: this.toProjectPath(resolvedPath),
|
|
4062
|
-
focusKey:
|
|
4150
|
+
focusKey: this.focusKeyForPath(resolvedPath),
|
|
4063
4151
|
focusPath: this.focusPathFor(resolvedPath),
|
|
4064
4152
|
modificationRisk: this.modificationRiskFor(node),
|
|
4065
4153
|
imports,
|