cloving 1.0.1 → 1.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.
Files changed (177) hide show
  1. package/README.md +145 -14
  2. package/dist/src/authority/commands.js +10 -2
  3. package/dist/src/authority/reconciliation.d.ts +2 -0
  4. package/dist/src/authority/reconciliation.js +10 -1
  5. package/dist/src/authority/transitions.d.ts +1 -1
  6. package/dist/src/authority/transitions.js +2 -0
  7. package/dist/src/broker/actor/authority-session.d.ts +2 -0
  8. package/dist/src/broker/actor/authority-session.js +3 -4
  9. package/dist/src/chats/analysis-batches.d.ts +24 -0
  10. package/dist/src/chats/analysis-batches.js +83 -0
  11. package/dist/src/chats/analysis-contract.d.ts +80 -0
  12. package/dist/src/chats/analysis-contract.js +16 -0
  13. package/dist/src/chats/analysis-data.d.ts +3 -0
  14. package/dist/src/chats/analysis-data.js +85 -0
  15. package/dist/src/chats/analysis-input.d.ts +12 -0
  16. package/dist/src/chats/analysis-input.js +83 -0
  17. package/dist/src/chats/analysis-links.d.ts +2 -0
  18. package/dist/src/chats/analysis-links.js +25 -0
  19. package/dist/src/chats/analysis-prompt.d.ts +10 -0
  20. package/dist/src/chats/analysis-prompt.js +16 -0
  21. package/dist/src/chats/analysis-proposal.d.ts +2 -0
  22. package/dist/src/chats/analysis-proposal.js +32 -0
  23. package/dist/src/chats/analysis-provenance.d.ts +4 -0
  24. package/dist/src/chats/analysis-provenance.js +99 -0
  25. package/dist/src/chats/analysis-shape.d.ts +3 -0
  26. package/dist/src/chats/analysis-shape.js +228 -0
  27. package/dist/src/chats/analysis.d.ts +3 -0
  28. package/dist/src/chats/analysis.js +42 -0
  29. package/dist/src/chats/corpus-input.d.ts +5 -0
  30. package/dist/src/chats/corpus-input.js +288 -0
  31. package/dist/src/chats/destination.js +3 -1
  32. package/dist/src/chats/import.d.ts +8 -6
  33. package/dist/src/chats/import.js +195 -23
  34. package/dist/src/chats/input-file.d.ts +3 -0
  35. package/dist/src/chats/input-file.js +32 -0
  36. package/dist/src/chats/normalize.d.ts +11 -2
  37. package/dist/src/chats/normalize.js +18 -2
  38. package/dist/src/chats/previous-import.d.ts +5 -0
  39. package/dist/src/chats/previous-import.js +34 -0
  40. package/dist/src/chats/reader.d.ts +7 -1
  41. package/dist/src/chats/reader.js +46 -15
  42. package/dist/src/chats/redact-import.d.ts +14 -0
  43. package/dist/src/chats/redact-import.js +141 -0
  44. package/dist/src/chats/redaction.d.ts +41 -0
  45. package/dist/src/chats/redaction.js +161 -0
  46. package/dist/src/chats/revision-input.d.ts +3 -0
  47. package/dist/src/chats/revision-input.js +180 -0
  48. package/dist/src/chats/revision.d.ts +48 -0
  49. package/dist/src/chats/revision.js +94 -0
  50. package/dist/src/chats/selection.d.ts +25 -0
  51. package/dist/src/chats/selection.js +109 -0
  52. package/dist/src/chats/source.d.ts +23 -0
  53. package/dist/src/chats/source.js +135 -0
  54. package/dist/src/chats/structure.d.ts +57 -0
  55. package/dist/src/chats/structure.js +216 -0
  56. package/dist/src/cli/arguments.d.ts +8 -0
  57. package/dist/src/cli/arguments.js +94 -13
  58. package/dist/src/cli/client.d.ts +1 -0
  59. package/dist/src/cli/client.js +38 -1
  60. package/dist/src/cli/commands.d.ts +3 -0
  61. package/dist/src/cli/commands.js +29 -11
  62. package/dist/src/cli/main.d.ts +2 -1
  63. package/dist/src/cli/main.js +61 -14
  64. package/dist/src/cli/render.d.ts +2 -0
  65. package/dist/src/cli/render.js +70 -2
  66. package/dist/src/cli/usage-render.d.ts +3 -0
  67. package/dist/src/cli/usage-render.js +28 -0
  68. package/dist/src/cli/usage.d.ts +3 -2
  69. package/dist/src/cli/usage.js +38 -6
  70. package/dist/src/cli/watch-actions.d.ts +10 -0
  71. package/dist/src/cli/watch-actions.js +58 -0
  72. package/dist/src/cli/watch-board-view.d.ts +32 -0
  73. package/dist/src/cli/watch-board-view.js +88 -15
  74. package/dist/src/cli/watch-board.js +6 -1
  75. package/dist/src/cli/watch-card-client.d.ts +1 -0
  76. package/dist/src/cli/watch-card-client.js +44 -0
  77. package/dist/src/cli/watch-card-forms.d.ts +2 -0
  78. package/dist/src/cli/watch-card-forms.js +33 -0
  79. package/dist/src/cli/watch-card-reply.d.ts +5 -0
  80. package/dist/src/cli/watch-card-reply.js +34 -0
  81. package/dist/src/cli/watch-card-view.d.ts +40 -0
  82. package/dist/src/cli/watch-card-view.js +86 -0
  83. package/dist/src/cli/watch-client.d.ts +1 -0
  84. package/dist/src/cli/watch-client.js +118 -0
  85. package/dist/src/cli/watch-model.js +31 -14
  86. package/dist/src/cli/watch-page.d.ts +1 -1
  87. package/dist/src/cli/watch-page.js +71 -85
  88. package/dist/src/cli/watch-pr-board.d.ts +3 -0
  89. package/dist/src/cli/watch-pr-board.js +26 -0
  90. package/dist/src/cli/watch-pr-command.d.ts +15 -0
  91. package/dist/src/cli/watch-pr-command.js +37 -0
  92. package/dist/src/cli/watch-pr-identity.d.ts +20 -0
  93. package/dist/src/cli/watch-pr-identity.js +62 -0
  94. package/dist/src/cli/watch-pr-observer.d.ts +19 -0
  95. package/dist/src/cli/watch-pr-observer.js +108 -0
  96. package/dist/src/cli/watch-pr-settings.d.ts +8 -0
  97. package/dist/src/cli/watch-pr-settings.js +47 -0
  98. package/dist/src/cli/watch-reads.js +4 -0
  99. package/dist/src/cli/watch-render.d.ts +16 -2
  100. package/dist/src/cli/watch-render.js +10 -3
  101. package/dist/src/cli/watch-run-input.d.ts +14 -0
  102. package/dist/src/cli/watch-run-input.js +49 -0
  103. package/dist/src/cli/watch-server.d.ts +3 -0
  104. package/dist/src/cli/watch-server.js +44 -6
  105. package/dist/src/cli/watch-start-view.d.ts +18 -0
  106. package/dist/src/cli/watch-start-view.js +77 -0
  107. package/dist/src/cli/watch-turn.d.ts +1 -0
  108. package/dist/src/cli/watch-turn.js +7 -2
  109. package/dist/src/cli/watch-usage.d.ts +6 -0
  110. package/dist/src/cli/watch-usage.js +65 -0
  111. package/dist/src/cli/watch-view.d.ts +15 -0
  112. package/dist/src/cli/watch-view.js +61 -17
  113. package/dist/src/cli.js +48 -6
  114. package/dist/src/daemon/analysis-launch.d.ts +5 -0
  115. package/dist/src/daemon/analysis-launch.js +81 -0
  116. package/dist/src/daemon/analysis-token.d.ts +6 -0
  117. package/dist/src/daemon/analysis-token.js +88 -0
  118. package/dist/src/daemon/analysis-worker.d.ts +22 -0
  119. package/dist/src/daemon/analysis-worker.js +219 -0
  120. package/dist/src/daemon/chats-files.d.ts +15 -0
  121. package/dist/src/daemon/chats-files.js +113 -0
  122. package/dist/src/daemon/chats-worker.d.ts +12 -0
  123. package/dist/src/daemon/chats-worker.js +200 -0
  124. package/dist/src/daemon/claude-turn.d.ts +10 -0
  125. package/dist/src/daemon/claude-turn.js +96 -15
  126. package/dist/src/daemon/claude-worker.d.ts +4 -1
  127. package/dist/src/daemon/claude-worker.js +99 -47
  128. package/dist/src/daemon/codex-stream.js +16 -0
  129. package/dist/src/daemon/daemon.d.ts +3 -0
  130. package/dist/src/daemon/daemon.js +16 -7
  131. package/dist/src/daemon/github-methods.js +4 -0
  132. package/dist/src/daemon/github-ticket-status.d.ts +3 -0
  133. package/dist/src/daemon/github-ticket-status.js +268 -0
  134. package/dist/src/daemon/issue-queue.d.ts +1 -0
  135. package/dist/src/daemon/issue-queue.js +115 -24
  136. package/dist/src/daemon/operation-journal.d.ts +1 -0
  137. package/dist/src/daemon/operation-journal.js +4 -2
  138. package/dist/src/daemon/retrying-turn.d.ts +9 -0
  139. package/dist/src/daemon/retrying-turn.js +185 -0
  140. package/dist/src/host/world-reconciliation.d.ts +2 -0
  141. package/dist/src/host/world-reconciliation.js +6 -0
  142. package/dist/src/host/world-runs.js +9 -1
  143. package/dist/src/service/run-handlers.js +15 -23
  144. package/dist/src/service/worker-totals.d.ts +3 -0
  145. package/dist/src/service/worker-totals.js +28 -0
  146. package/dist/src/ticket-status.d.ts +8 -0
  147. package/dist/src/ticket-status.js +45 -0
  148. package/dist/src/token-prices.d.ts +19 -0
  149. package/dist/src/token-prices.js +76 -0
  150. package/dist/src/worker-usage.d.ts +22 -0
  151. package/dist/src/worker-usage.js +44 -0
  152. package/dist/src/workflow-candidate.d.ts +29 -0
  153. package/dist/src/workflow-candidate.js +59 -0
  154. package/dist/src/workflow-contracts.d.ts +2 -0
  155. package/dist/src/workflow-contracts.js +27 -5
  156. package/dist/src/workflow-declaration.d.ts +1 -1
  157. package/dist/src/workflow-declaration.js +3 -1
  158. package/dist/src/workflows.d.ts +1 -0
  159. package/dist/src/workflows.js +1 -0
  160. package/dist/tools/self-host-processes.js +7 -2
  161. package/dist/tools/self-host-workflows.d.ts +1 -1
  162. package/dist/tools/self-host-workflows.js +2 -2
  163. package/dist/tools/self-host.js +6 -6
  164. package/dist/tools/self-update-host.d.ts +7 -0
  165. package/dist/tools/self-update-host.js +48 -1
  166. package/dist/tools/self-update.d.ts +1 -0
  167. package/dist/tools/self-update.js +85 -13
  168. package/dist/workflows/analyze-chats.d.ts +10 -0
  169. package/dist/workflows/analyze-chats.declaration.json +9 -0
  170. package/dist/workflows/analyze-chats.js +73 -0
  171. package/dist/workflows/develop-ticket.d.ts +1 -7
  172. package/dist/workflows/develop-ticket.js +42 -141
  173. package/dist/workflows/issue-labels.d.ts +3 -1
  174. package/dist/workflows/issue-labels.js +34 -0
  175. package/dist/workflows/ticket-prompts.d.ts +35 -0
  176. package/dist/workflows/ticket-prompts.js +133 -0
  177. package/package.json +5 -3
@@ -0,0 +1,41 @@
1
+ import type { MessageRecord } from "./normalize.ts";
2
+ export interface RedactionSettings {
3
+ readonly home: string | null;
4
+ readonly privateTexts: readonly string[];
5
+ }
6
+ export declare const replacements: {
7
+ readonly privateKey: "[PRIVATE_KEY]";
8
+ readonly credentialUrl: "[CREDENTIAL_URL]";
9
+ readonly homePath: "[HOME_PATH]";
10
+ readonly token: "[TOKEN]";
11
+ readonly email: "[EMAIL]";
12
+ readonly privateText: "[PRIVATE_TEXT]";
13
+ };
14
+ export declare const categories: readonly ["privateKey", "credentialUrl", "homePath", "token", "email", "privateText"];
15
+ export type Category = keyof typeof replacements;
16
+ export type RedactionCounts = Record<Category, number>;
17
+ export declare function emptyCounts(): RedactionCounts;
18
+ export declare function redactRecord(record: MessageRecord, settings: RedactionSettings): {
19
+ record: {
20
+ text: string;
21
+ blocks: Record<string, unknown>[];
22
+ sourceId: string;
23
+ role: "user" | "assistant";
24
+ timestamp: string | null;
25
+ sessionId: string | null;
26
+ schemaVersion: 1;
27
+ } | {
28
+ text: string;
29
+ blocks: Record<string, unknown>[];
30
+ sourceId: string;
31
+ role: "user" | "assistant";
32
+ timestamp: string | null;
33
+ sessionId: string | null;
34
+ schemaVersion: 2;
35
+ uuid: string | null;
36
+ isSidechain: boolean | null;
37
+ parentUuid?: string | null;
38
+ };
39
+ counts: RedactionCounts;
40
+ totalCount: number;
41
+ };
@@ -0,0 +1,161 @@
1
+ export const replacements = {
2
+ privateKey: "[PRIVATE_KEY]",
3
+ credentialUrl: "[CREDENTIAL_URL]",
4
+ homePath: "[HOME_PATH]",
5
+ token: "[TOKEN]",
6
+ email: "[EMAIL]",
7
+ privateText: "[PRIVATE_TEXT]",
8
+ };
9
+ export const categories = [
10
+ "privateKey",
11
+ "credentialUrl",
12
+ "homePath",
13
+ "token",
14
+ "email",
15
+ "privateText",
16
+ ];
17
+ export function emptyCounts() {
18
+ return {
19
+ privateKey: 0,
20
+ credentialUrl: 0,
21
+ homePath: 0,
22
+ token: 0,
23
+ email: 0,
24
+ privateText: 0,
25
+ };
26
+ }
27
+ function escaped(text) {
28
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29
+ }
30
+ function patternSpans(text, rule) {
31
+ return [...text.matchAll(rule.pattern)].map((match) => ({
32
+ start: match.index,
33
+ end: match.index + match[0].length,
34
+ category: rule.category,
35
+ }));
36
+ }
37
+ function homeRoot(home) {
38
+ if (home === null || home === "")
39
+ return "~";
40
+ if (home === "/")
41
+ return "(?:(?=/)|~)";
42
+ return `(?:${escaped(home.replace(/\/$/, ""))}|~)`;
43
+ }
44
+ function homeSpans(text, home) {
45
+ const root = homeRoot(home);
46
+ const pattern = new RegExp(String.raw `(?<![\p{L}\p{N}_./~\\-])${root}(?=$|[\s/\x22'\x60<>,;:!?()\[\]{}])(?:/[^\s\x22'\x60<>]*)?`, "gu");
47
+ return patternSpans(text, { pattern, category: "homePath" }).map((span) => {
48
+ const quote = text[span.start - 1];
49
+ if (quote !== '"' && quote !== "'" && quote !== "`")
50
+ return span;
51
+ const end = text.indexOf(quote, span.end);
52
+ return end === -1 ? span : { ...span, end };
53
+ });
54
+ }
55
+ function spans(text, settings) {
56
+ const rules = [
57
+ {
58
+ pattern: /-----BEGIN ((?:[A-Z0-9]+ )*PRIVATE KEY)-----[\s\S]*?-----END \1-----/g,
59
+ category: "privateKey",
60
+ },
61
+ {
62
+ pattern: /\b[a-z][a-z\d+.-]*:\/\/[^\s<>"'`/?#]*@[^\s<>"'`]+/gi,
63
+ category: "credentialUrl",
64
+ },
65
+ {
66
+ pattern: /(?<![\p{L}\p{N}_])(?:gh[pousr]_|github_pat_|sk-ant-|sk-(?:proj-|svcacct-)?)[a-z\d_-]+/giu,
67
+ category: "token",
68
+ },
69
+ {
70
+ pattern: /[\w.!#$%&'*+/=?^`{|}~-]+@[\w.-]+\.[a-z]{2,}/gi,
71
+ category: "email",
72
+ },
73
+ ];
74
+ const matches = rules.flatMap((rule) => patternSpans(text, rule));
75
+ for (const match of homeSpans(text, settings.home))
76
+ matches.push(match);
77
+ for (const privateText of settings.privateTexts) {
78
+ if (privateText.trim() === "")
79
+ throw new Error("invalid-settings");
80
+ const pattern = new RegExp(`(?=(${escaped(privateText)}))`, "gi");
81
+ for (const match of text.matchAll(pattern))
82
+ matches.push({
83
+ start: match.index,
84
+ end: match.index + String(match[1]).length,
85
+ category: "privateText",
86
+ });
87
+ }
88
+ return mergeSpans(matches);
89
+ }
90
+ function mergeSpans(matches) {
91
+ const priority = categories;
92
+ const merged = [];
93
+ for (const match of matches.sort((left, right) => left.start - right.start)) {
94
+ const previous = merged.at(-1);
95
+ if (previous === undefined || previous.end <= match.start) {
96
+ merged.push({ ...match });
97
+ continue;
98
+ }
99
+ previous.end = Math.max(previous.end, match.end);
100
+ if (priority.indexOf(match.category) < priority.indexOf(previous.category))
101
+ previous.category = match.category;
102
+ }
103
+ return merged;
104
+ }
105
+ function redactText(text, context) {
106
+ const { settings, counts } = context;
107
+ let result = "";
108
+ let cursor = 0;
109
+ for (const span of spans(text, settings)) {
110
+ result += text.slice(cursor, span.start) + replacements[span.category];
111
+ cursor = span.end;
112
+ counts[span.category]++;
113
+ }
114
+ return result + text.slice(cursor);
115
+ }
116
+ function rawJsonApi(value) {
117
+ return "isRawJSON" in value && typeof value.isRawJSON === "function";
118
+ }
119
+ function rawJson(value) {
120
+ return rawJsonApi(JSON) && JSON.isRawJSON(value);
121
+ }
122
+ function nested(value, context) {
123
+ if (typeof value === "string")
124
+ return redactText(value, context);
125
+ if (rawJson(value))
126
+ return value;
127
+ if (Array.isArray(value))
128
+ return value.map((item) => nested(item, context));
129
+ if (value === null || typeof value !== "object")
130
+ return value;
131
+ const result = {};
132
+ for (const [key, item] of Object.entries(value)) {
133
+ const redactedKey = redactText(key, context);
134
+ if (Object.hasOwn(result, redactedKey))
135
+ throw new Error("member-name-collision");
136
+ Object.defineProperty(result, redactedKey, {
137
+ value: nested(item, context),
138
+ enumerable: true,
139
+ });
140
+ }
141
+ return result;
142
+ }
143
+ export function redactRecord(record, settings) {
144
+ const counts = emptyCounts();
145
+ const context = { settings, counts };
146
+ const blocks = record.blocks.map((block) => {
147
+ if (block["type"] === "tool_use")
148
+ return { ...block, input: nested(block["input"], context) };
149
+ if (block["type"] === "tool_result")
150
+ return { ...block, content: nested(block["content"], context) };
151
+ if (block["type"] === "text" && typeof block["text"] === "string")
152
+ return { ...block, text: redactText(block["text"], context) };
153
+ return block;
154
+ });
155
+ const text = redactText(record.text, context);
156
+ return {
157
+ record: { ...record, text, blocks },
158
+ counts,
159
+ totalCount: Object.values(counts).reduce((sum, count) => sum + count, 0),
160
+ };
161
+ }
@@ -0,0 +1,3 @@
1
+ import { type EvidenceSource } from "./revision.ts";
2
+ export declare function parseEvidenceCoverage(text: string, importId: string): readonly EvidenceSource[];
3
+ export declare function nextEvidenceRevision(text: string | undefined, importId: string): number;
@@ -0,0 +1,180 @@
1
+ import { validateCoverage, withinRoot } from "./corpus-input.js";
2
+ import { evidenceReason, revisionReasons, } from "./revision.js";
3
+ function object(value) {
4
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5
+ }
6
+ function exact(value, fields) {
7
+ const { required, optional = [] } = fields;
8
+ return (required.every((key) => Object.hasOwn(value, key)) &&
9
+ Object.keys(value).every((key) => required.includes(key) || optional.includes(key)));
10
+ }
11
+ function natural(value) {
12
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
13
+ }
14
+ function absolute(value) {
15
+ return (typeof value === "string" &&
16
+ value.startsWith("/") &&
17
+ !value.includes("\0") &&
18
+ value.split("/").every((part) => part !== "." && part !== ".."));
19
+ }
20
+ function digest(value) {
21
+ return (value === null ||
22
+ (typeof value === "string" && /^sha256:[a-f\d]{64}$/.test(value)));
23
+ }
24
+ function evidenceValue(value) {
25
+ return (object(value) &&
26
+ digest(value["digest"]) &&
27
+ (!Object.hasOwn(value, "byteSize") ||
28
+ (natural(value["byteSize"]) && value["byteSize"] <= 10_485_760)));
29
+ }
30
+ function source(value, roots) {
31
+ if (!evidenceValue(value) ||
32
+ !absolute(value["path"]) ||
33
+ typeof value["status"] !== "string")
34
+ throw new Error("invalid-import");
35
+ return {
36
+ path: value["path"],
37
+ root: selectedRoot(value["selection"], roots),
38
+ status: value["status"],
39
+ digest: value.digest,
40
+ ...(value.byteSize === undefined ? {} : { byteSize: value.byteSize }),
41
+ };
42
+ }
43
+ export function parseEvidenceCoverage(text, importId) {
44
+ validateCoverage(text, importId);
45
+ const value = JSON.parse(text);
46
+ if (!object(value))
47
+ throw new Error("invalid-import");
48
+ const entries = value["schemaVersion"] === 1 ? value["files"] : value["entries"];
49
+ const roots = value["schemaVersion"] === 1 ? [] : value["roots"];
50
+ if (!Array.isArray(entries) ||
51
+ !Array.isArray(roots) ||
52
+ !roots.every((root) => typeof root === "string"))
53
+ throw new Error("invalid-import");
54
+ return entries.map((entry) => source(entry, roots));
55
+ }
56
+ function revisionValue(value) {
57
+ return (value === null ||
58
+ (evidenceValue(value) &&
59
+ exact(value, { required: ["digest"], optional: ["byteSize"] })));
60
+ }
61
+ function revisionEntry(value) {
62
+ if (!object(value) ||
63
+ !exact(value, {
64
+ required: ["path", "root", "before", "after", "status", "reason"],
65
+ }))
66
+ return false;
67
+ if (!selectionKey(value))
68
+ return false;
69
+ const reason = value["reason"];
70
+ if (typeof reason !== "string" || !Object.hasOwn(revisionReasons, reason))
71
+ return false;
72
+ const expected = Object.entries(revisionReasons).find(([key]) => key === reason)?.[1];
73
+ return (value["status"] === expected &&
74
+ revisionPresence(value) &&
75
+ consistentReason(value));
76
+ }
77
+ function revisionPresence(value) {
78
+ const reason = value["reason"];
79
+ if (reason === "no-previous-entry")
80
+ return value["before"] === null && value["after"] !== null;
81
+ if (reason === "not-selected")
82
+ return value["before"] !== null && value["after"] === null;
83
+ return value["before"] !== null && value["after"] !== null;
84
+ }
85
+ function validCounts(value, entries) {
86
+ const statuses = [
87
+ "new",
88
+ "missing",
89
+ "unchanged",
90
+ "changed",
91
+ "truncated",
92
+ "extended",
93
+ ];
94
+ return (object(value) &&
95
+ exact(value, { required: statuses }) &&
96
+ statuses.every((status) => natural(value[status]) &&
97
+ value[status] ===
98
+ entries.filter((entry) => entry["status"] === status).length));
99
+ }
100
+ export function nextEvidenceRevision(text, importId) {
101
+ if (text === undefined)
102
+ return 1;
103
+ const value = JSON.parse(text);
104
+ if (!object(value) ||
105
+ !exact(value, {
106
+ required: [
107
+ "schemaVersion",
108
+ "importId",
109
+ "previousImportId",
110
+ "evidenceRevision",
111
+ "counts",
112
+ "entries",
113
+ ],
114
+ }))
115
+ throw new Error("invalid-import");
116
+ if (!revisionHeader(value, importId))
117
+ throw new Error("invalid-import");
118
+ const entries = value["entries"];
119
+ if (!Array.isArray(entries) ||
120
+ !entries.every(revisionEntry) ||
121
+ !validCounts(value["counts"], entries))
122
+ throw new Error("invalid-import");
123
+ const revision = value["evidenceRevision"];
124
+ if (!natural(revision))
125
+ throw new Error("invalid-import");
126
+ return revision + 1;
127
+ }
128
+ function selectedRoot(selection, roots) {
129
+ if (roots.length === 0 || !object(selection) || selection["kind"] !== "root")
130
+ return null;
131
+ const index = selection["index"];
132
+ if (!natural(index) || roots[index] === undefined)
133
+ throw new Error("invalid-import");
134
+ return roots[index];
135
+ }
136
+ function selectionKey(value) {
137
+ return (absolute(value["path"]) &&
138
+ (value["root"] === null ||
139
+ (absolute(value["root"]) && withinRoot(value["root"], value["path"]))));
140
+ }
141
+ function revisionHeader(value, importId) {
142
+ const revision = value["evidenceRevision"];
143
+ if (value["schemaVersion"] !== 1 ||
144
+ value["importId"] !== importId ||
145
+ !natural(revision) ||
146
+ revision === 0 ||
147
+ revision === Number.MAX_SAFE_INTEGER)
148
+ return false;
149
+ const previous = value["previousImportId"];
150
+ if (typeof previous !== "string" ||
151
+ previous === importId ||
152
+ !/^[a-f\d]{8}-[a-f\d]{4}-4[a-f\d]{3}-[89ab][a-f\d]{3}-[a-f\d]{12}$/.test(previous))
153
+ return false;
154
+ return true;
155
+ }
156
+ function consistentReason(value) {
157
+ const before = value["before"];
158
+ const after = value["after"];
159
+ if (!revisionValue(before) || !revisionValue(after))
160
+ return false;
161
+ const reason = value["reason"];
162
+ const unverified = [
163
+ "no-previous-entry",
164
+ "not-selected",
165
+ "not-included",
166
+ ].includes(String(reason));
167
+ if (!unverified && after?.digest === null)
168
+ return false;
169
+ const prefix = reason === "verified-prefix" ? before?.digest : undefined;
170
+ return (evidenceReason({
171
+ before: comparable(before, { status: "included" }),
172
+ after: comparable(after, {
173
+ status: reason === "not-included" ? "unsupported" : "included",
174
+ prefixDigest: prefix ?? undefined,
175
+ }),
176
+ }) === reason);
177
+ }
178
+ function comparable(value, state) {
179
+ return value === null ? null : { ...value, ...state };
180
+ }
@@ -0,0 +1,48 @@
1
+ export interface SelectionKey {
2
+ readonly path: string;
3
+ readonly root: string | null;
4
+ }
5
+ export interface EvidenceValue {
6
+ readonly digest: string | null;
7
+ readonly byteSize?: number | undefined;
8
+ }
9
+ export interface ComparableEvidence extends EvidenceValue {
10
+ readonly status: string;
11
+ readonly prefixDigest?: string | undefined;
12
+ }
13
+ export interface EvidenceSource extends SelectionKey, ComparableEvidence {
14
+ }
15
+ export declare const revisionReasons: {
16
+ readonly "no-previous-entry": "new";
17
+ readonly "not-selected": "missing";
18
+ readonly "not-included": "missing";
19
+ readonly "same-digest": "unchanged";
20
+ readonly "previous-digest-unknown": "changed";
21
+ readonly "previous-size-unknown": "changed";
22
+ readonly "shorter-source": "truncated";
23
+ readonly "verified-prefix": "extended";
24
+ readonly "digest-changed": "changed";
25
+ };
26
+ export type RevisionReason = keyof typeof revisionReasons;
27
+ export type RevisionStatus = (typeof revisionReasons)[RevisionReason];
28
+ export interface RevisionEntry extends SelectionKey {
29
+ readonly before: EvidenceValue | null;
30
+ readonly after: EvidenceValue | null;
31
+ readonly status: RevisionStatus;
32
+ readonly reason: RevisionReason;
33
+ }
34
+ export declare function pairEvidence(data: {
35
+ readonly previous: readonly SelectionKey[];
36
+ readonly current: readonly SelectionKey[];
37
+ }): readonly (number | null)[];
38
+ export declare function evidenceReason(data: {
39
+ readonly before: ComparableEvidence | null;
40
+ readonly after: ComparableEvidence | null;
41
+ }): RevisionReason;
42
+ export declare function compareEvidence(data: {
43
+ readonly previous: readonly EvidenceSource[];
44
+ readonly current: readonly EvidenceSource[];
45
+ }): {
46
+ counts: Record<RevisionStatus, number>;
47
+ entries: RevisionEntry[];
48
+ };
@@ -0,0 +1,94 @@
1
+ export const revisionReasons = {
2
+ "no-previous-entry": "new",
3
+ "not-selected": "missing",
4
+ "not-included": "missing",
5
+ "same-digest": "unchanged",
6
+ "previous-digest-unknown": "changed",
7
+ "previous-size-unknown": "changed",
8
+ "shorter-source": "truncated",
9
+ "verified-prefix": "extended",
10
+ "digest-changed": "changed",
11
+ };
12
+ export function pairEvidence(data) {
13
+ const queues = new Map();
14
+ const key = (entry) => JSON.stringify([entry.path, entry.root]);
15
+ data.previous.forEach((entry, index) => {
16
+ const encoded = key(entry);
17
+ const queue = queues.get(encoded) ?? [];
18
+ queue.push(index);
19
+ queues.set(encoded, queue);
20
+ });
21
+ return data.current.map((entry) => queues.get(key(entry))?.shift() ?? null);
22
+ }
23
+ export function evidenceReason(data) {
24
+ const { before, after } = data;
25
+ if (before === null)
26
+ return "no-previous-entry";
27
+ if (after === null)
28
+ return "not-selected";
29
+ if (after.status !== "included")
30
+ return "not-included";
31
+ if (before.digest !== null && before.digest === after.digest)
32
+ return "same-digest";
33
+ if (before.digest === null)
34
+ return "previous-digest-unknown";
35
+ if (before.byteSize === undefined)
36
+ return "previous-size-unknown";
37
+ return sizeReason({ ...before, byteSize: before.byteSize }, after);
38
+ }
39
+ function sizeReason(before, after) {
40
+ if (after.byteSize !== undefined && after.byteSize < before.byteSize)
41
+ return "shorter-source";
42
+ if (after.byteSize !== undefined &&
43
+ after.byteSize > before.byteSize &&
44
+ after.prefixDigest === before.digest)
45
+ return "verified-prefix";
46
+ return "digest-changed";
47
+ }
48
+ function value(source) {
49
+ if (source === undefined)
50
+ return null;
51
+ return {
52
+ digest: source.digest,
53
+ ...(source.byteSize === undefined ? {} : { byteSize: source.byteSize }),
54
+ };
55
+ }
56
+ function entry(before, after) {
57
+ const key = after ?? before;
58
+ if (key === undefined)
59
+ throw new Error("invalid-evidence");
60
+ const why = evidenceReason({ before: before ?? null, after: after ?? null });
61
+ return {
62
+ path: key.path,
63
+ root: key.root,
64
+ before: value(before),
65
+ after: value(after),
66
+ status: revisionReasons[why],
67
+ reason: why,
68
+ };
69
+ }
70
+ export function compareEvidence(data) {
71
+ const pairs = pairEvidence(data);
72
+ const used = new Set(pairs);
73
+ const entries = data.current.map((after, index) => {
74
+ const matched = pairs[index];
75
+ return entry(matched === null || matched === undefined
76
+ ? undefined
77
+ : data.previous[matched], after);
78
+ });
79
+ data.previous.forEach((before, index) => {
80
+ if (!used.has(index))
81
+ entries.push(entry(before, undefined));
82
+ });
83
+ const counts = {
84
+ new: 0,
85
+ missing: 0,
86
+ unchanged: 0,
87
+ changed: 0,
88
+ truncated: 0,
89
+ extended: 0,
90
+ };
91
+ for (const item of entries)
92
+ counts[item.status]++;
93
+ return { counts, entries };
94
+ }
@@ -0,0 +1,25 @@
1
+ import { type ScanSource } from "./source.ts";
2
+ export interface SelectionRequest {
3
+ readonly paths: readonly string[];
4
+ readonly roots?: readonly string[];
5
+ readonly excludeProjects?: readonly string[];
6
+ readonly workingDirectory: string;
7
+ }
8
+ export interface SelectionOutcome {
9
+ readonly status: "unreadable" | "unsupported" | "excluded" | "pending";
10
+ readonly reason: string;
11
+ }
12
+ export interface SourceSelection {
13
+ readonly kind: "file" | "subtree";
14
+ readonly path: string;
15
+ readonly selection: {
16
+ readonly kind: "file" | "root";
17
+ readonly index: number;
18
+ };
19
+ readonly outcome?: SelectionOutcome;
20
+ readonly inspection?: ScanSource;
21
+ }
22
+ export declare function selectChatSources(request: SelectionRequest): Promise<{
23
+ roots: string[];
24
+ entries: SourceSelection[];
25
+ }>;
@@ -0,0 +1,109 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { basename, join, relative, resolve } from "node:path";
3
+ import { inspectScanSource, noFollowPath, refuseRepositoryRoot, sourceFailure, verifyScanSource, verifyScanAncestor, } from "./source.js";
4
+ function outcome(reason) {
5
+ return {
6
+ status: reason === "source-unreadable" ? "unreadable" : "unsupported",
7
+ reason,
8
+ };
9
+ }
10
+ async function scanEntry(entry, context) {
11
+ try {
12
+ const { exclusions, depth } = context;
13
+ const root = entry.inspection?.root ?? entry.path;
14
+ const inspection = await inspectScanSource(entry.path, root);
15
+ if (entry.inspection !== undefined)
16
+ verifyScanAncestor(entry.inspection, inspection);
17
+ const inspected = { ...entry, kind: inspection.kind, inspection };
18
+ const disposition = entryDisposition(inspected, context);
19
+ if (disposition !== undefined)
20
+ return [{ ...inspected, outcome: disposition }];
21
+ if (inspection.kind === "file")
22
+ return [inspected];
23
+ const entries = [];
24
+ const children = await readdir(noFollowPath(entry.path), {
25
+ withFileTypes: true,
26
+ });
27
+ await verifyScanSource(inspection);
28
+ for (const child of children) {
29
+ const scanned = await scanEntry({
30
+ ...inspected,
31
+ kind: child.isDirectory() ? "subtree" : "file",
32
+ path: join(entry.path, child.name),
33
+ }, { exclusions, depth: depth + 1 });
34
+ for (const selected of scanned)
35
+ entries.push(selected);
36
+ }
37
+ return entries;
38
+ }
39
+ catch (error) {
40
+ return [{ ...entry, outcome: outcome(sourceFailure(error)) }];
41
+ }
42
+ }
43
+ function entryDisposition(entry, context) {
44
+ if (entry.kind === "file")
45
+ return entry.path.endsWith(".jsonl")
46
+ ? undefined
47
+ : outcome("unsupported-extension");
48
+ if (context.depth === 1 && context.exclusions.includes(basename(entry.path)))
49
+ return { status: "excluded", reason: "project-excluded" };
50
+ if (context.depth === 4)
51
+ return { status: "pending", reason: "depth-limit" };
52
+ return;
53
+ }
54
+ async function prepareRoot(path) {
55
+ const entry = {
56
+ path,
57
+ kind: "subtree",
58
+ selection: { kind: "root", index: 0 },
59
+ };
60
+ try {
61
+ const inspection = await inspectScanSource(path, path);
62
+ if (inspection.kind !== "subtree")
63
+ throw new Error("non-directory-root");
64
+ await refuseRepositoryRoot(inspection.root);
65
+ return { ...entry, path: inspection.root, inspection };
66
+ }
67
+ catch (error) {
68
+ return { ...entry, outcome: outcome(sourceFailure(error)) };
69
+ }
70
+ }
71
+ export async function selectChatSources(request) {
72
+ const entries = request.paths.map((path, index) => ({
73
+ path: resolve(request.workingDirectory, path),
74
+ kind: "file",
75
+ selection: { kind: "file", index },
76
+ }));
77
+ const selectedRoots = [];
78
+ for (const value of request.roots ?? [])
79
+ selectedRoots.push(await prepareRoot(resolve(request.workingDirectory, value)));
80
+ selectedRoots.sort((left, right) => stringOrder(left.path, right.path));
81
+ const roots = selectedRoots.map((root) => root.path);
82
+ for (const [index, root] of selectedRoots.entries()) {
83
+ const selected = { ...root, selection: { kind: "root", index } };
84
+ const scanned = root.outcome === undefined
85
+ ? await scanEntry(selected, {
86
+ exclusions: request.excludeProjects ?? [],
87
+ depth: 0,
88
+ })
89
+ : [selected];
90
+ scanned.sort((left, right) => stringOrder(relative(root.path, left.path), relative(root.path, right.path)));
91
+ for (const entry of scanned)
92
+ entries.push(entry);
93
+ }
94
+ let attempts = 0;
95
+ return {
96
+ roots,
97
+ entries: entries.map((entry) => {
98
+ if (entry.selection.kind === "file" || entry.outcome !== undefined)
99
+ return entry;
100
+ attempts++;
101
+ return attempts <= 100
102
+ ? entry
103
+ : { ...entry, outcome: { status: "pending", reason: "file-limit" } };
104
+ }),
105
+ };
106
+ }
107
+ function stringOrder(left, right) {
108
+ return left < right ? -1 : Number(left > right);
109
+ }
@@ -0,0 +1,23 @@
1
+ import type { Stats } from "node:fs";
2
+ export interface SourceIdentity {
3
+ readonly path: string;
4
+ readonly device: number;
5
+ readonly inode: number;
6
+ }
7
+ export interface ScanSource {
8
+ readonly path: string;
9
+ readonly root: string;
10
+ readonly identities: readonly SourceIdentity[];
11
+ readonly size: number;
12
+ readonly modified: number;
13
+ readonly changed: number;
14
+ readonly kind: "file" | "subtree";
15
+ }
16
+ export declare function contained(root: string, path: string): boolean;
17
+ export declare function inspectScanSource(path: string, root: string): Promise<ScanSource>;
18
+ export declare function noFollowPath(path: string): string;
19
+ export declare function refuseRepositoryRoot(path: string): Promise<void>;
20
+ export declare function sourceFailure(error: unknown): string;
21
+ export declare function verifyScanSource(expected: ScanSource): Promise<void>;
22
+ export declare function verifyOpenSource(expected: ScanSource, stats: Stats): void;
23
+ export declare function verifyScanAncestor(expected: ScanSource, current: ScanSource): void;