friction-log 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/LICENSE +21 -0
  3. package/README.md +98 -2
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +504 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/bilanz.d.ts +23 -0
  8. package/dist/commands/bilanz.js +106 -0
  9. package/dist/commands/bilanz.js.map +1 -0
  10. package/dist/commands/digest.d.ts +36 -0
  11. package/dist/commands/digest.js +288 -0
  12. package/dist/commands/digest.js.map +1 -0
  13. package/dist/commands/export.d.ts +36 -0
  14. package/dist/commands/export.js +118 -0
  15. package/dist/commands/export.js.map +1 -0
  16. package/dist/commands/file.d.ts +18 -0
  17. package/dist/commands/file.js +51 -0
  18. package/dist/commands/file.js.map +1 -0
  19. package/dist/commands/import.d.ts +31 -0
  20. package/dist/commands/import.js +283 -0
  21. package/dist/commands/import.js.map +1 -0
  22. package/dist/commands/init.d.ts +44 -0
  23. package/dist/commands/init.js +256 -0
  24. package/dist/commands/init.js.map +1 -0
  25. package/dist/commands/list.d.ts +16 -0
  26. package/dist/commands/list.js +57 -0
  27. package/dist/commands/list.js.map +1 -0
  28. package/dist/commands/log.d.ts +18 -0
  29. package/dist/commands/log.js +52 -0
  30. package/dist/commands/log.js.map +1 -0
  31. package/dist/commands/rm.d.ts +9 -0
  32. package/dist/commands/rm.js +22 -0
  33. package/dist/commands/rm.js.map +1 -0
  34. package/dist/commands/scan.d.ts +25 -0
  35. package/dist/commands/scan.js +73 -0
  36. package/dist/commands/scan.js.map +1 -0
  37. package/dist/commands/search.d.ts +15 -0
  38. package/dist/commands/search.js +37 -0
  39. package/dist/commands/search.js.map +1 -0
  40. package/dist/commands/sync-export.d.ts +46 -0
  41. package/dist/commands/sync-export.js +80 -0
  42. package/dist/commands/sync-export.js.map +1 -0
  43. package/dist/commands/update.d.ts +12 -0
  44. package/dist/commands/update.js +24 -0
  45. package/dist/commands/update.js.map +1 -0
  46. package/dist/config.d.ts +31 -0
  47. package/dist/config.js +145 -0
  48. package/dist/config.js.map +1 -0
  49. package/dist/db.d.ts +81 -0
  50. package/dist/db.js +548 -0
  51. package/dist/db.js.map +1 -0
  52. package/dist/index.d.ts +15 -0
  53. package/dist/index.js +16 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/paths.d.ts +12 -0
  56. package/dist/paths.js +34 -0
  57. package/dist/paths.js.map +1 -0
  58. package/dist/scanners/claude-code.d.ts +6 -0
  59. package/dist/scanners/claude-code.js +202 -0
  60. package/dist/scanners/claude-code.js.map +1 -0
  61. package/dist/scanners/index.d.ts +4 -0
  62. package/dist/scanners/index.js +12 -0
  63. package/dist/scanners/index.js.map +1 -0
  64. package/dist/sinks/agent-tasks.d.ts +36 -0
  65. package/dist/sinks/agent-tasks.js +153 -0
  66. package/dist/sinks/agent-tasks.js.map +1 -0
  67. package/dist/sinks/github-issues.d.ts +20 -0
  68. package/dist/sinks/github-issues.js +98 -0
  69. package/dist/sinks/github-issues.js.map +1 -0
  70. package/dist/sinks/index.d.ts +11 -0
  71. package/dist/sinks/index.js +48 -0
  72. package/dist/sinks/index.js.map +1 -0
  73. package/dist/sinks/linear.d.ts +25 -0
  74. package/dist/sinks/linear.js +157 -0
  75. package/dist/sinks/linear.js.map +1 -0
  76. package/dist/sinks/markdown-file.d.ts +7 -0
  77. package/dist/sinks/markdown-file.js +54 -0
  78. package/dist/sinks/markdown-file.js.map +1 -0
  79. package/dist/sinks/stdout-json.d.ts +15 -0
  80. package/dist/sinks/stdout-json.js +49 -0
  81. package/dist/sinks/stdout-json.js.map +1 -0
  82. package/dist/templates/auth-expiry.yml +30 -0
  83. package/dist/templates/doc-gap.yml +30 -0
  84. package/dist/templates/output-overflow.yml +25 -0
  85. package/dist/templates/schema-drift.yml +26 -0
  86. package/dist/templates/tool-error.yml +34 -0
  87. package/dist/templates/tool-missing-capability.yml +26 -0
  88. package/dist/templates/workflow-friction.yml +26 -0
  89. package/dist/templates.d.ts +5 -0
  90. package/dist/templates.js +88 -0
  91. package/dist/templates.js.map +1 -0
  92. package/dist/types.d.ts +79 -0
  93. package/dist/types.js +2 -0
  94. package/dist/types.js.map +1 -0
  95. package/docs/commands.md +74 -0
  96. package/docs/design.md +25 -0
  97. package/docs/sinks.md +79 -0
  98. package/docs/storage.md +20 -0
  99. package/docs/sync-export.md +23 -0
  100. package/package.json +64 -4
  101. package/templates/auth-expiry.yml +30 -0
  102. package/templates/doc-gap.yml +30 -0
  103. package/templates/output-overflow.yml +25 -0
  104. package/templates/schema-drift.yml +26 -0
  105. package/templates/tool-error.yml +34 -0
  106. package/templates/tool-missing-capability.yml +26 -0
  107. package/templates/workflow-friction.yml +26 -0
@@ -0,0 +1,46 @@
1
+ import { type FrictionLogConfig } from "../config.js";
2
+ export interface SyncExportCommandInput {
3
+ dbPath?: string;
4
+ configPath?: string;
5
+ }
6
+ export interface SyncExportCommandOutput {
7
+ path: string;
8
+ origin: string;
9
+ count: number;
10
+ }
11
+ /**
12
+ * `friction-log sync-export`: explicit, on-demand write of the full
13
+ * deterministic export. Throws (CLI exits non-zero) when `sync_export` is
14
+ * not configured, since the whole point of calling this verb is to produce
15
+ * the file; unlike the write-through helper below, silence would be
16
+ * surprising here.
17
+ */
18
+ export declare function runSyncExport(input?: SyncExportCommandInput): SyncExportCommandOutput;
19
+ /**
20
+ * Write-through helper: called after a successful mutation from each of the
21
+ * six mutating commands (log, scan, import, update, rm, file). Config-gated
22
+ * exact no-op when `sync_export` is not configured, so existing users see
23
+ * zero behavior change.
24
+ *
25
+ * Config loading happens INSIDE this function's own try/catch, not at the
26
+ * call site: the friction row is already committed by the time any of the
27
+ * six commands calls this, so a config-load failure (a malformed
28
+ * config.yml, even one with no `sync_export` block at all; or a half-set
29
+ * FRICTION_LOG_SYNC_EXPORT_* env pair) must degrade to the same stderr
30
+ * warning as a write failure, never propagate and crash the command. A
31
+ * crash here previously meant the mutation had already succeeded but the
32
+ * CLI reported failure, inviting a retry that would duplicate the row.
33
+ *
34
+ * Two ways to call it: pass `configPath` (log, scan, import, update, rm,
35
+ * none of which need the config for anything else) or an already-loaded
36
+ * `config` (file.ts, which needs it beforehand anyway to merge sink
37
+ * options, and would otherwise load it twice).
38
+ */
39
+ export declare function maybeSyncExport(params: {
40
+ dbPath: string;
41
+ configPath?: string;
42
+ }): void;
43
+ export declare function maybeSyncExport(params: {
44
+ dbPath: string;
45
+ config: FrictionLogConfig;
46
+ }): void;
@@ -0,0 +1,80 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { loadConfig, } from "../config.js";
4
+ import { FrictionDb } from "../db.js";
5
+ import { defaultDbPath } from "../paths.js";
6
+ import { frictionToExport } from "./export.js";
7
+ /**
8
+ * `friction-log sync-export`: explicit, on-demand write of the full
9
+ * deterministic export. Throws (CLI exits non-zero) when `sync_export` is
10
+ * not configured, since the whole point of calling this verb is to produce
11
+ * the file; unlike the write-through helper below, silence would be
12
+ * surprising here.
13
+ */
14
+ export function runSyncExport(input = {}) {
15
+ const config = loadConfig(input.configPath);
16
+ if (!config.syncExport) {
17
+ throw new Error('friction-log: sync-export requires a "sync_export" block (path + origin) in config.yml, or the ' +
18
+ "FRICTION_LOG_SYNC_EXPORT_PATH / FRICTION_LOG_SYNC_EXPORT_ORIGIN env vars");
19
+ }
20
+ const dbPath = input.dbPath ?? defaultDbPath();
21
+ const count = writeSyncExportFile(dbPath, config.syncExport);
22
+ return {
23
+ path: config.syncExport.path,
24
+ origin: config.syncExport.origin,
25
+ count,
26
+ };
27
+ }
28
+ export function maybeSyncExport(params) {
29
+ const { dbPath } = params;
30
+ try {
31
+ const config = params.config ?? loadConfig(params.configPath);
32
+ if (!config.syncExport)
33
+ return;
34
+ writeSyncExportFile(dbPath, config.syncExport);
35
+ }
36
+ catch (err) {
37
+ process.stderr.write(`friction-log: warning: sync-export write-through failed: ${err.message}\n`);
38
+ }
39
+ }
40
+ function writeSyncExportFile(dbPath, cfg) {
41
+ const db = new FrictionDb(dbPath);
42
+ try {
43
+ const frictions = db.listAllFrictionsForSyncExport();
44
+ const records = frictions.map((f) => ({
45
+ ...frictionToExport(f),
46
+ tags: db.tagsFor(f.id),
47
+ }));
48
+ // Fixed key order ({ origin, records }, ExportRecord fields always built
49
+ // in the same order by frictionToExport) and no exportedAt/timestamp
50
+ // field: two runs with no intervening mutation produce byte-identical
51
+ // JSON, which is exploited immediately below to skip the write (and the
52
+ // mtime bump) entirely when nothing actually changed.
53
+ const payload = { origin: cfg.origin, records };
54
+ const json = JSON.stringify(payload, null, 2) + "\n";
55
+ let existing = null;
56
+ try {
57
+ existing = readFileSync(cfg.path, "utf8");
58
+ }
59
+ catch {
60
+ existing = null;
61
+ }
62
+ if (existing !== json) {
63
+ mkdirSync(dirname(cfg.path), { recursive: true });
64
+ // Atomic write: write to a same-directory tmp file, then rename over
65
+ // the real path, so a reader (or a sync tool) never observes a
66
+ // partially-written file.
67
+ // PID suffix: two concurrent writers sharing one config (Stop-hook
68
+ // scan racing an operator-run command) must not share a temp file,
69
+ // or the rename can publish a mix of two snapshots.
70
+ const tmpPath = `${cfg.path}.${process.pid}.tmp`;
71
+ writeFileSync(tmpPath, json, "utf8");
72
+ renameSync(tmpPath, cfg.path);
73
+ }
74
+ return records.length;
75
+ }
76
+ finally {
77
+ db.close();
78
+ }
79
+ }
80
+ //# sourceMappingURL=sync-export.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-export.js","sourceRoot":"","sources":["../../src/commands/sync-export.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { FrictionStatus } from "../types.js";
2
+ export interface UpdateCommandInput {
3
+ frictionId: number;
4
+ status: FrictionStatus;
5
+ dbPath?: string;
6
+ configPath?: string;
7
+ }
8
+ export interface UpdateCommandOutput {
9
+ id: number;
10
+ status: FrictionStatus;
11
+ }
12
+ export declare function runUpdate(input: UpdateCommandInput): UpdateCommandOutput;
@@ -0,0 +1,24 @@
1
+ import { FrictionDb } from "../db.js";
2
+ import { defaultDbPath } from "../paths.js";
3
+ import { maybeSyncExport } from "./sync-export.js";
4
+ export function runUpdate(input) {
5
+ const dbPath = input.dbPath ?? defaultDbPath();
6
+ const db = new FrictionDb(dbPath);
7
+ try {
8
+ const friction = db.getFriction(input.frictionId);
9
+ if (!friction) {
10
+ throw new Error(`friction-log: friction id=${input.frictionId} not found`);
11
+ }
12
+ db.updateFrictionStatus(input.frictionId, input.status);
13
+ // Skip write-through when the status did not actually change (the
14
+ // export payload's status field would be identical either way).
15
+ if (friction.status !== input.status) {
16
+ maybeSyncExport({ dbPath, configPath: input.configPath });
17
+ }
18
+ return { id: input.frictionId, status: input.status };
19
+ }
20
+ finally {
21
+ db.close();
22
+ }
23
+ }
24
+ //# sourceMappingURL=update.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update.js","sourceRoot":"","sources":["../../src/commands/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAenD,MAAM,UAAU,SAAS,CAAC,KAAyB;IACjD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,aAAa,EAAE,CAAC;IAC/C,MAAM,EAAE,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAClD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CACb,6BAA6B,KAAK,CAAC,UAAU,YAAY,CAC1D,CAAC;QACJ,CAAC;QACD,EAAE,CAAC,oBAAoB,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACxD,kEAAkE;QAClE,gEAAgE;QAChE,IAAI,QAAQ,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;YACrC,eAAe,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;QAC5D,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;IACxD,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}
@@ -0,0 +1,31 @@
1
+ export type SinkConfig = Record<string, unknown>;
2
+ /**
3
+ * Config-gated write-through + read-only peer merge. Absent this block,
4
+ * `sync-export` and the per-mutation write-through are exact no-ops (see
5
+ * commands/sync-export.ts). `peerPaths` are files written by OTHER machines'
6
+ * own `sync_export.path` (or write-through); `digest --include-peers` reads
7
+ * them read-only. Transport between machines (Dropbox, iCloud,
8
+ * agent-memory-sync, ...) is out of scope for this package.
9
+ */
10
+ export interface SyncExportConfig {
11
+ path: string;
12
+ origin: string;
13
+ peerPaths: string[];
14
+ }
15
+ export interface FrictionLogConfig {
16
+ sinks: Record<string, SinkConfig>;
17
+ syncExport?: SyncExportConfig;
18
+ }
19
+ export declare function loadConfig(path?: string): FrictionLogConfig;
20
+ /**
21
+ * Merge per-sink config with CLI overrides. CLI wins on key collision so a
22
+ * one-off `--sink-opt repo=other/repo` overrides the config's default repo.
23
+ */
24
+ export declare function mergeSinkOpts(configSection: SinkConfig | undefined, cliOverrides: SinkConfig | undefined): SinkConfig;
25
+ /**
26
+ * Parse repeated `--sink-opt key=value` CLI flags into a SinkConfig.
27
+ * Values are coerced from string with cheap heuristics: comma-separated lists
28
+ * become arrays, "true"/"false" become booleans, integers become numbers. To
29
+ * keep a literal that looks like a list/number, prefix with `s:`.
30
+ */
31
+ export declare function parseSinkOpts(pairs: string[]): SinkConfig;
package/dist/config.js ADDED
@@ -0,0 +1,145 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parse as parseYaml } from "yaml";
3
+ import { defaultConfigPath, syncExportOriginOverride, syncExportPathOverride, } from "./paths.js";
4
+ const EMPTY_CONFIG = { sinks: {} };
5
+ export function loadConfig(path = defaultConfigPath()) {
6
+ let raw;
7
+ try {
8
+ raw = readFileSync(path, "utf8");
9
+ }
10
+ catch (err) {
11
+ // ENOENT is the common case: the user has not created a config file. Any
12
+ // other error (permission, bad symlink) is worth surfacing so we don't
13
+ // silently fall back to defaults when the user thinks they have a config.
14
+ if (err.code === "ENOENT") {
15
+ return applySyncExportEnvOverrides(EMPTY_CONFIG);
16
+ }
17
+ throw new Error(`friction-log: failed to read config file ${path}: ${err.message}`);
18
+ }
19
+ let parsed;
20
+ try {
21
+ parsed = parseYaml(raw);
22
+ }
23
+ catch (err) {
24
+ throw new Error(`friction-log: failed to parse YAML in ${path}: ${err.message}`);
25
+ }
26
+ if (parsed == null)
27
+ return applySyncExportEnvOverrides(EMPTY_CONFIG);
28
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
29
+ throw new Error(`friction-log: config at ${path} must be a YAML mapping at the top level`);
30
+ }
31
+ const root = parsed;
32
+ const sinksRaw = root.sinks;
33
+ const sinks = {};
34
+ if (sinksRaw != null) {
35
+ if (typeof sinksRaw !== "object" || Array.isArray(sinksRaw)) {
36
+ throw new Error(`friction-log: config at ${path} has a non-mapping "sinks" value`);
37
+ }
38
+ for (const [name, value] of Object.entries(sinksRaw)) {
39
+ if (value == null)
40
+ continue;
41
+ if (typeof value !== "object" || Array.isArray(value)) {
42
+ throw new Error(`friction-log: config sinks.${name} must be a YAML mapping`);
43
+ }
44
+ sinks[name] = value;
45
+ }
46
+ }
47
+ const syncExport = parseSyncExportBlock(root.sync_export, path);
48
+ return applySyncExportEnvOverrides({ sinks, syncExport });
49
+ }
50
+ function parseSyncExportBlock(raw, configPath) {
51
+ if (raw == null)
52
+ return undefined;
53
+ if (typeof raw !== "object" || Array.isArray(raw)) {
54
+ throw new Error(`friction-log: config at ${configPath} has a non-mapping "sync_export" value`);
55
+ }
56
+ const obj = raw;
57
+ const rawPath = obj.path;
58
+ const rawOrigin = obj.origin;
59
+ const rawPeers = obj.peer_paths;
60
+ if (typeof rawPath !== "string" || !rawPath.trim()) {
61
+ throw new Error(`friction-log: config sync_export.path at ${configPath} must be a non-empty string`);
62
+ }
63
+ if (typeof rawOrigin !== "string" || !rawOrigin.trim()) {
64
+ throw new Error(`friction-log: config sync_export.origin at ${configPath} must be a non-empty string`);
65
+ }
66
+ let peerPaths = [];
67
+ if (rawPeers != null) {
68
+ if (!Array.isArray(rawPeers) ||
69
+ !rawPeers.every((p) => typeof p === "string")) {
70
+ throw new Error(`friction-log: config sync_export.peer_paths at ${configPath} must be a list of strings`);
71
+ }
72
+ peerPaths = rawPeers;
73
+ }
74
+ return { path: rawPath, origin: rawOrigin, peerPaths };
75
+ }
76
+ /**
77
+ * FRICTION_LOG_SYNC_EXPORT_PATH / FRICTION_LOG_SYNC_EXPORT_ORIGIN win over
78
+ * the YAML `sync_export` block when set (paths.ts env-var convention), so a
79
+ * config.yml can be shared across machines (e.g. via dotfiles) while each
80
+ * machine supplies its own path/origin via env. Either var alone, with no
81
+ * counterpart resolvable from config or env, is a config error rather than
82
+ * a silent partial activation.
83
+ */
84
+ function applySyncExportEnvOverrides(config) {
85
+ const envPath = syncExportPathOverride();
86
+ const envOrigin = syncExportOriginOverride();
87
+ if (envPath === undefined && envOrigin === undefined)
88
+ return config;
89
+ const path = envPath ?? config.syncExport?.path;
90
+ const origin = envOrigin ?? config.syncExport?.origin;
91
+ if (!path || !origin) {
92
+ throw new Error("friction-log: FRICTION_LOG_SYNC_EXPORT_PATH / FRICTION_LOG_SYNC_EXPORT_ORIGIN require both a path and an origin " +
93
+ "(from the env vars or the config sync_export block) to activate sync-export");
94
+ }
95
+ return {
96
+ ...config,
97
+ syncExport: { path, origin, peerPaths: config.syncExport?.peerPaths ?? [] },
98
+ };
99
+ }
100
+ /**
101
+ * Merge per-sink config with CLI overrides. CLI wins on key collision so a
102
+ * one-off `--sink-opt repo=other/repo` overrides the config's default repo.
103
+ */
104
+ export function mergeSinkOpts(configSection, cliOverrides) {
105
+ return { ...(configSection ?? {}), ...(cliOverrides ?? {}) };
106
+ }
107
+ const KV_PATTERN = /^([A-Za-z0-9_.-]+)=(.*)$/s;
108
+ /**
109
+ * Parse repeated `--sink-opt key=value` CLI flags into a SinkConfig.
110
+ * Values are coerced from string with cheap heuristics: comma-separated lists
111
+ * become arrays, "true"/"false" become booleans, integers become numbers. To
112
+ * keep a literal that looks like a list/number, prefix with `s:`.
113
+ */
114
+ export function parseSinkOpts(pairs) {
115
+ const out = {};
116
+ for (const pair of pairs) {
117
+ const m = KV_PATTERN.exec(pair);
118
+ if (!m) {
119
+ throw new Error(`friction-log: --sink-opt expected key=value, got "${pair}"`);
120
+ }
121
+ const key = m[1];
122
+ const rawValue = m[2];
123
+ out[key] = coerceValue(rawValue);
124
+ }
125
+ return out;
126
+ }
127
+ function coerceValue(raw) {
128
+ if (raw.startsWith("s:"))
129
+ return raw.slice(2);
130
+ if (raw === "true")
131
+ return true;
132
+ if (raw === "false")
133
+ return false;
134
+ if (raw === "null")
135
+ return null;
136
+ if (/^-?\d+$/.test(raw))
137
+ return Number(raw);
138
+ if (raw.includes(","))
139
+ return raw
140
+ .split(",")
141
+ .map((s) => s.trim())
142
+ .filter(Boolean);
143
+ return raw;
144
+ }
145
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/db.d.ts ADDED
@@ -0,0 +1,81 @@
1
+ import type { Friction, FrictionSource, FrictionStatus, Severity, Session, Task } from "./types.js";
2
+ export interface InsertFrictionInput {
3
+ sessionId?: string | null;
4
+ toolSurface?: string | null;
5
+ title: string;
6
+ description?: string | null;
7
+ capturedAt?: string;
8
+ severity?: Severity | null;
9
+ category?: string | null;
10
+ source: FrictionSource;
11
+ recurrenceOfId?: number | null;
12
+ }
13
+ export interface ListFrictionsFilter {
14
+ status?: FrictionStatus;
15
+ tool?: string;
16
+ category?: string;
17
+ source?: FrictionSource;
18
+ sinceIso?: string;
19
+ limit?: number;
20
+ }
21
+ export type DigestGroupBy = "tool" | "category" | "severity" | "source";
22
+ export interface DigestRow {
23
+ group: string;
24
+ total: number;
25
+ open: number;
26
+ filed: number;
27
+ resolved: number;
28
+ wontfix: number;
29
+ recurrences: number;
30
+ avgHoursToTriage: number | null;
31
+ }
32
+ export interface InsertTaskInput {
33
+ frictionId: number;
34
+ sinkName: string;
35
+ sinkTarget?: string | null;
36
+ externalRef?: string | null;
37
+ prUrl?: string | null;
38
+ resolutionStatus?: string | null;
39
+ }
40
+ export declare class FrictionDb {
41
+ private readonly db;
42
+ constructor(path: string);
43
+ private migrate;
44
+ schemaVersion(): number;
45
+ upsertSession(s: Session): void;
46
+ /**
47
+ * Create a minimal sessions row for `id` if none exists yet. Used by
48
+ * the `log` command path so `friction-log log --session <fresh-id>`
49
+ * can attribute a friction to a live runtime session without a prior
50
+ * `friction-log import`. Defaults: started_at = now, adapter =
51
+ * 'manual' (mirrors `frictions.source = 'manual'` for log-path rows).
52
+ * Idempotent: a subsequent `upsertSession` from an `import` or `scan`
53
+ * later overwrites the placeholder with full metadata.
54
+ */
55
+ ensureSession(id: string): void;
56
+ getSession(id: string): Session | null;
57
+ insertFriction(input: InsertFrictionInput): Friction;
58
+ findRecurrenceRoot(toolSurface: string | null, title: string): number | null;
59
+ getFriction(id: number): Friction | null;
60
+ updateFrictionStatus(id: number, status: FrictionStatus): void;
61
+ deleteFriction(id: number): boolean;
62
+ findFrictionByTriple(sessionId: string | null, toolSurface: string | null, title: string): Friction | null;
63
+ listFrictionsForSession(sessionId: string): Friction[];
64
+ getMostRecentSession(): Session | null;
65
+ listFrictions(filter?: ListFrictionsFilter): Friction[];
66
+ /**
67
+ * Every friction, no limit, sorted oldest-first with an `id` tiebreaker.
68
+ * Dedicated to sync-export: `listFrictions` caps at 10_000 and sorts
69
+ * newest-first for interactive use, neither of which is right for a full,
70
+ * deterministic machine-readable dump.
71
+ */
72
+ listAllFrictionsForSyncExport(): Friction[];
73
+ insertTask(input: InsertTaskInput): Task;
74
+ getTask(id: number): Task | null;
75
+ listTasksForFriction(frictionId: number): Task[];
76
+ addTag(frictionId: number, tag: string): void;
77
+ tagsFor(frictionId: number): string[];
78
+ searchFrictions(query: string, filter?: ListFrictionsFilter): Friction[];
79
+ digest(groupBy: DigestGroupBy, sinceIso?: string): DigestRow[];
80
+ close(): void;
81
+ }