@stevenvincentone/intidev-agentloops 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.
@@ -0,0 +1,364 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PostgresStateBackend = exports.TICKET_SCHEMA_SQL = void 0;
4
+ exports.serializeState = serializeState;
5
+ exports.deserializeRows = deserializeRows;
6
+ /**
7
+ * Public relational schema for the ledger. Faithfully mirrors `LoopState`:
8
+ * timestamps are stored as ISO text to keep round-trips identical to the JSON
9
+ * backend, and ordered child collections carry an `ord` column.
10
+ */
11
+ exports.TICKET_SCHEMA_SQL = `
12
+ CREATE TABLE IF NOT EXISTS loop_meta (
13
+ id integer PRIMARY KEY DEFAULT 1 CHECK (id = 1),
14
+ project text NOT NULL,
15
+ version integer NOT NULL,
16
+ created_at text NOT NULL,
17
+ updated_at text NOT NULL,
18
+ next_ticket_seq integer NOT NULL,
19
+ next_pattern_seq integer NOT NULL
20
+ );
21
+ CREATE TABLE IF NOT EXISTS ticket_patterns (
22
+ id text PRIMARY KEY,
23
+ family text NOT NULL,
24
+ title text NOT NULL,
25
+ status text NOT NULL,
26
+ created_at text NOT NULL,
27
+ updated_at text NOT NULL
28
+ );
29
+ CREATE TABLE IF NOT EXISTS tickets (
30
+ id text PRIMARY KEY,
31
+ family text NOT NULL,
32
+ kind text NOT NULL,
33
+ source text NOT NULL,
34
+ title text NOT NULL,
35
+ summary text NOT NULL,
36
+ severity text NOT NULL,
37
+ confidence text NOT NULL,
38
+ status text NOT NULL,
39
+ created_at text NOT NULL,
40
+ updated_at text NOT NULL,
41
+ started_at text,
42
+ resolved_at text,
43
+ handoff_text text,
44
+ guard_status text,
45
+ guard_summary text,
46
+ pattern_id text,
47
+ verification text,
48
+ reproducible boolean,
49
+ resolution_summary text
50
+ );
51
+ CREATE TABLE IF NOT EXISTS ticket_aliases (ticket_id text NOT NULL, alias text NOT NULL, ord integer NOT NULL);
52
+ CREATE TABLE IF NOT EXISTS ticket_tags (ticket_id text NOT NULL, tag text NOT NULL, ord integer NOT NULL);
53
+ CREATE TABLE IF NOT EXISTS ticket_notes (
54
+ id text NOT NULL,
55
+ ticket_id text NOT NULL,
56
+ type text NOT NULL,
57
+ body text NOT NULL,
58
+ author text,
59
+ created_at text NOT NULL,
60
+ ord integer NOT NULL
61
+ );
62
+ CREATE TABLE IF NOT EXISTS ticket_pattern_links (pattern_id text NOT NULL, ticket_id text NOT NULL, ord integer NOT NULL);
63
+ `;
64
+ /** Tables in dependency-free delete order (no FKs; order is for readability). */
65
+ const TABLES = [
66
+ "ticket_pattern_links",
67
+ "ticket_notes",
68
+ "ticket_tags",
69
+ "ticket_aliases",
70
+ "tickets",
71
+ "ticket_patterns",
72
+ "loop_meta",
73
+ ];
74
+ // --- Pure mapping (LoopState <-> relational rows), no DB -------------------
75
+ function serializeState(state) {
76
+ const aliases = [];
77
+ const tags = [];
78
+ const notes = [];
79
+ const links = [];
80
+ for (const ticket of state.tickets) {
81
+ ticket.aliases.forEach((alias, ord) => aliases.push({ ticketId: ticket.id, alias, ord }));
82
+ ticket.tags.forEach((tag, ord) => tags.push({ ticketId: ticket.id, tag, ord }));
83
+ ticket.notes.forEach((note, ord) => notes.push({
84
+ id: note.id,
85
+ ticketId: ticket.id,
86
+ type: note.type,
87
+ body: note.body,
88
+ author: note.author ?? null,
89
+ createdAt: note.createdAt,
90
+ ord,
91
+ }));
92
+ }
93
+ for (const pattern of state.patterns) {
94
+ pattern.ticketIds.forEach((ticketId, ord) => links.push({ patternId: pattern.id, ticketId, ord }));
95
+ }
96
+ return {
97
+ meta: {
98
+ project: state.project,
99
+ version: state.version,
100
+ createdAt: state.createdAt,
101
+ updatedAt: state.updatedAt,
102
+ nextTicketSeq: state.nextTicketSeq,
103
+ nextPatternSeq: state.nextPatternSeq,
104
+ },
105
+ patterns: state.patterns.map((p) => ({
106
+ id: p.id,
107
+ family: p.family,
108
+ title: p.title,
109
+ status: p.status,
110
+ createdAt: p.createdAt,
111
+ updatedAt: p.updatedAt,
112
+ })),
113
+ tickets: state.tickets.map((t) => ({
114
+ id: t.id,
115
+ family: t.family,
116
+ kind: t.kind,
117
+ source: t.source,
118
+ title: t.title,
119
+ summary: t.summary,
120
+ severity: t.severity,
121
+ confidence: t.confidence,
122
+ status: t.status,
123
+ createdAt: t.createdAt,
124
+ updatedAt: t.updatedAt,
125
+ startedAt: t.startedAt ?? null,
126
+ resolvedAt: t.resolvedAt ?? null,
127
+ handoffText: t.handoffText ?? null,
128
+ guardStatus: t.guardStatus ?? null,
129
+ guardSummary: t.guardSummary ?? null,
130
+ patternId: t.patternId ?? null,
131
+ verification: t.verification ?? null,
132
+ reproducible: t.reproducible ?? null,
133
+ resolutionSummary: t.resolutionSummary ?? null,
134
+ })),
135
+ aliases,
136
+ tags,
137
+ notes,
138
+ links,
139
+ };
140
+ }
141
+ function byOrd(a, b) {
142
+ return a.ord - b.ord;
143
+ }
144
+ function deserializeRows(rows) {
145
+ const aliasesByTicket = new Map();
146
+ for (const a of rows.aliases)
147
+ (aliasesByTicket.get(a.ticketId) ?? aliasesByTicket.set(a.ticketId, []).get(a.ticketId)).push(a);
148
+ const tagsByTicket = new Map();
149
+ for (const t of rows.tags)
150
+ (tagsByTicket.get(t.ticketId) ?? tagsByTicket.set(t.ticketId, []).get(t.ticketId)).push(t);
151
+ const notesByTicket = new Map();
152
+ for (const n of rows.notes)
153
+ (notesByTicket.get(n.ticketId) ?? notesByTicket.set(n.ticketId, []).get(n.ticketId)).push(n);
154
+ const linksByPattern = new Map();
155
+ for (const l of rows.links)
156
+ (linksByPattern.get(l.patternId) ?? linksByPattern.set(l.patternId, []).get(l.patternId)).push(l);
157
+ const tickets = rows.tickets.map((row) => {
158
+ const ticket = {
159
+ id: row.id,
160
+ family: row.family,
161
+ kind: row.kind,
162
+ source: row.source,
163
+ title: row.title,
164
+ summary: row.summary,
165
+ severity: row.severity,
166
+ confidence: row.confidence,
167
+ status: row.status,
168
+ createdAt: row.createdAt,
169
+ updatedAt: row.updatedAt,
170
+ aliases: (aliasesByTicket.get(row.id) ?? []).sort(byOrd).map((a) => a.alias),
171
+ tags: (tagsByTicket.get(row.id) ?? []).sort(byOrd).map((t) => t.tag),
172
+ notes: (notesByTicket.get(row.id) ?? []).sort(byOrd).map((n) => {
173
+ const note = {
174
+ id: n.id,
175
+ type: n.type,
176
+ body: n.body,
177
+ createdAt: n.createdAt,
178
+ };
179
+ if (n.author != null)
180
+ note.author = n.author;
181
+ return note;
182
+ }),
183
+ };
184
+ if (row.startedAt != null)
185
+ ticket.startedAt = row.startedAt;
186
+ if (row.resolvedAt != null)
187
+ ticket.resolvedAt = row.resolvedAt;
188
+ if (row.handoffText != null)
189
+ ticket.handoffText = row.handoffText;
190
+ if (row.guardStatus != null)
191
+ ticket.guardStatus = row.guardStatus;
192
+ if (row.guardSummary != null)
193
+ ticket.guardSummary = row.guardSummary;
194
+ if (row.patternId != null)
195
+ ticket.patternId = row.patternId;
196
+ if (row.verification != null)
197
+ ticket.verification = row.verification;
198
+ if (row.reproducible != null)
199
+ ticket.reproducible = row.reproducible;
200
+ if (row.resolutionSummary != null)
201
+ ticket.resolutionSummary = row.resolutionSummary;
202
+ return ticket;
203
+ });
204
+ const patterns = rows.patterns.map((row) => ({
205
+ id: row.id,
206
+ family: row.family,
207
+ title: row.title,
208
+ status: row.status,
209
+ createdAt: row.createdAt,
210
+ updatedAt: row.updatedAt,
211
+ ticketIds: (linksByPattern.get(row.id) ?? []).sort(byOrd).map((l) => l.ticketId),
212
+ }));
213
+ return {
214
+ version: rows.meta.version,
215
+ project: rows.meta.project,
216
+ createdAt: rows.meta.createdAt,
217
+ updatedAt: rows.meta.updatedAt,
218
+ nextTicketSeq: rows.meta.nextTicketSeq,
219
+ nextPatternSeq: rows.meta.nextPatternSeq,
220
+ tickets,
221
+ patterns,
222
+ };
223
+ }
224
+ function isPool(client) {
225
+ return typeof client.connect === "function";
226
+ }
227
+ /**
228
+ * Postgres-backed `StateBackend` over the relational `ticket_*` schema. Brings
229
+ * no `pg` dependency — the host injects a `pg.Pool`/`pg.Client`-shaped client.
230
+ * Saves are transactional (whole-snapshot replace), so a failed write never
231
+ * leaves a partially-updated ledger.
232
+ */
233
+ class PostgresStateBackend {
234
+ client;
235
+ migrated = false;
236
+ constructor(client) {
237
+ this.client = client;
238
+ }
239
+ async migrate() {
240
+ await this.client.query(exports.TICKET_SCHEMA_SQL);
241
+ this.migrated = true;
242
+ }
243
+ async ensureSchema() {
244
+ if (!this.migrated)
245
+ await this.migrate();
246
+ }
247
+ async withConnection(run) {
248
+ if (isPool(this.client)) {
249
+ const conn = await this.client.connect();
250
+ try {
251
+ return await run(conn);
252
+ }
253
+ finally {
254
+ conn.release();
255
+ }
256
+ }
257
+ return run(this.client);
258
+ }
259
+ async load() {
260
+ await this.ensureSchema();
261
+ return this.withConnection(async (c) => {
262
+ const meta = (await c.query("SELECT * FROM loop_meta WHERE id = 1")).rows[0];
263
+ if (!meta)
264
+ return null;
265
+ const patterns = (await c.query("SELECT * FROM ticket_patterns ORDER BY id")).rows;
266
+ const tickets = (await c.query("SELECT * FROM tickets ORDER BY id")).rows;
267
+ const aliases = (await c.query("SELECT * FROM ticket_aliases ORDER BY ticket_id, ord")).rows;
268
+ const tags = (await c.query("SELECT * FROM ticket_tags ORDER BY ticket_id, ord")).rows;
269
+ const notes = (await c.query("SELECT * FROM ticket_notes ORDER BY ticket_id, ord")).rows;
270
+ const links = (await c.query("SELECT * FROM ticket_pattern_links ORDER BY pattern_id, ord")).rows;
271
+ const rows = {
272
+ meta: {
273
+ project: String(meta.project),
274
+ version: Number(meta.version),
275
+ createdAt: String(meta.created_at),
276
+ updatedAt: String(meta.updated_at),
277
+ nextTicketSeq: Number(meta.next_ticket_seq),
278
+ nextPatternSeq: Number(meta.next_pattern_seq),
279
+ },
280
+ patterns: patterns.map((r) => ({
281
+ id: String(r.id),
282
+ family: String(r.family),
283
+ title: String(r.title),
284
+ status: String(r.status),
285
+ createdAt: String(r.created_at),
286
+ updatedAt: String(r.updated_at),
287
+ })),
288
+ tickets: tickets.map((r) => ({
289
+ id: String(r.id),
290
+ family: String(r.family),
291
+ kind: String(r.kind),
292
+ source: String(r.source),
293
+ title: String(r.title),
294
+ summary: String(r.summary),
295
+ severity: String(r.severity),
296
+ confidence: String(r.confidence),
297
+ status: String(r.status),
298
+ createdAt: String(r.created_at),
299
+ updatedAt: String(r.updated_at),
300
+ startedAt: r.started_at ?? null,
301
+ resolvedAt: r.resolved_at ?? null,
302
+ handoffText: r.handoff_text ?? null,
303
+ guardStatus: r.guard_status ?? null,
304
+ guardSummary: r.guard_summary ?? null,
305
+ patternId: r.pattern_id ?? null,
306
+ verification: r.verification ?? null,
307
+ reproducible: r.reproducible ?? null,
308
+ resolutionSummary: r.resolution_summary ?? null,
309
+ })),
310
+ aliases: aliases.map((r) => ({ ticketId: String(r.ticket_id), alias: String(r.alias), ord: Number(r.ord) })),
311
+ tags: tags.map((r) => ({ ticketId: String(r.ticket_id), tag: String(r.tag), ord: Number(r.ord) })),
312
+ notes: notes.map((r) => ({
313
+ id: String(r.id),
314
+ ticketId: String(r.ticket_id),
315
+ type: String(r.type),
316
+ body: String(r.body),
317
+ author: r.author ?? null,
318
+ createdAt: String(r.created_at),
319
+ ord: Number(r.ord),
320
+ })),
321
+ links: links.map((r) => ({ patternId: String(r.pattern_id), ticketId: String(r.ticket_id), ord: Number(r.ord) })),
322
+ };
323
+ return deserializeRows(rows);
324
+ });
325
+ }
326
+ async save(state) {
327
+ await this.ensureSchema();
328
+ const rows = serializeState(state);
329
+ await this.withConnection(async (c) => {
330
+ await c.query("BEGIN");
331
+ try {
332
+ for (const table of TABLES)
333
+ await c.query(`DELETE FROM ${table}`);
334
+ await c.query("INSERT INTO loop_meta (id, project, version, created_at, updated_at, next_ticket_seq, next_pattern_seq) VALUES (1, $1, $2, $3, $4, $5, $6)", [rows.meta.project, rows.meta.version, rows.meta.createdAt, rows.meta.updatedAt, rows.meta.nextTicketSeq, rows.meta.nextPatternSeq]);
335
+ for (const p of rows.patterns) {
336
+ await c.query("INSERT INTO ticket_patterns (id, family, title, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6)", [p.id, p.family, p.title, p.status, p.createdAt, p.updatedAt]);
337
+ }
338
+ for (const t of rows.tickets) {
339
+ await c.query(`INSERT INTO tickets (id, family, kind, source, title, summary, severity, confidence, status, created_at, updated_at, started_at, resolved_at, handoff_text, guard_status, guard_summary, pattern_id, verification, reproducible, resolution_summary)
340
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20)`, [t.id, t.family, t.kind, t.source, t.title, t.summary, t.severity, t.confidence, t.status, t.createdAt, t.updatedAt, t.startedAt, t.resolvedAt, t.handoffText, t.guardStatus, t.guardSummary, t.patternId, t.verification, t.reproducible, t.resolutionSummary]);
341
+ }
342
+ for (const a of rows.aliases) {
343
+ await c.query("INSERT INTO ticket_aliases (ticket_id, alias, ord) VALUES ($1, $2, $3)", [a.ticketId, a.alias, a.ord]);
344
+ }
345
+ for (const tag of rows.tags) {
346
+ await c.query("INSERT INTO ticket_tags (ticket_id, tag, ord) VALUES ($1, $2, $3)", [tag.ticketId, tag.tag, tag.ord]);
347
+ }
348
+ for (const n of rows.notes) {
349
+ await c.query("INSERT INTO ticket_notes (id, ticket_id, type, body, author, created_at, ord) VALUES ($1, $2, $3, $4, $5, $6, $7)", [n.id, n.ticketId, n.type, n.body, n.author, n.createdAt, n.ord]);
350
+ }
351
+ for (const l of rows.links) {
352
+ await c.query("INSERT INTO ticket_pattern_links (pattern_id, ticket_id, ord) VALUES ($1, $2, $3)", [l.patternId, l.ticketId, l.ord]);
353
+ }
354
+ await c.query("COMMIT");
355
+ }
356
+ catch (error) {
357
+ await c.query("ROLLBACK");
358
+ throw error;
359
+ }
360
+ });
361
+ }
362
+ }
363
+ exports.PostgresStateBackend = PostgresStateBackend;
364
+ //# sourceMappingURL=postgres.js.map
@@ -0,0 +1,65 @@
1
+ import { Ticket } from "./types";
2
+ export declare const PRIOR_ART_SCHEMA_VERSION: 1;
3
+ /**
4
+ * Weights for the deterministic relatedness signals. Fixed defaults live in core
5
+ * (keeping behavior deterministic per the extraction principles) but a project
6
+ * can override any of them via `config.priorArt.weights`.
7
+ */
8
+ export interface PriorArtWeights {
9
+ /** Same family. */
10
+ family: number;
11
+ /** Belongs to the same Pattern. */
12
+ pattern: number;
13
+ /** Per shared tag. */
14
+ tag: number;
15
+ /** Same ticket kind. */
16
+ kind: number;
17
+ /** Multiplied by the title/summary token overlap (Jaccard, 0..1). */
18
+ textOverlap: number;
19
+ }
20
+ export declare const DEFAULT_PRIOR_ART_WEIGHTS: PriorArtWeights;
21
+ export interface PriorArtOptions {
22
+ weights?: Partial<PriorArtWeights>;
23
+ /** Minimum score for a candidate to be considered related. Default 1. */
24
+ minScore?: number;
25
+ /** Maximum related tickets to return. Default 10. */
26
+ limit?: number;
27
+ }
28
+ export interface RelatedTicket {
29
+ id: string;
30
+ alias: string;
31
+ kind: string;
32
+ source: string;
33
+ family: string;
34
+ status: string;
35
+ title: string;
36
+ score: number;
37
+ /** Human-readable evidence for the edge, e.g. ["family", "tag:export", "text:0.42"]. */
38
+ signals: string[];
39
+ }
40
+ export interface PriorArtReport {
41
+ schemaVersion: typeof PRIOR_ART_SCHEMA_VERSION;
42
+ generatedAt: string;
43
+ ticket: {
44
+ id: string;
45
+ alias: string;
46
+ family: string;
47
+ kind: string;
48
+ title: string;
49
+ };
50
+ weights: PriorArtWeights;
51
+ filters: {
52
+ minScore: number;
53
+ limit: number;
54
+ };
55
+ related: RelatedTicket[];
56
+ }
57
+ /**
58
+ * Prior-art lookup (ported concept from Inti's relationship graph): rank the
59
+ * tickets most related to `targetId` using deterministic signals — shared
60
+ * family, shared Pattern, shared tags, same kind, and title/summary token
61
+ * overlap — each contributing a configurable weight.
62
+ *
63
+ * Pure and deterministic apart from `generatedAt`.
64
+ */
65
+ export declare function relatedTickets(targetId: string, tickets: Ticket[], options?: PriorArtOptions): PriorArtReport;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_PRIOR_ART_WEIGHTS = exports.PRIOR_ART_SCHEMA_VERSION = void 0;
4
+ exports.relatedTickets = relatedTickets;
5
+ exports.PRIOR_ART_SCHEMA_VERSION = 1;
6
+ exports.DEFAULT_PRIOR_ART_WEIGHTS = {
7
+ family: 3,
8
+ pattern: 3,
9
+ tag: 2,
10
+ kind: 1,
11
+ textOverlap: 4,
12
+ };
13
+ const STOP_TOKEN_MIN_LENGTH = 3;
14
+ function tokenize(text) {
15
+ return new Set(text
16
+ .toLowerCase()
17
+ .split(/[^a-z0-9]+/)
18
+ .filter((token) => token.length >= STOP_TOKEN_MIN_LENGTH));
19
+ }
20
+ function jaccard(a, b) {
21
+ if (a.size === 0 || b.size === 0)
22
+ return 0;
23
+ let intersection = 0;
24
+ for (const token of a)
25
+ if (b.has(token))
26
+ intersection += 1;
27
+ const union = a.size + b.size - intersection;
28
+ return union === 0 ? 0 : intersection / union;
29
+ }
30
+ function round(value) {
31
+ return Math.round(value * 100) / 100;
32
+ }
33
+ /**
34
+ * Prior-art lookup (ported concept from Inti's relationship graph): rank the
35
+ * tickets most related to `targetId` using deterministic signals — shared
36
+ * family, shared Pattern, shared tags, same kind, and title/summary token
37
+ * overlap — each contributing a configurable weight.
38
+ *
39
+ * Pure and deterministic apart from `generatedAt`.
40
+ */
41
+ function relatedTickets(targetId, tickets, options = {}) {
42
+ const weights = { ...exports.DEFAULT_PRIOR_ART_WEIGHTS, ...options.weights };
43
+ const minScore = options.minScore ?? 1;
44
+ const limit = options.limit ?? 10;
45
+ const target = tickets.find((ticket) => ticket.id === targetId);
46
+ if (!target) {
47
+ throw new Error(`Not found: ${targetId}`);
48
+ }
49
+ const targetTokens = tokenize(`${target.title} ${target.summary}`);
50
+ const targetTags = new Set(target.tags);
51
+ const related = [];
52
+ for (const candidate of tickets) {
53
+ if (candidate.id === target.id)
54
+ continue;
55
+ let score = 0;
56
+ const signals = [];
57
+ if (weights.family && candidate.family === target.family) {
58
+ score += weights.family;
59
+ signals.push("family");
60
+ }
61
+ if (weights.pattern && candidate.patternId && candidate.patternId === target.patternId) {
62
+ score += weights.pattern;
63
+ signals.push("pattern");
64
+ }
65
+ if (weights.tag) {
66
+ for (const tag of candidate.tags) {
67
+ if (targetTags.has(tag)) {
68
+ score += weights.tag;
69
+ signals.push(`tag:${tag}`);
70
+ }
71
+ }
72
+ }
73
+ if (weights.kind && candidate.kind === target.kind) {
74
+ score += weights.kind;
75
+ signals.push("kind");
76
+ }
77
+ if (weights.textOverlap) {
78
+ const overlap = jaccard(targetTokens, tokenize(`${candidate.title} ${candidate.summary}`));
79
+ if (overlap > 0) {
80
+ score += weights.textOverlap * overlap;
81
+ signals.push(`text:${overlap.toFixed(2)}`);
82
+ }
83
+ }
84
+ if (score >= minScore) {
85
+ related.push({
86
+ id: candidate.id,
87
+ alias: candidate.aliases[0] ?? candidate.id,
88
+ kind: candidate.kind,
89
+ source: candidate.source,
90
+ family: candidate.family,
91
+ status: candidate.status,
92
+ title: candidate.title,
93
+ score: round(score),
94
+ signals,
95
+ });
96
+ }
97
+ }
98
+ related.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
99
+ return {
100
+ schemaVersion: exports.PRIOR_ART_SCHEMA_VERSION,
101
+ generatedAt: new Date().toISOString(),
102
+ ticket: {
103
+ id: target.id,
104
+ alias: target.aliases[0] ?? target.id,
105
+ family: target.family,
106
+ kind: target.kind,
107
+ title: target.title,
108
+ },
109
+ weights,
110
+ filters: { minScore, limit },
111
+ related: related.slice(0, limit),
112
+ };
113
+ }
114
+ //# sourceMappingURL=prior-art.js.map
@@ -0,0 +1,14 @@
1
+ import { ProjectConfig, RedactionRule, TicketRedactor } from "./types";
2
+ /** Identity redactor — stores content unchanged. The default. */
3
+ export declare const noopRedactor: TicketRedactor;
4
+ /**
5
+ * Build a redactor from a list of regex rules. Each rule's pattern is replaced
6
+ * with its `replacement` (default `[redacted]`). `redactJson` walks structures
7
+ * and redacts every string leaf.
8
+ */
9
+ export declare function createPatternRedactor(rules: RedactionRule[]): TicketRedactor;
10
+ /**
11
+ * Resolve the redactor to use: an explicit override wins; otherwise build one
12
+ * from `config.redaction.patterns`; otherwise the no-op redactor.
13
+ */
14
+ export declare function resolveRedactor(config: ProjectConfig, override?: TicketRedactor): TicketRedactor;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noopRedactor = void 0;
4
+ exports.createPatternRedactor = createPatternRedactor;
5
+ exports.resolveRedactor = resolveRedactor;
6
+ /** Identity redactor — stores content unchanged. The default. */
7
+ exports.noopRedactor = {
8
+ redactText: (value) => value,
9
+ redactJson: (value) => value,
10
+ };
11
+ /**
12
+ * Build a redactor from a list of regex rules. Each rule's pattern is replaced
13
+ * with its `replacement` (default `[redacted]`). `redactJson` walks structures
14
+ * and redacts every string leaf.
15
+ */
16
+ function createPatternRedactor(rules) {
17
+ const compiled = rules.map((rule) => ({
18
+ regex: new RegExp(rule.pattern, rule.flags ?? "g"),
19
+ replacement: rule.replacement ?? "[redacted]",
20
+ }));
21
+ const redactText = (value) => {
22
+ let out = value;
23
+ for (const { regex, replacement } of compiled) {
24
+ regex.lastIndex = 0;
25
+ out = out.replace(regex, replacement);
26
+ }
27
+ return out;
28
+ };
29
+ const redactJson = (value) => {
30
+ if (typeof value === "string")
31
+ return redactText(value);
32
+ if (Array.isArray(value))
33
+ return value.map(redactJson);
34
+ if (value && typeof value === "object") {
35
+ const out = {};
36
+ for (const [key, val] of Object.entries(value)) {
37
+ out[key] = redactJson(val);
38
+ }
39
+ return out;
40
+ }
41
+ return value;
42
+ };
43
+ return { redactText, redactJson };
44
+ }
45
+ /**
46
+ * Resolve the redactor to use: an explicit override wins; otherwise build one
47
+ * from `config.redaction.patterns`; otherwise the no-op redactor.
48
+ */
49
+ function resolveRedactor(config, override) {
50
+ if (override)
51
+ return override;
52
+ const rules = config.redaction?.patterns;
53
+ if (rules && rules.length > 0)
54
+ return createPatternRedactor(rules);
55
+ return exports.noopRedactor;
56
+ }
57
+ //# sourceMappingURL=redaction.js.map
@@ -0,0 +1,8 @@
1
+ import { type Server } from "node:http";
2
+ import { AgentLoopStore } from "./store";
3
+ /**
4
+ * A zero-dependency HTTP server that renders the dashboard at `/` and exposes
5
+ * read-only JSON at `/api/*`, re-reading the store on each request (so it
6
+ * reflects the live ledger over filesystem or Postgres).
7
+ */
8
+ export declare function createDashboardServer(store: AgentLoopStore): Server;
package/dist/serve.js ADDED
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDashboardServer = createDashboardServer;
4
+ const node_http_1 = require("node:http");
5
+ const dashboard_1 = require("./dashboard");
6
+ function sendJson(res, value) {
7
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
8
+ res.end(JSON.stringify(value, null, 2));
9
+ }
10
+ async function handle(store, req, res) {
11
+ const url = (req.url ?? "/").split("?")[0];
12
+ try {
13
+ if (url === "/api/summary")
14
+ return sendJson(res, await store.summary());
15
+ if (url === "/api/tickets")
16
+ return sendJson(res, await store.listTickets({ status: "all" }));
17
+ if (url === "/api/patterns")
18
+ return sendJson(res, await store.listPatterns({ status: "all" }));
19
+ if (url === "/api/convergence") {
20
+ return sendJson(res, await store.sourceConvergence({ includeAll: true }));
21
+ }
22
+ if (url === "/api/guard-gaps")
23
+ return sendJson(res, await store.guardGaps({}));
24
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
25
+ res.end((0, dashboard_1.renderDashboard)(await (0, dashboard_1.gatherDashboardData)(store)));
26
+ }
27
+ catch (error) {
28
+ res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
29
+ res.end(error instanceof Error ? error.message : String(error));
30
+ }
31
+ }
32
+ /**
33
+ * A zero-dependency HTTP server that renders the dashboard at `/` and exposes
34
+ * read-only JSON at `/api/*`, re-reading the store on each request (so it
35
+ * reflects the live ledger over filesystem or Postgres).
36
+ */
37
+ function createDashboardServer(store) {
38
+ return (0, node_http_1.createServer)((req, res) => {
39
+ void handle(store, req, res);
40
+ });
41
+ }
42
+ //# sourceMappingURL=serve.js.map
@@ -0,0 +1,22 @@
1
+ import { ProjectConfig } from "./types";
2
+ import { StateBackend } from "./backend";
3
+ export interface BackendSelection {
4
+ backend: StateBackend;
5
+ kind: "filesystem" | "postgres";
6
+ /** Release any held resources (e.g. close the Postgres pool). */
7
+ dispose(): Promise<void>;
8
+ }
9
+ export interface ResolveBackendOptions {
10
+ cwd: string;
11
+ config: ProjectConfig;
12
+ /** Explicit connection string; overrides env and config. */
13
+ databaseUrl?: string;
14
+ }
15
+ /** Connection string precedence: explicit arg → `DATABASE_URL` env → config. */
16
+ export declare function resolvePostgresUrl(options: ResolveBackendOptions): string | undefined;
17
+ /**
18
+ * Pick a `StateBackend` for the CLI/MCP: Postgres when a connection string is
19
+ * configured, otherwise the filesystem. `pg` is loaded lazily and is an optional
20
+ * peer dependency — filesystem users never need it.
21
+ */
22
+ export declare function resolveBackend(options: ResolveBackendOptions): Promise<BackendSelection>;