@reneturcios/tracewatch 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +138 -2
- package/dist/adapters.d.ts +31 -0
- package/dist/adapters.js +128 -0
- package/dist/adapters.js.map +1 -0
- package/dist/blame.d.ts +15 -0
- package/dist/blame.js +165 -0
- package/dist/blame.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +236 -0
- package/dist/cli.js.map +1 -0
- package/dist/core.d.ts +18 -0
- package/dist/core.js +19 -0
- package/dist/core.js.map +1 -0
- package/dist/diff.d.ts +21 -0
- package/dist/diff.js +131 -0
- package/dist/diff.js.map +1 -0
- package/dist/git.d.ts +16 -0
- package/dist/git.js +80 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest.d.ts +54 -0
- package/dist/ingest.js +182 -0
- package/dist/ingest.js.map +1 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +80 -0
- package/dist/mcp.js.map +1 -0
- package/dist/render.d.ts +13 -0
- package/dist/render.js +151 -0
- package/dist/render.js.map +1 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.js +61 -0
- package/dist/report.js.map +1 -0
- package/dist/store.d.ts +31 -0
- package/dist/store.js +163 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +215 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +16 -0
- package/dist/util.js +65 -0
- package/dist/util.js.map +1 -0
- package/dist/watch.d.ts +62 -0
- package/dist/watch.js +401 -0
- package/dist/watch.js.map +1 -0
- package/package.json +1 -6
- package/server.json +1 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store: append-only JSONL files under one directory.
|
|
3
|
+
*
|
|
4
|
+
* scans.jsonl one ScanRecord per ingest
|
|
5
|
+
* findings.jsonl one FindingObs per finding per scan
|
|
6
|
+
* detections.jsonl one DetectionRecord per watch detection
|
|
7
|
+
* alerts.jsonl one AlertRecord per emitted alert
|
|
8
|
+
* watch-state.json checkpoints for watch mode (rewritten atomically)
|
|
9
|
+
*
|
|
10
|
+
* Reads tolerate a torn final line (a crash mid-append) by skipping any
|
|
11
|
+
* line that does not parse; appends are single write() calls per record.
|
|
12
|
+
* Finding status is derived from the observation series on every read,
|
|
13
|
+
* never stored, so the store cannot disagree with itself.
|
|
14
|
+
*/
|
|
15
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
export function defaultStoreDir() {
|
|
19
|
+
return process.env.TRACEWATCH_STORE ?? join(homedir(), ".tracewatch");
|
|
20
|
+
}
|
|
21
|
+
function readJsonl(file) {
|
|
22
|
+
if (!existsSync(file))
|
|
23
|
+
return [];
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
if (!trimmed)
|
|
28
|
+
continue;
|
|
29
|
+
try {
|
|
30
|
+
out.push(JSON.parse(trimmed));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Torn or hand-edited line: skip it rather than failing every read.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
export class Store {
|
|
39
|
+
dir;
|
|
40
|
+
constructor(dir) {
|
|
41
|
+
this.dir = dir;
|
|
42
|
+
}
|
|
43
|
+
file(name) {
|
|
44
|
+
return join(this.dir, name);
|
|
45
|
+
}
|
|
46
|
+
ensure() {
|
|
47
|
+
mkdirSync(this.dir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
append(name, record) {
|
|
50
|
+
this.ensure();
|
|
51
|
+
appendFileSync(this.file(name), `${JSON.stringify(record)}\n`, "utf8");
|
|
52
|
+
}
|
|
53
|
+
appendScan(rec) {
|
|
54
|
+
this.append("scans.jsonl", rec);
|
|
55
|
+
}
|
|
56
|
+
appendFindings(obs) {
|
|
57
|
+
for (const o of obs)
|
|
58
|
+
this.append("findings.jsonl", o);
|
|
59
|
+
}
|
|
60
|
+
appendDetection(rec) {
|
|
61
|
+
this.append("detections.jsonl", rec);
|
|
62
|
+
}
|
|
63
|
+
appendAlert(rec) {
|
|
64
|
+
this.append("alerts.jsonl", rec);
|
|
65
|
+
}
|
|
66
|
+
scans(repo) {
|
|
67
|
+
const all = readJsonl(this.file("scans.jsonl"));
|
|
68
|
+
return repo ? all.filter((s) => s.repo === repo) : all;
|
|
69
|
+
}
|
|
70
|
+
findings(repo) {
|
|
71
|
+
const all = readJsonl(this.file("findings.jsonl"));
|
|
72
|
+
return repo ? all.filter((f) => f.repo === repo) : all;
|
|
73
|
+
}
|
|
74
|
+
detections(repo) {
|
|
75
|
+
const all = readJsonl(this.file("detections.jsonl"));
|
|
76
|
+
return repo ? all.filter((d) => d.repo === repo) : all;
|
|
77
|
+
}
|
|
78
|
+
alerts(repo) {
|
|
79
|
+
const all = readJsonl(this.file("alerts.jsonl"));
|
|
80
|
+
return repo ? all.filter((a) => a.repo === repo) : all;
|
|
81
|
+
}
|
|
82
|
+
repos() {
|
|
83
|
+
return [...new Set(this.scans().map((s) => s.repo))].sort();
|
|
84
|
+
}
|
|
85
|
+
scanById(scanId) {
|
|
86
|
+
return this.scans().find((s) => s.scanId === scanId);
|
|
87
|
+
}
|
|
88
|
+
latestScan(repo) {
|
|
89
|
+
const list = this.scans(repo);
|
|
90
|
+
return list[list.length - 1];
|
|
91
|
+
}
|
|
92
|
+
observations(scanId) {
|
|
93
|
+
return this.findings().filter((f) => f.scanId === scanId);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Derive every tracked finding for a repo: status, first/last seen,
|
|
97
|
+
* resolution time, and content version count. Deterministic: output is
|
|
98
|
+
* sorted by identity, and scan order is store append order.
|
|
99
|
+
*/
|
|
100
|
+
trackedFindings(repo) {
|
|
101
|
+
const scanList = this.scans(repo);
|
|
102
|
+
const scanAt = new Map(scanList.map((s) => [s.scanId, s.at]));
|
|
103
|
+
const scanIndex = new Map(scanList.map((s, i) => [s.scanId, i]));
|
|
104
|
+
const byIdentity = new Map();
|
|
105
|
+
for (const obs of this.findings(repo)) {
|
|
106
|
+
const arr = byIdentity.get(obs.identity) ?? [];
|
|
107
|
+
arr.push(obs);
|
|
108
|
+
byIdentity.set(obs.identity, arr);
|
|
109
|
+
}
|
|
110
|
+
const latest = scanList[scanList.length - 1];
|
|
111
|
+
const out = [];
|
|
112
|
+
for (const [identity, obsList] of byIdentity) {
|
|
113
|
+
const sorted = [...obsList].sort((a, b) => (scanIndex.get(a.scanId) ?? 0) - (scanIndex.get(b.scanId) ?? 0));
|
|
114
|
+
const first = sorted[0];
|
|
115
|
+
const last = sorted[sorted.length - 1];
|
|
116
|
+
const isOpen = latest !== undefined && last.scanId === latest.scanId;
|
|
117
|
+
let resolvedAt;
|
|
118
|
+
if (!isOpen && latest) {
|
|
119
|
+
const lastIdx = scanIndex.get(last.scanId) ?? 0;
|
|
120
|
+
const next = scanList[lastIdx + 1];
|
|
121
|
+
if (next)
|
|
122
|
+
resolvedAt = next.at;
|
|
123
|
+
}
|
|
124
|
+
out.push({
|
|
125
|
+
identity,
|
|
126
|
+
latest: last,
|
|
127
|
+
firstSeenAt: scanAt.get(first.scanId) ?? first.scanId,
|
|
128
|
+
lastSeenAt: scanAt.get(last.scanId) ?? last.scanId,
|
|
129
|
+
firstScanId: first.scanId,
|
|
130
|
+
lastScanId: last.scanId,
|
|
131
|
+
status: isOpen ? "open" : "resolved",
|
|
132
|
+
resolvedAt,
|
|
133
|
+
versions: new Set(sorted.map((o) => o.findingId)).size,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return out.sort((a, b) => a.identity.localeCompare(b.identity));
|
|
137
|
+
}
|
|
138
|
+
/** Find one tracked finding by full or prefix findingId, or by identity. */
|
|
139
|
+
findFinding(repo, idOrPrefix) {
|
|
140
|
+
const tracked = this.trackedFindings(repo);
|
|
141
|
+
return (tracked.find((t) => t.latest.findingId === idOrPrefix) ??
|
|
142
|
+
tracked.find((t) => t.identity === idOrPrefix) ??
|
|
143
|
+
tracked.find((t) => t.latest.findingId.startsWith(idOrPrefix) && idOrPrefix.length >= 8));
|
|
144
|
+
}
|
|
145
|
+
readWatchState() {
|
|
146
|
+
const file = this.file("watch-state.json");
|
|
147
|
+
if (!existsSync(file))
|
|
148
|
+
return null;
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
writeWatchState(state) {
|
|
157
|
+
this.ensure();
|
|
158
|
+
const tmp = `${this.file("watch-state.json")}.tmp`;
|
|
159
|
+
writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
160
|
+
renameSync(tmp, this.file("watch-state.json"));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACzG,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,MAAM,UAAU,eAAe;IAC7B,OAAO,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,aAAa,CAAC,CAAC;AACxE,CAAC;AAED,SAAS,SAAS,CAAI,IAAY;IAChC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,MAAM,GAAG,GAAQ,EAAE,CAAC;IACpB,KAAK,MAAM,IAAI,IAAI,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO;YAAE,SAAS;QACvB,IAAI,CAAC;YACH,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAM,CAAC,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,oEAAoE;QACtE,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,OAAO,KAAK;IACP,GAAG,CAAS;IAErB,YAAY,GAAW;QACrB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAEO,IAAI,CAAC,IAAY;QACvB,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC;IAEO,MAAM;QACZ,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAEO,MAAM,CAAC,IAAY,EAAE,MAAe;QAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACzE,CAAC;IAED,UAAU,CAAC,GAAe;QACxB,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;IAClC,CAAC;IAED,cAAc,CAAC,GAAiB;QAC9B,KAAK,MAAM,CAAC,IAAI,GAAG;YAAE,IAAI,CAAC,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,eAAe,CAAC,GAAoB;QAClC,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;IACvC,CAAC;IAED,WAAW,CAAC,GAAgB;QAC1B,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,IAAa;QACjB,MAAM,GAAG,GAAG,SAAS,CAAa,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC;QAC5D,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACzD,CAAC;IAED,QAAQ,CAAC,IAAa;QACpB,MAAM,GAAG,GAAG,SAAS,CAAa,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC;QAC/D,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACzD,CAAC;IAED,UAAU,CAAC,IAAa;QACtB,MAAM,GAAG,GAAG,SAAS,CAAkB,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC;QACtE,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACzD,CAAC;IAED,MAAM,CAAC,IAAa;QAClB,MAAM,GAAG,GAAG,SAAS,CAAc,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;QAC9D,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACzD,CAAC;IAED,KAAK;QACH,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9D,CAAC;IAED,QAAQ,CAAC,MAAc;QACrB,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC;IACvD,CAAC;IAED,UAAU,CAAC,IAAY;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC/B,CAAC;IAED,YAAY,CAAC,MAAc;QACzB,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC;IAC5D,CAAC;IAED;;;;OAIG;IACH,eAAe,CAAC,IAAY;QAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC9D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACjE,MAAM,UAAU,GAAG,IAAI,GAAG,EAAwB,CAAC;QACnD,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YAC/C,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACd,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QACpC,CAAC;QACD,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC7C,MAAM,GAAG,GAAqB,EAAE,CAAC;QACjC,KAAK,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,UAAU,EAAE,CAAC;YAC7C,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAC9B,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAC1E,CAAC;YACF,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACxB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACvC,MAAM,MAAM,GAAG,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC;YACrE,IAAI,UAA8B,CAAC;YACnC,IAAI,CAAC,MAAM,IAAI,MAAM,EAAE,CAAC;gBACtB,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;gBAChD,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;gBACnC,IAAI,IAAI;oBAAE,UAAU,GAAG,IAAI,CAAC,EAAE,CAAC;YACjC,CAAC;YACD,GAAG,CAAC,IAAI,CAAC;gBACP,QAAQ;gBACR,MAAM,EAAE,IAAI;gBACZ,WAAW,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM;gBACrD,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM;gBAClD,WAAW,EAAE,KAAK,CAAC,MAAM;gBACzB,UAAU,EAAE,IAAI,CAAC,MAAM;gBACvB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU;gBACpC,UAAU;gBACV,QAAQ,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;aACvD,CAAC,CAAC;QACL,CAAC;QACD,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,4EAA4E;IAC5E,WAAW,CAAC,IAAY,EAAE,UAAkB;QAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;QAC3C,OAAO,CACL,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,KAAK,UAAU,CAAC;YACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC;YAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,IAAI,CAAC,CAAC,CACzF,CAAC;IACJ,CAAC;IAED,cAAc;QACZ,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QAC3C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACnC,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAM,CAAC;QACrD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,eAAe,CAAC,KAAc;QAC5B,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC;QACnD,aAAa,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAClE,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC;IACjD,CAAC;CACF"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tracewatch types.
|
|
3
|
+
*
|
|
4
|
+
* tracewatch is the Security Wave's memory: it ingests the JSON reports of
|
|
5
|
+
* hookcheck, actionpin, and injectcheck, stores them as timestamped scan
|
|
6
|
+
* records, and tracks every finding across scans under a stable ID so a
|
|
7
|
+
* finding can be diffed, aged, and blamed to the commit that introduced it.
|
|
8
|
+
* Every record here is plain JSON: the store is append-only JSONL files the
|
|
9
|
+
* operator can grep by hand.
|
|
10
|
+
*/
|
|
11
|
+
export type ToolName = "hookcheck" | "actionpin" | "injectcheck";
|
|
12
|
+
export declare const TOOLS: ToolName[];
|
|
13
|
+
/** Single severity scale every tool's native scale is mapped onto. */
|
|
14
|
+
export type Severity = "critical" | "high" | "medium" | "low" | "info";
|
|
15
|
+
export declare const SEVERITY_ORDER: Severity[];
|
|
16
|
+
export interface ToolScanInfo {
|
|
17
|
+
/** Version of the tool that produced the report, e.g. "0.1.14". */
|
|
18
|
+
version: string;
|
|
19
|
+
/** The tool's own verdict string, e.g. "COMPROMISED-PATTERN". */
|
|
20
|
+
verdict: string;
|
|
21
|
+
counts: Record<string, number>;
|
|
22
|
+
status: "ok" | "error";
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ScanRecord {
|
|
26
|
+
scanId: string;
|
|
27
|
+
/** Repo key the scan belongs to (a name, or an owner/repo slug). */
|
|
28
|
+
repo: string;
|
|
29
|
+
/** ISO timestamp of the ingest. */
|
|
30
|
+
at: string;
|
|
31
|
+
source: "run" | "import" | "watch";
|
|
32
|
+
/** Local path that was scanned, when known. */
|
|
33
|
+
path?: string;
|
|
34
|
+
/** GitHub slug (owner/repo) when known. */
|
|
35
|
+
github?: string;
|
|
36
|
+
/** git HEAD of the target at scan time, when it is a repo. */
|
|
37
|
+
headSha?: string | null;
|
|
38
|
+
tools: Partial<Record<ToolName, ToolScanInfo>>;
|
|
39
|
+
warnings: string[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One observation of one finding in one scan. The store keeps every
|
|
43
|
+
* observation; a finding's status (open, resolved, changed) is always
|
|
44
|
+
* derived from the observation series, never stored as mutable state.
|
|
45
|
+
*/
|
|
46
|
+
export interface FindingObs {
|
|
47
|
+
/** tw_ + 16 hex: hash of tool, rule, normalized location, content signature. */
|
|
48
|
+
findingId: string;
|
|
49
|
+
/** Durable match key: tool | location | rule. Stable across content edits. */
|
|
50
|
+
identity: string;
|
|
51
|
+
scanId: string;
|
|
52
|
+
repo: string;
|
|
53
|
+
tool: ToolName;
|
|
54
|
+
rule: string;
|
|
55
|
+
severity: Severity;
|
|
56
|
+
/** File path normalized relative to the scan root (or a source label). */
|
|
57
|
+
file: string;
|
|
58
|
+
line?: number;
|
|
59
|
+
detail: string;
|
|
60
|
+
excerpt?: string;
|
|
61
|
+
/** Content signature the findingId was built from (sha256 hex). */
|
|
62
|
+
signature: string;
|
|
63
|
+
/** Tool-specific payload diff and blame need (command text, uses/ref, ...). */
|
|
64
|
+
data: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
/** A finding plus its derived status across the whole store. */
|
|
67
|
+
export interface TrackedFinding {
|
|
68
|
+
identity: string;
|
|
69
|
+
/** Most recent observation. */
|
|
70
|
+
latest: FindingObs;
|
|
71
|
+
firstSeenAt: string;
|
|
72
|
+
lastSeenAt: string;
|
|
73
|
+
firstScanId: string;
|
|
74
|
+
lastScanId: string;
|
|
75
|
+
status: "open" | "resolved";
|
|
76
|
+
/** Scan time at which the finding was first absent after being present. */
|
|
77
|
+
resolvedAt?: string;
|
|
78
|
+
/** Distinct findingIds over time: more than one means the content changed. */
|
|
79
|
+
versions: number;
|
|
80
|
+
}
|
|
81
|
+
export interface DetectionRecord {
|
|
82
|
+
at: string;
|
|
83
|
+
repo: string;
|
|
84
|
+
kind: "issue" | "pr" | "tree" | "file";
|
|
85
|
+
/** Issue/PR number, file path, or commit SHA the content arrived as. */
|
|
86
|
+
sourceRef: string;
|
|
87
|
+
/** The content's own creation timestamp (issue created_at, commit time, file birth/mtime). */
|
|
88
|
+
createdAt: string;
|
|
89
|
+
timeToDetectMs: number;
|
|
90
|
+
scanId?: string;
|
|
91
|
+
findingIds: string[];
|
|
92
|
+
}
|
|
93
|
+
export interface AlertRecord {
|
|
94
|
+
at: string;
|
|
95
|
+
repo: string;
|
|
96
|
+
kind: "new-finding" | "changed-finding" | "ref-event" | "detection" | "watch-note";
|
|
97
|
+
message: string;
|
|
98
|
+
findingId?: string;
|
|
99
|
+
detection?: DetectionRecord;
|
|
100
|
+
}
|
|
101
|
+
export interface RefEvent {
|
|
102
|
+
identity: string;
|
|
103
|
+
file: string;
|
|
104
|
+
action: string;
|
|
105
|
+
oldRef: string;
|
|
106
|
+
newRef: string;
|
|
107
|
+
oldClassification: string;
|
|
108
|
+
newClassification: string;
|
|
109
|
+
kind: "ref-move" | "pin-regression" | "pin-improvement";
|
|
110
|
+
/** Scan the event was observed in (the "to" scan of the diff). */
|
|
111
|
+
scanId: string;
|
|
112
|
+
at: string;
|
|
113
|
+
}
|
|
114
|
+
export interface ChangedFinding {
|
|
115
|
+
identity: string;
|
|
116
|
+
before: FindingObs;
|
|
117
|
+
after: FindingObs;
|
|
118
|
+
/** Human-readable field moves, e.g. "severity: medium -> high". */
|
|
119
|
+
changes: string[];
|
|
120
|
+
refEvent?: RefEvent;
|
|
121
|
+
}
|
|
122
|
+
export interface ScanDiff {
|
|
123
|
+
repo: string;
|
|
124
|
+
from: {
|
|
125
|
+
scanId: string;
|
|
126
|
+
at: string;
|
|
127
|
+
} | null;
|
|
128
|
+
to: {
|
|
129
|
+
scanId: string;
|
|
130
|
+
at: string;
|
|
131
|
+
};
|
|
132
|
+
added: FindingObs[];
|
|
133
|
+
resolved: FindingObs[];
|
|
134
|
+
changed: ChangedFinding[];
|
|
135
|
+
unchangedCount: number;
|
|
136
|
+
refEvents: RefEvent[];
|
|
137
|
+
verdictMoves: {
|
|
138
|
+
tool: ToolName;
|
|
139
|
+
from: string | null;
|
|
140
|
+
to: string | null;
|
|
141
|
+
}[];
|
|
142
|
+
}
|
|
143
|
+
export interface CommitInfo {
|
|
144
|
+
sha: string;
|
|
145
|
+
author: string;
|
|
146
|
+
date: string;
|
|
147
|
+
subject: string;
|
|
148
|
+
}
|
|
149
|
+
export interface PullRef {
|
|
150
|
+
number: number;
|
|
151
|
+
title: string;
|
|
152
|
+
url: string;
|
|
153
|
+
state: string;
|
|
154
|
+
}
|
|
155
|
+
export interface Provenance {
|
|
156
|
+
repo: string;
|
|
157
|
+
finding: FindingObs;
|
|
158
|
+
status: "open" | "resolved";
|
|
159
|
+
firstSeenAt: string;
|
|
160
|
+
lastSeenAt: string;
|
|
161
|
+
confirmed: {
|
|
162
|
+
/** Earliest commit whose diff introduced the finding's needle (git log -S). */
|
|
163
|
+
introducingCommit?: CommitInfo;
|
|
164
|
+
/** Commit git blame assigns to the finding's current line at HEAD. */
|
|
165
|
+
currentLineCommit?: CommitInfo;
|
|
166
|
+
pullRequests?: PullRef[];
|
|
167
|
+
/** For action ref moves: first-seen and last-seen refs from the store itself. */
|
|
168
|
+
refHistory?: {
|
|
169
|
+
ref: string;
|
|
170
|
+
classification: string;
|
|
171
|
+
firstScanId: string;
|
|
172
|
+
firstAt: string;
|
|
173
|
+
lastScanId: string;
|
|
174
|
+
lastAt: string;
|
|
175
|
+
}[];
|
|
176
|
+
source?: {
|
|
177
|
+
kind: string;
|
|
178
|
+
ref: string;
|
|
179
|
+
url?: string;
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
/** Statements supported only circumstantially; labeled, never merged with confirmed. */
|
|
183
|
+
inferred: string[];
|
|
184
|
+
/** What could not be established, with the reason. Never guessed around. */
|
|
185
|
+
notEstablished: string[];
|
|
186
|
+
}
|
|
187
|
+
export interface HealthReport {
|
|
188
|
+
repo: string;
|
|
189
|
+
scanCount: number;
|
|
190
|
+
firstScanAt: string | null;
|
|
191
|
+
lastScanAt: string | null;
|
|
192
|
+
verdictTrend: {
|
|
193
|
+
scanId: string;
|
|
194
|
+
at: string;
|
|
195
|
+
verdicts: Partial<Record<ToolName, string>>;
|
|
196
|
+
}[];
|
|
197
|
+
open: {
|
|
198
|
+
total: number;
|
|
199
|
+
byTool: Record<string, number>;
|
|
200
|
+
bySeverity: Record<string, number>;
|
|
201
|
+
};
|
|
202
|
+
resolvedCount: number;
|
|
203
|
+
meanTimeOpenMs: number | null;
|
|
204
|
+
refEvents: RefEvent[];
|
|
205
|
+
detections: {
|
|
206
|
+
count: number;
|
|
207
|
+
medianMs: number | null;
|
|
208
|
+
maxMs: number | null;
|
|
209
|
+
byKind: Record<string, {
|
|
210
|
+
count: number;
|
|
211
|
+
medianMs: number | null;
|
|
212
|
+
maxMs: number | null;
|
|
213
|
+
}>;
|
|
214
|
+
};
|
|
215
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tracewatch types.
|
|
3
|
+
*
|
|
4
|
+
* tracewatch is the Security Wave's memory: it ingests the JSON reports of
|
|
5
|
+
* hookcheck, actionpin, and injectcheck, stores them as timestamped scan
|
|
6
|
+
* records, and tracks every finding across scans under a stable ID so a
|
|
7
|
+
* finding can be diffed, aged, and blamed to the commit that introduced it.
|
|
8
|
+
* Every record here is plain JSON: the store is append-only JSONL files the
|
|
9
|
+
* operator can grep by hand.
|
|
10
|
+
*/
|
|
11
|
+
export const TOOLS = ["hookcheck", "actionpin", "injectcheck"];
|
|
12
|
+
export const SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
|
|
13
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,CAAC,MAAM,KAAK,GAAe,CAAC,WAAW,EAAE,WAAW,EAAE,aAAa,CAAC,CAAC;AAK3E,MAAM,CAAC,MAAM,cAAc,GAAe,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC"}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function sha256(text: string): string;
|
|
2
|
+
/** Content signature for a finding: hash of its defining content parts. */
|
|
3
|
+
export declare function signatureOf(parts: string[]): string;
|
|
4
|
+
export declare function makeIdentity(tool: string, location: string, rule: string): string;
|
|
5
|
+
export declare function makeFindingId(tool: string, rule: string, location: string, signature: string): string;
|
|
6
|
+
export declare function makeScanId(repo: string, at: string, findingIds: string[]): string;
|
|
7
|
+
/** Collapse all whitespace runs so trivial reflows keep one signature. */
|
|
8
|
+
export declare function collapseWs(text: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Normalize a file path from a tool report to repo-relative form. Reports
|
|
11
|
+
* may carry absolute paths (actionpin echoes the scanned path); anything
|
|
12
|
+
* under the scan root is relativized, separators become forward slashes.
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizePath(p: string, root?: string): string;
|
|
15
|
+
export declare function median(nums: number[]): number | null;
|
|
16
|
+
export declare function formatDuration(ms: number | null): string;
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small deterministic helpers: hashing, stable IDs, path normalization,
|
|
3
|
+
* and time math. Everything here is pure so diffs and reports built on it
|
|
4
|
+
* are reproducible from the same store state.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { isAbsolute, relative, sep } from "node:path";
|
|
8
|
+
export function sha256(text) {
|
|
9
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
10
|
+
}
|
|
11
|
+
/** Content signature for a finding: hash of its defining content parts. */
|
|
12
|
+
export function signatureOf(parts) {
|
|
13
|
+
return sha256(parts.join("\n"));
|
|
14
|
+
}
|
|
15
|
+
export function makeIdentity(tool, location, rule) {
|
|
16
|
+
return `${tool}|${location}|${rule}`;
|
|
17
|
+
}
|
|
18
|
+
export function makeFindingId(tool, rule, location, signature) {
|
|
19
|
+
return `tw_${sha256(`${tool}|${rule}|${location}|${signature}`).slice(0, 16)}`;
|
|
20
|
+
}
|
|
21
|
+
export function makeScanId(repo, at, findingIds) {
|
|
22
|
+
return `sc_${sha256(`${repo}|${at}|${[...findingIds].sort().join(",")}`).slice(0, 12)}`;
|
|
23
|
+
}
|
|
24
|
+
/** Collapse all whitespace runs so trivial reflows keep one signature. */
|
|
25
|
+
export function collapseWs(text) {
|
|
26
|
+
return text.replace(/\s+/g, " ").trim();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Normalize a file path from a tool report to repo-relative form. Reports
|
|
30
|
+
* may carry absolute paths (actionpin echoes the scanned path); anything
|
|
31
|
+
* under the scan root is relativized, separators become forward slashes.
|
|
32
|
+
*/
|
|
33
|
+
export function normalizePath(p, root) {
|
|
34
|
+
let out = p;
|
|
35
|
+
if (root && isAbsolute(out)) {
|
|
36
|
+
const rel = relative(root, out);
|
|
37
|
+
if (!rel.startsWith(".."))
|
|
38
|
+
out = rel;
|
|
39
|
+
}
|
|
40
|
+
return out.split(sep).join("/");
|
|
41
|
+
}
|
|
42
|
+
export function median(nums) {
|
|
43
|
+
if (nums.length === 0)
|
|
44
|
+
return null;
|
|
45
|
+
const sorted = [...nums].sort((a, b) => a - b);
|
|
46
|
+
const mid = Math.floor(sorted.length / 2);
|
|
47
|
+
return sorted.length % 2 === 1 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
|
|
48
|
+
}
|
|
49
|
+
export function formatDuration(ms) {
|
|
50
|
+
if (ms === null || !Number.isFinite(ms))
|
|
51
|
+
return "n/a";
|
|
52
|
+
if (ms < 1000)
|
|
53
|
+
return `${Math.round(ms)} ms`;
|
|
54
|
+
const s = ms / 1000;
|
|
55
|
+
if (s < 60)
|
|
56
|
+
return `${s.toFixed(1)} s`;
|
|
57
|
+
const m = s / 60;
|
|
58
|
+
if (m < 60)
|
|
59
|
+
return `${m.toFixed(1)} min`;
|
|
60
|
+
const h = m / 60;
|
|
61
|
+
if (h < 48)
|
|
62
|
+
return `${h.toFixed(1)} h`;
|
|
63
|
+
return `${(h / 24).toFixed(1)} d`;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=util.js.map
|
package/dist/util.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAEtD,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjE,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,WAAW,CAAC,KAAe;IACzC,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,QAAgB,EAAE,IAAY;IACvE,OAAO,GAAG,IAAI,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,IAAY,EAAE,QAAgB,EAAE,SAAiB;IAC3F,OAAO,MAAM,MAAM,CAAC,GAAG,IAAI,IAAI,IAAI,IAAI,QAAQ,IAAI,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AACjF,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,EAAU,EAAE,UAAoB;IACvE,OAAO,MAAM,MAAM,CAAC,GAAG,IAAI,IAAI,EAAE,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AAC1F,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,CAAS,EAAE,IAAa;IACpD,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,IAAI,IAAI,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAChC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,GAAG,GAAG,GAAG,CAAC;IACvC,CAAC;IACD,OAAO,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAc;IACnC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC1C,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AACjG,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,EAAiB;IAC9C,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,OAAO,KAAK,CAAC;IACtD,IAAI,EAAE,GAAG,IAAI;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC;IAC7C,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;IACpB,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IACvC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;IACjB,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IACzC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;IACjB,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IACvC,OAAO,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACpC,CAAC"}
|
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type AdaptedReport } from "./adapters.js";
|
|
2
|
+
import { type IngestResult } from "./ingest.js";
|
|
3
|
+
import type { Store } from "./store.js";
|
|
4
|
+
import type { AlertRecord } from "./types.js";
|
|
5
|
+
export interface FileMark {
|
|
6
|
+
mtimeMs: number;
|
|
7
|
+
size: number;
|
|
8
|
+
birthtimeMs: number;
|
|
9
|
+
hash?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface RepoWatchState {
|
|
12
|
+
target: string;
|
|
13
|
+
lastHead?: string | null;
|
|
14
|
+
snapshot?: Record<string, FileMark>;
|
|
15
|
+
issuesSeen?: Record<string, string>;
|
|
16
|
+
issueNoteAt?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface WatchStateFile {
|
|
19
|
+
repos: Record<string, RepoWatchState>;
|
|
20
|
+
}
|
|
21
|
+
export declare function snapshotTree(root: string): Record<string, FileMark>;
|
|
22
|
+
export interface WatchDeps {
|
|
23
|
+
scanFile: (path: string) => Promise<AdaptedReport | null>;
|
|
24
|
+
ingest: (store: Store, path: string, opts: {
|
|
25
|
+
name?: string;
|
|
26
|
+
source?: "watch";
|
|
27
|
+
}) => Promise<IngestResult>;
|
|
28
|
+
ghGet: (path: string) => Promise<string | null>;
|
|
29
|
+
now: () => Date;
|
|
30
|
+
/** Tool version lookup; injectable so tests never spawn real tools. */
|
|
31
|
+
version?: (tool: "injectcheck") => Promise<string>;
|
|
32
|
+
}
|
|
33
|
+
export declare const defaultDeps: WatchDeps;
|
|
34
|
+
export interface WatchContext {
|
|
35
|
+
store: Store;
|
|
36
|
+
deps: WatchDeps;
|
|
37
|
+
emit: (alert: AlertRecord) => void;
|
|
38
|
+
}
|
|
39
|
+
/** One local poll cycle. Returns the alerts emitted this cycle. */
|
|
40
|
+
export declare function pollLocalOnce(ctx: WatchContext, repo: string, targetPath: string): Promise<AlertRecord[]>;
|
|
41
|
+
/** One GitHub poll cycle (issues/PRs + HEAD). Returns alerts emitted. */
|
|
42
|
+
export declare function pollGithubOnce(ctx: WatchContext, slug: string, opts?: {
|
|
43
|
+
path?: string;
|
|
44
|
+
}): Promise<AlertRecord[]>;
|
|
45
|
+
export interface WatchOptions {
|
|
46
|
+
store: Store;
|
|
47
|
+
target: string;
|
|
48
|
+
/** Repo key override (default: directory basename, or the slug itself). */
|
|
49
|
+
name?: string;
|
|
50
|
+
path?: string;
|
|
51
|
+
intervalSec?: number;
|
|
52
|
+
once?: boolean;
|
|
53
|
+
maxMinutes?: number;
|
|
54
|
+
alertsFile?: string;
|
|
55
|
+
deps?: WatchDeps;
|
|
56
|
+
stderr?: (line: string) => void;
|
|
57
|
+
}
|
|
58
|
+
/** Run watch until stopped (SIGINT/SIGTERM), --once, or --max-minutes. */
|
|
59
|
+
export declare function watch(opts: WatchOptions): Promise<{
|
|
60
|
+
cycles: number;
|
|
61
|
+
alerts: number;
|
|
62
|
+
}>;
|