@rejot-dev/thalo 0.0.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 (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +396 -0
  3. package/dist/ast/ast-types.d.ts +469 -0
  4. package/dist/ast/ast-types.d.ts.map +1 -0
  5. package/dist/ast/ast-types.js +11 -0
  6. package/dist/ast/ast-types.js.map +1 -0
  7. package/dist/ast/builder.js +158 -0
  8. package/dist/ast/builder.js.map +1 -0
  9. package/dist/ast/extract.js +748 -0
  10. package/dist/ast/extract.js.map +1 -0
  11. package/dist/ast/node-at-position.d.ts +147 -0
  12. package/dist/ast/node-at-position.d.ts.map +1 -0
  13. package/dist/ast/node-at-position.js +382 -0
  14. package/dist/ast/node-at-position.js.map +1 -0
  15. package/dist/ast/visitor.js +232 -0
  16. package/dist/ast/visitor.js.map +1 -0
  17. package/dist/checker/check.d.ts +53 -0
  18. package/dist/checker/check.d.ts.map +1 -0
  19. package/dist/checker/check.js +105 -0
  20. package/dist/checker/check.js.map +1 -0
  21. package/dist/checker/rules/actualize-missing-updated.js +34 -0
  22. package/dist/checker/rules/actualize-missing-updated.js.map +1 -0
  23. package/dist/checker/rules/actualize-unresolved-target.js +42 -0
  24. package/dist/checker/rules/actualize-unresolved-target.js.map +1 -0
  25. package/dist/checker/rules/alter-before-define.js +53 -0
  26. package/dist/checker/rules/alter-before-define.js.map +1 -0
  27. package/dist/checker/rules/alter-undefined-entity.js +32 -0
  28. package/dist/checker/rules/alter-undefined-entity.js.map +1 -0
  29. package/dist/checker/rules/create-requires-section.js +34 -0
  30. package/dist/checker/rules/create-requires-section.js.map +1 -0
  31. package/dist/checker/rules/define-entity-requires-section.js +31 -0
  32. package/dist/checker/rules/define-entity-requires-section.js.map +1 -0
  33. package/dist/checker/rules/duplicate-entity-definition.js +37 -0
  34. package/dist/checker/rules/duplicate-entity-definition.js.map +1 -0
  35. package/dist/checker/rules/duplicate-field-in-schema.js +38 -0
  36. package/dist/checker/rules/duplicate-field-in-schema.js.map +1 -0
  37. package/dist/checker/rules/duplicate-link-id.js +52 -0
  38. package/dist/checker/rules/duplicate-link-id.js.map +1 -0
  39. package/dist/checker/rules/duplicate-metadata-key.js +21 -0
  40. package/dist/checker/rules/duplicate-metadata-key.js.map +1 -0
  41. package/dist/checker/rules/duplicate-section-heading.js +41 -0
  42. package/dist/checker/rules/duplicate-section-heading.js.map +1 -0
  43. package/dist/checker/rules/duplicate-section-in-schema.js +38 -0
  44. package/dist/checker/rules/duplicate-section-in-schema.js.map +1 -0
  45. package/dist/checker/rules/duplicate-timestamp.js +104 -0
  46. package/dist/checker/rules/duplicate-timestamp.js.map +1 -0
  47. package/dist/checker/rules/empty-required-value.js +45 -0
  48. package/dist/checker/rules/empty-required-value.js.map +1 -0
  49. package/dist/checker/rules/empty-section.js +21 -0
  50. package/dist/checker/rules/empty-section.js.map +1 -0
  51. package/dist/checker/rules/invalid-date-range-value.js +56 -0
  52. package/dist/checker/rules/invalid-date-range-value.js.map +1 -0
  53. package/dist/checker/rules/invalid-default-value.js +86 -0
  54. package/dist/checker/rules/invalid-default-value.js.map +1 -0
  55. package/dist/checker/rules/invalid-field-type.js +45 -0
  56. package/dist/checker/rules/invalid-field-type.js.map +1 -0
  57. package/dist/checker/rules/missing-required-field.js +48 -0
  58. package/dist/checker/rules/missing-required-field.js.map +1 -0
  59. package/dist/checker/rules/missing-required-section.js +51 -0
  60. package/dist/checker/rules/missing-required-section.js.map +1 -0
  61. package/dist/checker/rules/missing-title.js +56 -0
  62. package/dist/checker/rules/missing-title.js.map +1 -0
  63. package/dist/checker/rules/remove-undefined-field.js +42 -0
  64. package/dist/checker/rules/remove-undefined-field.js.map +1 -0
  65. package/dist/checker/rules/remove-undefined-section.js +42 -0
  66. package/dist/checker/rules/remove-undefined-section.js.map +1 -0
  67. package/dist/checker/rules/rules.d.ts +71 -0
  68. package/dist/checker/rules/rules.d.ts.map +1 -0
  69. package/dist/checker/rules/rules.js +102 -0
  70. package/dist/checker/rules/rules.js.map +1 -0
  71. package/dist/checker/rules/synthesis-empty-query.js +35 -0
  72. package/dist/checker/rules/synthesis-empty-query.js.map +1 -0
  73. package/dist/checker/rules/synthesis-missing-prompt.js +42 -0
  74. package/dist/checker/rules/synthesis-missing-prompt.js.map +1 -0
  75. package/dist/checker/rules/synthesis-missing-sources.js +32 -0
  76. package/dist/checker/rules/synthesis-missing-sources.js.map +1 -0
  77. package/dist/checker/rules/synthesis-unknown-query-entity.js +39 -0
  78. package/dist/checker/rules/synthesis-unknown-query-entity.js.map +1 -0
  79. package/dist/checker/rules/timestamp-out-of-order.js +55 -0
  80. package/dist/checker/rules/timestamp-out-of-order.js.map +1 -0
  81. package/dist/checker/rules/unknown-entity.js +32 -0
  82. package/dist/checker/rules/unknown-entity.js.map +1 -0
  83. package/dist/checker/rules/unknown-field.js +40 -0
  84. package/dist/checker/rules/unknown-field.js.map +1 -0
  85. package/dist/checker/rules/unknown-section.js +47 -0
  86. package/dist/checker/rules/unknown-section.js.map +1 -0
  87. package/dist/checker/rules/unresolved-link.js +34 -0
  88. package/dist/checker/rules/unresolved-link.js.map +1 -0
  89. package/dist/checker/rules/update-without-create.js +65 -0
  90. package/dist/checker/rules/update-without-create.js.map +1 -0
  91. package/dist/checker/visitor.d.ts +69 -0
  92. package/dist/checker/visitor.d.ts.map +1 -0
  93. package/dist/checker/visitor.js +67 -0
  94. package/dist/checker/visitor.js.map +1 -0
  95. package/dist/checker/workspace-index.d.ts +50 -0
  96. package/dist/checker/workspace-index.d.ts.map +1 -0
  97. package/dist/checker/workspace-index.js +108 -0
  98. package/dist/checker/workspace-index.js.map +1 -0
  99. package/dist/commands/actualize.d.ts +113 -0
  100. package/dist/commands/actualize.d.ts.map +1 -0
  101. package/dist/commands/actualize.js +111 -0
  102. package/dist/commands/actualize.js.map +1 -0
  103. package/dist/commands/check.d.ts +65 -0
  104. package/dist/commands/check.d.ts.map +1 -0
  105. package/dist/commands/check.js +61 -0
  106. package/dist/commands/check.js.map +1 -0
  107. package/dist/commands/format.d.ts +90 -0
  108. package/dist/commands/format.d.ts.map +1 -0
  109. package/dist/commands/format.js +80 -0
  110. package/dist/commands/format.js.map +1 -0
  111. package/dist/commands/query.d.ts +152 -0
  112. package/dist/commands/query.d.ts.map +1 -0
  113. package/dist/commands/query.js +151 -0
  114. package/dist/commands/query.js.map +1 -0
  115. package/dist/constants.d.ts +31 -0
  116. package/dist/constants.d.ts.map +1 -0
  117. package/dist/constants.js +51 -0
  118. package/dist/constants.js.map +1 -0
  119. package/dist/files.d.ts +58 -0
  120. package/dist/files.d.ts.map +1 -0
  121. package/dist/files.js +103 -0
  122. package/dist/files.js.map +1 -0
  123. package/dist/formatters.d.ts +39 -0
  124. package/dist/formatters.d.ts.map +1 -0
  125. package/dist/formatters.js +200 -0
  126. package/dist/formatters.js.map +1 -0
  127. package/dist/fragment.d.ts +22 -0
  128. package/dist/fragment.d.ts.map +1 -0
  129. package/dist/git/git.js +240 -0
  130. package/dist/git/git.js.map +1 -0
  131. package/dist/merge/conflict-detector.d.ts +89 -0
  132. package/dist/merge/conflict-detector.d.ts.map +1 -0
  133. package/dist/merge/conflict-detector.js +352 -0
  134. package/dist/merge/conflict-detector.js.map +1 -0
  135. package/dist/merge/conflict-formatter.js +143 -0
  136. package/dist/merge/conflict-formatter.js.map +1 -0
  137. package/dist/merge/driver.d.ts +54 -0
  138. package/dist/merge/driver.d.ts.map +1 -0
  139. package/dist/merge/driver.js +112 -0
  140. package/dist/merge/driver.js.map +1 -0
  141. package/dist/merge/entry-matcher.d.ts +50 -0
  142. package/dist/merge/entry-matcher.d.ts.map +1 -0
  143. package/dist/merge/entry-matcher.js +141 -0
  144. package/dist/merge/entry-matcher.js.map +1 -0
  145. package/dist/merge/entry-merger.js +194 -0
  146. package/dist/merge/entry-merger.js.map +1 -0
  147. package/dist/merge/merge-result-builder.d.ts +62 -0
  148. package/dist/merge/merge-result-builder.d.ts.map +1 -0
  149. package/dist/merge/merge-result-builder.js +89 -0
  150. package/dist/merge/merge-result-builder.js.map +1 -0
  151. package/dist/mod.d.ts +31 -0
  152. package/dist/mod.js +23 -0
  153. package/dist/model/document.d.ts +134 -0
  154. package/dist/model/document.d.ts.map +1 -0
  155. package/dist/model/document.js +275 -0
  156. package/dist/model/document.js.map +1 -0
  157. package/dist/model/line-index.d.ts +85 -0
  158. package/dist/model/line-index.d.ts.map +1 -0
  159. package/dist/model/line-index.js +159 -0
  160. package/dist/model/line-index.js.map +1 -0
  161. package/dist/model/workspace.d.ts +296 -0
  162. package/dist/model/workspace.d.ts.map +1 -0
  163. package/dist/model/workspace.js +562 -0
  164. package/dist/model/workspace.js.map +1 -0
  165. package/dist/parser.js +27 -0
  166. package/dist/parser.js.map +1 -0
  167. package/dist/parser.native.d.ts +51 -0
  168. package/dist/parser.native.d.ts.map +1 -0
  169. package/dist/parser.native.js +62 -0
  170. package/dist/parser.native.js.map +1 -0
  171. package/dist/parser.shared.d.ts +99 -0
  172. package/dist/parser.shared.d.ts.map +1 -0
  173. package/dist/parser.shared.js +124 -0
  174. package/dist/parser.shared.js.map +1 -0
  175. package/dist/parser.web.d.ts +67 -0
  176. package/dist/parser.web.d.ts.map +1 -0
  177. package/dist/parser.web.js +49 -0
  178. package/dist/parser.web.js.map +1 -0
  179. package/dist/schema/registry.d.ts +108 -0
  180. package/dist/schema/registry.d.ts.map +1 -0
  181. package/dist/schema/registry.js +281 -0
  182. package/dist/schema/registry.js.map +1 -0
  183. package/dist/semantic/analyzer.d.ts +107 -0
  184. package/dist/semantic/analyzer.d.ts.map +1 -0
  185. package/dist/semantic/analyzer.js +261 -0
  186. package/dist/semantic/analyzer.js.map +1 -0
  187. package/dist/services/change-tracker/change-tracker.d.ts +111 -0
  188. package/dist/services/change-tracker/change-tracker.d.ts.map +1 -0
  189. package/dist/services/change-tracker/change-tracker.js +62 -0
  190. package/dist/services/change-tracker/change-tracker.js.map +1 -0
  191. package/dist/services/change-tracker/create-tracker.d.ts +42 -0
  192. package/dist/services/change-tracker/create-tracker.d.ts.map +1 -0
  193. package/dist/services/change-tracker/create-tracker.js +53 -0
  194. package/dist/services/change-tracker/create-tracker.js.map +1 -0
  195. package/dist/services/change-tracker/git-tracker.d.ts +59 -0
  196. package/dist/services/change-tracker/git-tracker.d.ts.map +1 -0
  197. package/dist/services/change-tracker/git-tracker.js +218 -0
  198. package/dist/services/change-tracker/git-tracker.js.map +1 -0
  199. package/dist/services/change-tracker/timestamp-tracker.d.ts +22 -0
  200. package/dist/services/change-tracker/timestamp-tracker.d.ts.map +1 -0
  201. package/dist/services/change-tracker/timestamp-tracker.js +74 -0
  202. package/dist/services/change-tracker/timestamp-tracker.js.map +1 -0
  203. package/dist/services/definition.d.ts +37 -0
  204. package/dist/services/definition.d.ts.map +1 -0
  205. package/dist/services/definition.js +43 -0
  206. package/dist/services/definition.js.map +1 -0
  207. package/dist/services/entity-navigation.d.ts +200 -0
  208. package/dist/services/entity-navigation.d.ts.map +1 -0
  209. package/dist/services/entity-navigation.js +211 -0
  210. package/dist/services/entity-navigation.js.map +1 -0
  211. package/dist/services/hover.d.ts +81 -0
  212. package/dist/services/hover.d.ts.map +1 -0
  213. package/dist/services/hover.js +669 -0
  214. package/dist/services/hover.js.map +1 -0
  215. package/dist/services/query.d.ts +116 -0
  216. package/dist/services/query.d.ts.map +1 -0
  217. package/dist/services/query.js +225 -0
  218. package/dist/services/query.js.map +1 -0
  219. package/dist/services/references.d.ts +52 -0
  220. package/dist/services/references.d.ts.map +1 -0
  221. package/dist/services/references.js +66 -0
  222. package/dist/services/references.js.map +1 -0
  223. package/dist/services/semantic-tokens.d.ts +54 -0
  224. package/dist/services/semantic-tokens.d.ts.map +1 -0
  225. package/dist/services/semantic-tokens.js +213 -0
  226. package/dist/services/semantic-tokens.js.map +1 -0
  227. package/dist/services/synthesis.d.ts +90 -0
  228. package/dist/services/synthesis.d.ts.map +1 -0
  229. package/dist/services/synthesis.js +113 -0
  230. package/dist/services/synthesis.js.map +1 -0
  231. package/dist/source-map.d.ts +42 -0
  232. package/dist/source-map.d.ts.map +1 -0
  233. package/dist/source-map.js +170 -0
  234. package/dist/source-map.js.map +1 -0
  235. package/package.json +128 -0
  236. package/tree-sitter-thalo.wasm +0 -0
  237. package/web-tree-sitter.wasm +0 -0
@@ -0,0 +1,59 @@
1
+ import { Workspace } from "../../model/workspace.js";
2
+ import { Query } from "../query.js";
3
+ import { ChangeMarker, ChangeTracker, ChangeTrackerOptions, ChangedEntriesResult } from "./change-tracker.js";
4
+
5
+ //#region src/services/change-tracker/git-tracker.d.ts
6
+
7
+ /**
8
+ * Git-based change tracker.
9
+ *
10
+ * Uses git to determine which entries have changed since the last actualization.
11
+ * This allows detection of in-place edits, not just new entries.
12
+ *
13
+ * Algorithm:
14
+ * 1. Get files modified since the marker commit
15
+ * 2. For each modified .thalo file:
16
+ * - Get file content at the marker commit
17
+ * - Parse both versions
18
+ * - Compare entries by identity (linkId or timestamp)
19
+ * - Mark entries as changed if they're new or content differs
20
+ */
21
+ declare class GitChangeTracker implements ChangeTracker {
22
+ readonly type: "git";
23
+ private cwd;
24
+ private force;
25
+ private blameIgnoreRevs;
26
+ constructor(options?: ChangeTrackerOptions);
27
+ private getIgnoreRevs;
28
+ getCurrentMarker(): Promise<ChangeMarker>;
29
+ getChangedEntries(workspace: Workspace, queries: Query[], marker: ChangeMarker | null): Promise<ChangedEntriesResult>;
30
+ /**
31
+ * Get all files that contain entries matching the queries.
32
+ */
33
+ private getSourceFiles;
34
+ /**
35
+ * Get all entries matching the queries (for first run or fallback)
36
+ */
37
+ private getAllMatchingEntries;
38
+ /**
39
+ * Get changed entries in a specific file.
40
+ *
41
+ * Note: This method only returns entries that exist in the current version of the file.
42
+ * Deleted entries are intentionally not tracked because for the actualization use case,
43
+ * we want entries to include in the synthesis prompt - deleted entries don't exist
44
+ * and therefore don't need to be synthesized.
45
+ */
46
+ private getChangedEntriesInFile;
47
+ /**
48
+ * Find a model in the workspace by relative path.
49
+ * Normalizes path separators for cross-platform compatibility.
50
+ *
51
+ * Handles cases where:
52
+ * - Git returns paths relative to repo root (e.g., "project/entries.thalo")
53
+ * - Workspace has files relative to cwd (e.g., "entries.thalo")
54
+ */
55
+ private findModelByRelativePath;
56
+ }
57
+ //#endregion
58
+ export { GitChangeTracker };
59
+ //# sourceMappingURL=git-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-tracker.d.ts","names":[],"sources":["../../../src/services/change-tracker/git-tracker.ts"],"sourcesContent":[],"mappings":";;;;;;;;AA0CA;;;;;;;;;;;;cAAa,gBAAA,YAA4B;;;;;wBAMlB;;sBAaK,QAAQ;+BAYrB,oBACF,iBACD,sBACP,QAAQ"}
@@ -0,0 +1,218 @@
1
+ import { entryMatchesQuery } from "../query.js";
2
+ import { UncommittedChangesError } from "./change-tracker.js";
3
+ import { extractSourceFile } from "../../ast/extract.js";
4
+ import { parseDocument } from "../../parser.js";
5
+ import { getEntryIdentity, serializeIdentity } from "../../merge/entry-matcher.js";
6
+ import { entriesEqual } from "../../merge/entry-merger.js";
7
+ import { commitExists, detectGitContext, getBlameCommitsForLineRange, getBlameIgnoreRevs, getCurrentCommit, getFileAtCommit, getFilesChangedSince, getUncommittedFiles, isCommitAncestorOf } from "../../git/git.js";
8
+
9
+ //#region src/services/change-tracker/git-tracker.ts
10
+ /**
11
+ * Git-based change tracker.
12
+ *
13
+ * Uses git to determine which entries have changed since the last actualization.
14
+ * This allows detection of in-place edits, not just new entries.
15
+ *
16
+ * Algorithm:
17
+ * 1. Get files modified since the marker commit
18
+ * 2. For each modified .thalo file:
19
+ * - Get file content at the marker commit
20
+ * - Parse both versions
21
+ * - Compare entries by identity (linkId or timestamp)
22
+ * - Mark entries as changed if they're new or content differs
23
+ */
24
+ var GitChangeTracker = class {
25
+ type = "git";
26
+ cwd;
27
+ force;
28
+ blameIgnoreRevs;
29
+ constructor(options = {}) {
30
+ this.cwd = options.cwd ?? process.cwd();
31
+ this.force = options.force ?? false;
32
+ }
33
+ async getIgnoreRevs() {
34
+ if (this.blameIgnoreRevs !== void 0) return this.blameIgnoreRevs;
35
+ this.blameIgnoreRevs = await getBlameIgnoreRevs(this.cwd);
36
+ return this.blameIgnoreRevs;
37
+ }
38
+ async getCurrentMarker() {
39
+ const commit = await getCurrentCommit(this.cwd);
40
+ if (!commit) throw new Error("Not in a git repository");
41
+ return {
42
+ type: "git",
43
+ value: commit
44
+ };
45
+ }
46
+ async getChangedEntries(workspace, queries, marker) {
47
+ if (!(await detectGitContext(this.cwd)).isGitRepo) throw new Error("Not in a git repository");
48
+ const currentMarker = await this.getCurrentMarker();
49
+ const sourceFiles = this.getSourceFiles(workspace, queries);
50
+ if (!this.force && sourceFiles.length > 0) {
51
+ const uncommitted = await getUncommittedFiles(this.cwd, sourceFiles);
52
+ if (uncommitted.length > 0) throw new UncommittedChangesError(uncommitted);
53
+ }
54
+ if (!marker) return {
55
+ entries: this.getAllMatchingEntries(workspace, queries),
56
+ currentMarker
57
+ };
58
+ if (marker.type === "ts") return {
59
+ entries: this.getAllMatchingEntries(workspace, queries),
60
+ currentMarker
61
+ };
62
+ if (!await commitExists(marker.value, this.cwd)) return {
63
+ entries: this.getAllMatchingEntries(workspace, queries),
64
+ currentMarker
65
+ };
66
+ const thaloChanges = (await getFilesChangedSince(marker.value, this.cwd)).filter((f) => f.path.endsWith(".thalo") || f.path.endsWith(".md"));
67
+ if (thaloChanges.length === 0) return {
68
+ entries: [],
69
+ currentMarker
70
+ };
71
+ const changedEntries = [];
72
+ const seenKeys = /* @__PURE__ */ new Set();
73
+ for (const change of thaloChanges) {
74
+ const entries = await this.getChangedEntriesInFile(workspace, change, marker.value, queries);
75
+ for (const entry of entries) {
76
+ const key = `${change.path}:${serializeIdentity(getEntryIdentity(entry))}`;
77
+ if (!seenKeys.has(key)) {
78
+ seenKeys.add(key);
79
+ changedEntries.push(entry);
80
+ }
81
+ }
82
+ }
83
+ return {
84
+ entries: changedEntries,
85
+ currentMarker
86
+ };
87
+ }
88
+ /**
89
+ * Get all files that contain entries matching the queries.
90
+ */
91
+ getSourceFiles(workspace, queries) {
92
+ const files = /* @__PURE__ */ new Set();
93
+ for (const model of workspace.allModels()) for (const entry of model.ast.entries) {
94
+ if (entry.type !== "instance_entry") continue;
95
+ for (const query of queries) if (entryMatchesQuery(entry, query)) {
96
+ files.add(model.file);
97
+ break;
98
+ }
99
+ }
100
+ return Array.from(files);
101
+ }
102
+ /**
103
+ * Get all entries matching the queries (for first run or fallback)
104
+ */
105
+ getAllMatchingEntries(workspace, queries) {
106
+ const results = [];
107
+ const seen = /* @__PURE__ */ new Set();
108
+ for (const model of workspace.allModels()) for (const entry of model.ast.entries) {
109
+ if (entry.type !== "instance_entry") continue;
110
+ const key = `${model.file}:${serializeIdentity(getEntryIdentity(entry))}`;
111
+ if (seen.has(key)) continue;
112
+ for (const query of queries) if (entryMatchesQuery(entry, query)) {
113
+ results.push(entry);
114
+ seen.add(key);
115
+ break;
116
+ }
117
+ }
118
+ return results;
119
+ }
120
+ /**
121
+ * Get changed entries in a specific file.
122
+ *
123
+ * Note: This method only returns entries that exist in the current version of the file.
124
+ * Deleted entries are intentionally not tracked because for the actualization use case,
125
+ * we want entries to include in the synthesis prompt - deleted entries don't exist
126
+ * and therefore don't need to be synthesized.
127
+ */
128
+ async getChangedEntriesInFile(workspace, change, markerCommit, queries) {
129
+ const model = this.findModelByRelativePath(workspace, change.path);
130
+ if (!model) return [];
131
+ const currentEntries = model.ast.entries.filter((e) => e.type === "instance_entry");
132
+ const ignoreRevs = await this.getIgnoreRevs();
133
+ if (ignoreRevs) {
134
+ const changed$1 = [];
135
+ function locationToLineRange(entry) {
136
+ const startRow = entry.location.startPosition.row;
137
+ const endRow = entry.location.endPosition.row;
138
+ const endCol = entry.location.endPosition.column;
139
+ const startLine = startRow + 1;
140
+ const endLine = endCol === 0 ? endRow : endRow + 1;
141
+ return {
142
+ startLine,
143
+ endLine: Math.max(startLine, endLine)
144
+ };
145
+ }
146
+ for (const entry of currentEntries) {
147
+ let matchesQuery = false;
148
+ for (const query of queries) if (entryMatchesQuery(entry, query)) {
149
+ matchesQuery = true;
150
+ break;
151
+ }
152
+ if (!matchesQuery) continue;
153
+ const { startLine, endLine } = locationToLineRange(entry);
154
+ const blamedCommits = await getBlameCommitsForLineRange(change.path, startLine, endLine, this.cwd, ignoreRevs);
155
+ let isChanged = false;
156
+ for (const commit of blamedCommits) if (!await isCommitAncestorOf(commit, markerCommit, this.cwd)) {
157
+ isChanged = true;
158
+ break;
159
+ }
160
+ if (isChanged) changed$1.push(entry);
161
+ }
162
+ return changed$1;
163
+ }
164
+ const pathAtCommit = change.oldPath ?? change.path;
165
+ const oldContent = await getFileAtCommit(pathAtCommit, markerCommit, this.cwd);
166
+ const oldEntryMap = /* @__PURE__ */ new Map();
167
+ if (oldContent) try {
168
+ const oldDoc = parseDocument(oldContent, { fileType: pathAtCommit.endsWith(".md") ? "markdown" : "thalo" });
169
+ for (const block of oldDoc.blocks) {
170
+ const oldAst = extractSourceFile(block.tree.rootNode);
171
+ for (const entry of oldAst.entries) {
172
+ const key = serializeIdentity(getEntryIdentity(entry));
173
+ oldEntryMap.set(key, entry);
174
+ }
175
+ }
176
+ } catch {}
177
+ const changed = [];
178
+ for (const entry of currentEntries) {
179
+ let matchesQuery = false;
180
+ for (const query of queries) if (entryMatchesQuery(entry, query)) {
181
+ matchesQuery = true;
182
+ break;
183
+ }
184
+ if (!matchesQuery) continue;
185
+ const key = serializeIdentity(getEntryIdentity(entry));
186
+ const oldEntry = oldEntryMap.get(key);
187
+ if (!oldEntry || !entriesEqual(oldEntry, entry)) changed.push(entry);
188
+ }
189
+ return changed;
190
+ }
191
+ /**
192
+ * Find a model in the workspace by relative path.
193
+ * Normalizes path separators for cross-platform compatibility.
194
+ *
195
+ * Handles cases where:
196
+ * - Git returns paths relative to repo root (e.g., "project/entries.thalo")
197
+ * - Workspace has files relative to cwd (e.g., "entries.thalo")
198
+ */
199
+ findModelByRelativePath(workspace, relativePath) {
200
+ const normalizedRelative = relativePath.replace(/\\/g, "/");
201
+ const relativeParts = normalizedRelative.split("/");
202
+ for (const model of workspace.allModels()) {
203
+ const normalizedModel = model.file.replace(/\\/g, "/");
204
+ if (normalizedModel === normalizedRelative) return model;
205
+ const modelParts = normalizedModel.split("/");
206
+ if (relativeParts.length >= modelParts.length) {
207
+ if (relativeParts.slice(-modelParts.length).join("/") === normalizedModel) return model;
208
+ }
209
+ if (modelParts.length >= relativeParts.length) {
210
+ if (modelParts.slice(-relativeParts.length).join("/") === normalizedRelative) return model;
211
+ }
212
+ }
213
+ }
214
+ };
215
+
216
+ //#endregion
217
+ export { GitChangeTracker };
218
+ //# sourceMappingURL=git-tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-tracker.js","names":["changedEntries: InstanceEntry[]","results: InstanceEntry[]","changed: InstanceEntry[]","changed"],"sources":["../../../src/services/change-tracker/git-tracker.ts"],"sourcesContent":["import type { Entry, InstanceEntry } from \"../../ast/ast-types.js\";\nimport type { Query } from \"../query.js\";\nimport type { Workspace } from \"../../model/workspace.js\";\nimport {\n detectGitContext,\n getCurrentCommit,\n getFilesChangedSince,\n getFileAtCommit,\n commitExists,\n getUncommittedFiles,\n getBlameIgnoreRevs,\n getBlameCommitsForLineRange,\n isCommitAncestorOf,\n type FileChange,\n} from \"../../git/git.js\";\nimport { getEntryIdentity, serializeIdentity } from \"../../merge/entry-matcher.js\";\nimport { entriesEqual } from \"../../merge/entry-merger.js\";\nimport { entryMatchesQuery } from \"../query.js\";\nimport { parseDocument } from \"../../parser.js\";\nimport { extractSourceFile } from \"../../ast/extract.js\";\nimport type {\n ChangeTracker,\n ChangeMarker,\n ChangedEntriesResult,\n ChangeTrackerOptions,\n} from \"./change-tracker.js\";\nimport { UncommittedChangesError } from \"./change-tracker.js\";\n\n/**\n * Git-based change tracker.\n *\n * Uses git to determine which entries have changed since the last actualization.\n * This allows detection of in-place edits, not just new entries.\n *\n * Algorithm:\n * 1. Get files modified since the marker commit\n * 2. For each modified .thalo file:\n * - Get file content at the marker commit\n * - Parse both versions\n * - Compare entries by identity (linkId or timestamp)\n * - Mark entries as changed if they're new or content differs\n */\nexport class GitChangeTracker implements ChangeTracker {\n readonly type = \"git\" as const;\n private cwd: string;\n private force: boolean;\n private blameIgnoreRevs: string[] | null | undefined;\n\n constructor(options: ChangeTrackerOptions = {}) {\n this.cwd = options.cwd ?? process.cwd();\n this.force = options.force ?? false;\n }\n\n private async getIgnoreRevs(): Promise<string[] | null> {\n if (this.blameIgnoreRevs !== undefined) {\n return this.blameIgnoreRevs;\n }\n this.blameIgnoreRevs = await getBlameIgnoreRevs(this.cwd);\n return this.blameIgnoreRevs;\n }\n\n async getCurrentMarker(): Promise<ChangeMarker> {\n const commit = await getCurrentCommit(this.cwd);\n if (!commit) {\n throw new Error(\"Not in a git repository\");\n }\n return {\n type: \"git\",\n value: commit,\n };\n }\n\n async getChangedEntries(\n workspace: Workspace,\n queries: Query[],\n marker: ChangeMarker | null,\n ): Promise<ChangedEntriesResult> {\n // Validate git context\n const gitContext = await detectGitContext(this.cwd);\n if (!gitContext.isGitRepo) {\n throw new Error(\"Not in a git repository\");\n }\n\n const currentMarker = await this.getCurrentMarker();\n\n // Find files that contain matching entries (source files for the queries)\n const sourceFiles = this.getSourceFiles(workspace, queries);\n\n // Check for uncommitted changes in source files (unless force is set)\n if (!this.force && sourceFiles.length > 0) {\n const uncommitted = await getUncommittedFiles(this.cwd, sourceFiles);\n if (uncommitted.length > 0) {\n throw new UncommittedChangesError(uncommitted);\n }\n }\n\n // If no marker, return all matching entries (first run)\n if (!marker) {\n return {\n entries: this.getAllMatchingEntries(workspace, queries),\n currentMarker,\n };\n }\n\n // If marker is a timestamp, we can still handle it by returning all entries\n // (graceful fallback - user switched from timestamp to git tracking)\n if (marker.type === \"ts\") {\n return {\n entries: this.getAllMatchingEntries(workspace, queries),\n currentMarker,\n };\n }\n\n // Validate the commit exists\n const exists = await commitExists(marker.value, this.cwd);\n if (!exists) {\n // Commit doesn't exist (maybe rebased or squashed)\n // Return all matching entries as fallback\n return {\n entries: this.getAllMatchingEntries(workspace, queries),\n currentMarker,\n };\n }\n\n // Get files changed since the marker commit (with rename detection)\n const changedFiles = await getFilesChangedSince(marker.value, this.cwd);\n const thaloChanges = changedFiles.filter(\n (f) => f.path.endsWith(\".thalo\") || f.path.endsWith(\".md\"),\n );\n\n // If no thalo files changed, return empty\n if (thaloChanges.length === 0) {\n return {\n entries: [],\n currentMarker,\n };\n }\n\n // Find changed entries\n const changedEntries: InstanceEntry[] = [];\n const seenKeys = new Set<string>();\n\n for (const change of thaloChanges) {\n const entries = await this.getChangedEntriesInFile(workspace, change, marker.value, queries);\n\n for (const entry of entries) {\n // Include file path in dedup key to handle timestamp-based identities across files\n // (Link IDs are globally unique, but timestamps can collide across files)\n const key = `${change.path}:${serializeIdentity(getEntryIdentity(entry))}`;\n if (!seenKeys.has(key)) {\n seenKeys.add(key);\n changedEntries.push(entry);\n }\n }\n }\n\n return {\n entries: changedEntries,\n currentMarker,\n };\n }\n\n /**\n * Get all files that contain entries matching the queries.\n */\n private getSourceFiles(workspace: Workspace, queries: Query[]): string[] {\n const files = new Set<string>();\n\n for (const model of workspace.allModels()) {\n for (const entry of model.ast.entries) {\n if (entry.type !== \"instance_entry\") {\n continue;\n }\n\n for (const query of queries) {\n if (entryMatchesQuery(entry, query)) {\n files.add(model.file);\n break;\n }\n }\n }\n }\n\n return Array.from(files);\n }\n\n /**\n * Get all entries matching the queries (for first run or fallback)\n */\n private getAllMatchingEntries(workspace: Workspace, queries: Query[]): InstanceEntry[] {\n const results: InstanceEntry[] = [];\n const seen = new Set<string>();\n\n for (const model of workspace.allModels()) {\n for (const entry of model.ast.entries) {\n if (entry.type !== \"instance_entry\") {\n continue;\n }\n\n const key = `${model.file}:${serializeIdentity(getEntryIdentity(entry))}`;\n\n if (seen.has(key)) {\n continue;\n }\n\n for (const query of queries) {\n if (entryMatchesQuery(entry, query)) {\n results.push(entry);\n seen.add(key);\n break;\n }\n }\n }\n }\n\n return results;\n }\n\n /**\n * Get changed entries in a specific file.\n *\n * Note: This method only returns entries that exist in the current version of the file.\n * Deleted entries are intentionally not tracked because for the actualization use case,\n * we want entries to include in the synthesis prompt - deleted entries don't exist\n * and therefore don't need to be synthesized.\n */\n private async getChangedEntriesInFile(\n workspace: Workspace,\n change: FileChange,\n markerCommit: string,\n queries: Query[],\n ): Promise<InstanceEntry[]> {\n // Find the model for this file in the workspace (use current path)\n const model = this.findModelByRelativePath(workspace, change.path);\n if (!model) {\n return [];\n }\n\n // Get current entries\n const currentEntries = model.ast.entries.filter(\n (e): e is InstanceEntry => e.type === \"instance_entry\",\n );\n\n // If a blame ignore-revs file exists, use blame-based change detection.\n // This allows users to ignore formatting-only commits (e.g. via `.git-blame-ignore-revs`)\n // so they don't retrigger syntheses.\n const ignoreRevs = await this.getIgnoreRevs();\n if (ignoreRevs) {\n const changed: InstanceEntry[] = [];\n\n // Normalize end line: tree-sitter endPosition is exclusive.\n function locationToLineRange(entry: InstanceEntry): { startLine: number; endLine: number } {\n const startRow = entry.location.startPosition.row;\n const endRow = entry.location.endPosition.row;\n const endCol = entry.location.endPosition.column;\n const startLine = startRow + 1;\n const endLine = endCol === 0 ? endRow : endRow + 1;\n return { startLine, endLine: Math.max(startLine, endLine) };\n }\n\n for (const entry of currentEntries) {\n // Check if entry matches any query\n let matchesQuery = false;\n for (const query of queries) {\n if (entryMatchesQuery(entry, query)) {\n matchesQuery = true;\n break;\n }\n }\n if (!matchesQuery) {\n continue;\n }\n\n const { startLine, endLine } = locationToLineRange(entry);\n const blamedCommits = await getBlameCommitsForLineRange(\n change.path,\n startLine,\n endLine,\n this.cwd,\n ignoreRevs,\n );\n\n let isChanged = false;\n for (const commit of blamedCommits) {\n // If the blamed commit is not an ancestor of the marker, then the entry\n // has changes \"after\" the marker (including merges), after applying ignore-revs.\n const isBeforeMarker = await isCommitAncestorOf(commit, markerCommit, this.cwd);\n if (!isBeforeMarker) {\n isChanged = true;\n break;\n }\n }\n\n if (isChanged) {\n changed.push(entry);\n }\n }\n\n return changed;\n }\n\n // Get old file content - use oldPath for renames, otherwise current path\n const pathAtCommit = change.oldPath ?? change.path;\n const oldContent = await getFileAtCommit(pathAtCommit, markerCommit, this.cwd);\n\n // Build map of old entries\n const oldEntryMap = new Map<string, Entry>();\n if (oldContent) {\n try {\n // Detect file type from extension (markdown files contain embedded thalo blocks)\n const fileType = pathAtCommit.endsWith(\".md\") ? \"markdown\" : \"thalo\";\n const oldDoc = parseDocument(oldContent, { fileType });\n\n // Iterate all blocks (markdown files may have multiple thalo blocks)\n for (const block of oldDoc.blocks) {\n const oldAst = extractSourceFile(block.tree.rootNode);\n for (const entry of oldAst.entries) {\n const key = serializeIdentity(getEntryIdentity(entry));\n oldEntryMap.set(key, entry);\n }\n }\n } catch {\n // Parse error in old content - treat all current entries as changed\n }\n }\n\n // Find changed entries\n const changed: InstanceEntry[] = [];\n\n for (const entry of currentEntries) {\n // Check if entry matches any query\n let matchesQuery = false;\n for (const query of queries) {\n if (entryMatchesQuery(entry, query)) {\n matchesQuery = true;\n break;\n }\n }\n\n if (!matchesQuery) {\n continue;\n }\n\n const key = serializeIdentity(getEntryIdentity(entry));\n const oldEntry = oldEntryMap.get(key);\n\n // Entry is changed if:\n // 1. It didn't exist at the marker commit (new entry)\n // 2. Its content differs from the marker commit (modified entry)\n if (!oldEntry || !entriesEqual(oldEntry, entry)) {\n changed.push(entry);\n }\n }\n\n return changed;\n }\n\n /**\n * Find a model in the workspace by relative path.\n * Normalizes path separators for cross-platform compatibility.\n *\n * Handles cases where:\n * - Git returns paths relative to repo root (e.g., \"project/entries.thalo\")\n * - Workspace has files relative to cwd (e.g., \"entries.thalo\")\n */\n private findModelByRelativePath(\n workspace: Workspace,\n relativePath: string,\n ): ReturnType<typeof workspace.getModel> {\n // Normalize to forward slashes for comparison (git always uses forward slashes)\n const normalizedRelative = relativePath.replace(/\\\\/g, \"/\");\n const relativeParts = normalizedRelative.split(\"/\");\n\n for (const model of workspace.allModels()) {\n const normalizedModel = model.file.replace(/\\\\/g, \"/\");\n\n // Exact match\n if (normalizedModel === normalizedRelative) {\n return model;\n }\n\n const modelParts = normalizedModel.split(\"/\");\n\n // Check if git path ends with model path (user running from subdirectory)\n // e.g., git returns \"project/entries.thalo\", model is \"entries.thalo\"\n if (relativeParts.length >= modelParts.length) {\n const relativeSuffix = relativeParts.slice(-modelParts.length).join(\"/\");\n if (relativeSuffix === normalizedModel) {\n return model;\n }\n }\n\n // Check if model path ends with git path (model has absolute/longer path)\n // e.g., model is \"/repo/project/entries.thalo\", git returns \"entries.thalo\"\n if (modelParts.length >= relativeParts.length) {\n const modelSuffix = modelParts.slice(-relativeParts.length).join(\"/\");\n if (modelSuffix === normalizedRelative) {\n return model;\n }\n }\n }\n return undefined;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { Workspace } from "../../model/workspace.js";
2
+ import { Query } from "../query.js";
3
+ import { ChangeMarker, ChangeTracker, ChangedEntriesResult } from "./change-tracker.js";
4
+
5
+ //#region src/services/change-tracker/timestamp-tracker.d.ts
6
+
7
+ /**
8
+ * Timestamp-based change tracker.
9
+ *
10
+ * This is the fallback implementation that uses entry timestamps
11
+ * to determine which entries are "new" since the last actualization.
12
+ *
13
+ * Simple but limited - can't detect in-place edits, only new entries.
14
+ */
15
+ declare class TimestampChangeTracker implements ChangeTracker {
16
+ readonly type: "ts";
17
+ getCurrentMarker(): Promise<ChangeMarker>;
18
+ getChangedEntries(workspace: Workspace, queries: Query[], marker: ChangeMarker | null): Promise<ChangedEntriesResult>;
19
+ }
20
+ //#endregion
21
+ export { TimestampChangeTracker };
22
+ //# sourceMappingURL=timestamp-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timestamp-tracker.d.ts","names":[],"sources":["../../../src/services/change-tracker/timestamp-tracker.ts"],"sourcesContent":[],"mappings":";;;;;;;;AA4CA;;;;;;AAca,cAdA,sBAAA,YAAkC,aAclC,CAAA;EAAR,SAAA,IAAA,EAAA,IAAA;EAd0C,gBAAA,CAAA,CAAA,EAGnB,OAHmB,CAGX,YAHW,CAAA;EAAa,iBAAA,CAAA,SAAA,EAW7C,SAX6C,EAAA,OAAA,EAY/C,KAZ+C,EAAA,EAAA,MAAA,EAahD,YAbgD,GAAA,IAAA,CAAA,EAcvD,OAduD,CAc/C,oBAd+C,CAAA"}
@@ -0,0 +1,74 @@
1
+ import { entryMatchesQuery } from "../query.js";
2
+ import { formatTimestamp } from "../../formatters.js";
3
+
4
+ //#region src/services/change-tracker/timestamp-tracker.ts
5
+ /**
6
+ * Convert a timestamp to epoch milliseconds for correct comparison across timezones
7
+ */
8
+ function timestampToEpoch(ts) {
9
+ const isoStr = formatTimestamp(ts);
10
+ return Date.parse(isoStr);
11
+ }
12
+ /**
13
+ * Parse an ISO timestamp string to epoch milliseconds
14
+ */
15
+ function parseTimestampToEpoch(isoStr) {
16
+ return Date.parse(isoStr);
17
+ }
18
+ /**
19
+ * Get current ISO timestamp
20
+ */
21
+ function getCurrentTimestamp() {
22
+ const now = /* @__PURE__ */ new Date();
23
+ return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}-${String(now.getUTCDate()).padStart(2, "0")}T${String(now.getUTCHours()).padStart(2, "0")}:${String(now.getUTCMinutes()).padStart(2, "0")}Z`;
24
+ }
25
+ /**
26
+ * Timestamp-based change tracker.
27
+ *
28
+ * This is the fallback implementation that uses entry timestamps
29
+ * to determine which entries are "new" since the last actualization.
30
+ *
31
+ * Simple but limited - can't detect in-place edits, only new entries.
32
+ */
33
+ var TimestampChangeTracker = class {
34
+ type = "ts";
35
+ async getCurrentMarker() {
36
+ return {
37
+ type: "ts",
38
+ value: getCurrentTimestamp()
39
+ };
40
+ }
41
+ async getChangedEntries(workspace, queries, marker) {
42
+ const afterTimestamp = marker?.type === "ts" ? marker.value : null;
43
+ const afterEpoch = afterTimestamp ? parseTimestampToEpoch(afterTimestamp) : null;
44
+ const results = [];
45
+ const seen = /* @__PURE__ */ new Set();
46
+ for (const model of workspace.allModels()) for (const entry of model.ast.entries) {
47
+ if (entry.type !== "instance_entry") continue;
48
+ const timestampEpoch = timestampToEpoch(entry.header.timestamp);
49
+ const timestampStr = formatTimestamp(entry.header.timestamp);
50
+ const entityType = entry.header.entity ?? "unknown";
51
+ const linkId = entry.header.link?.id ?? "";
52
+ const key = `${model.file}:${timestampStr}:${entityType}:${linkId}`;
53
+ if (seen.has(key)) continue;
54
+ if (afterEpoch !== null && timestampEpoch <= afterEpoch) continue;
55
+ for (const query of queries) if (entryMatchesQuery(entry, query)) {
56
+ results.push({
57
+ entry,
58
+ timestampEpoch
59
+ });
60
+ seen.add(key);
61
+ break;
62
+ }
63
+ }
64
+ results.sort((a, b) => a.timestampEpoch - b.timestampEpoch);
65
+ return {
66
+ entries: results.map((r) => r.entry),
67
+ currentMarker: await this.getCurrentMarker()
68
+ };
69
+ }
70
+ };
71
+
72
+ //#endregion
73
+ export { TimestampChangeTracker };
74
+ //# sourceMappingURL=timestamp-tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timestamp-tracker.js","names":["results: { entry: InstanceEntry; timestampEpoch: number }[]"],"sources":["../../../src/services/change-tracker/timestamp-tracker.ts"],"sourcesContent":["import type { Timestamp, InstanceEntry } from \"../../ast/ast-types.js\";\nimport type { Query } from \"../query.js\";\nimport type { Workspace } from \"../../model/workspace.js\";\nimport { entryMatchesQuery } from \"../query.js\";\nimport { formatTimestamp } from \"../../formatters.js\";\nimport type { ChangeTracker, ChangeMarker, ChangedEntriesResult } from \"./change-tracker.js\";\n\n/**\n * Convert a timestamp to epoch milliseconds for correct comparison across timezones\n */\nfunction timestampToEpoch(ts: Timestamp): number {\n const isoStr = formatTimestamp(ts);\n // Date.parse handles timezone offsets correctly\n return Date.parse(isoStr);\n}\n\n/**\n * Parse an ISO timestamp string to epoch milliseconds\n */\nfunction parseTimestampToEpoch(isoStr: string): number {\n return Date.parse(isoStr);\n}\n\n/**\n * Get current ISO timestamp\n */\nfunction getCurrentTimestamp(): string {\n const now = new Date();\n const year = now.getUTCFullYear();\n const month = String(now.getUTCMonth() + 1).padStart(2, \"0\");\n const day = String(now.getUTCDate()).padStart(2, \"0\");\n const hour = String(now.getUTCHours()).padStart(2, \"0\");\n const minute = String(now.getUTCMinutes()).padStart(2, \"0\");\n return `${year}-${month}-${day}T${hour}:${minute}Z`;\n}\n\n/**\n * Timestamp-based change tracker.\n *\n * This is the fallback implementation that uses entry timestamps\n * to determine which entries are \"new\" since the last actualization.\n *\n * Simple but limited - can't detect in-place edits, only new entries.\n */\nexport class TimestampChangeTracker implements ChangeTracker {\n readonly type = \"ts\" as const;\n\n async getCurrentMarker(): Promise<ChangeMarker> {\n return {\n type: \"ts\",\n value: getCurrentTimestamp(),\n };\n }\n\n async getChangedEntries(\n workspace: Workspace,\n queries: Query[],\n marker: ChangeMarker | null,\n ): Promise<ChangedEntriesResult> {\n // Only use the marker if it's a timestamp type\n // If marker is git type (from switching modes), return all entries\n const afterTimestamp = marker?.type === \"ts\" ? marker.value : null;\n // Convert to epoch for correct comparison across timezones\n const afterEpoch = afterTimestamp ? parseTimestampToEpoch(afterTimestamp) : null;\n\n const results: { entry: InstanceEntry; timestampEpoch: number }[] = [];\n const seen = new Set<string>();\n\n for (const model of workspace.allModels()) {\n for (const entry of model.ast.entries) {\n if (entry.type !== \"instance_entry\") {\n continue;\n }\n\n const timestampEpoch = timestampToEpoch(entry.header.timestamp);\n const timestampStr = formatTimestamp(entry.header.timestamp);\n // Include entity type and link in key to avoid collisions for same-minute entries\n const entityType = entry.header.entity ?? \"unknown\";\n const linkId = entry.header.link?.id ?? \"\";\n const key = `${model.file}:${timestampStr}:${entityType}:${linkId}`;\n\n // Skip if we've already seen this entry\n if (seen.has(key)) {\n continue;\n }\n\n // Skip if entry is before or at the cutoff (use epoch for correct timezone comparison)\n if (afterEpoch !== null && timestampEpoch <= afterEpoch) {\n continue;\n }\n\n // Check if entry matches any of the queries\n for (const query of queries) {\n if (entryMatchesQuery(entry, query)) {\n results.push({ entry, timestampEpoch });\n seen.add(key);\n break;\n }\n }\n }\n }\n\n // Sort by timestamp (epoch for correct ordering)\n results.sort((a, b) => a.timestampEpoch - b.timestampEpoch);\n\n return {\n entries: results.map((r) => r.entry),\n currentMarker: await this.getCurrentMarker(),\n };\n }\n}\n"],"mappings":";;;;;;;AAUA,SAAS,iBAAiB,IAAuB;CAC/C,MAAM,SAAS,gBAAgB,GAAG;AAElC,QAAO,KAAK,MAAM,OAAO;;;;;AAM3B,SAAS,sBAAsB,QAAwB;AACrD,QAAO,KAAK,MAAM,OAAO;;;;;AAM3B,SAAS,sBAA8B;CACrC,MAAM,sBAAM,IAAI,MAAM;AAMtB,QAAO,GALM,IAAI,gBAAgB,CAKlB,GAJD,OAAO,IAAI,aAAa,GAAG,EAAE,CAAC,SAAS,GAAG,IAAI,CAIpC,GAHZ,OAAO,IAAI,YAAY,CAAC,CAAC,SAAS,GAAG,IAAI,CAGtB,GAFlB,OAAO,IAAI,aAAa,CAAC,CAAC,SAAS,GAAG,IAAI,CAEhB,GADxB,OAAO,IAAI,eAAe,CAAC,CAAC,SAAS,GAAG,IAAI,CACV;;;;;;;;;;AAWnD,IAAa,yBAAb,MAA6D;CAC3D,AAAS,OAAO;CAEhB,MAAM,mBAA0C;AAC9C,SAAO;GACL,MAAM;GACN,OAAO,qBAAqB;GAC7B;;CAGH,MAAM,kBACJ,WACA,SACA,QAC+B;EAG/B,MAAM,iBAAiB,QAAQ,SAAS,OAAO,OAAO,QAAQ;EAE9D,MAAM,aAAa,iBAAiB,sBAAsB,eAAe,GAAG;EAE5E,MAAMA,UAA8D,EAAE;EACtE,MAAM,uBAAO,IAAI,KAAa;AAE9B,OAAK,MAAM,SAAS,UAAU,WAAW,CACvC,MAAK,MAAM,SAAS,MAAM,IAAI,SAAS;AACrC,OAAI,MAAM,SAAS,iBACjB;GAGF,MAAM,iBAAiB,iBAAiB,MAAM,OAAO,UAAU;GAC/D,MAAM,eAAe,gBAAgB,MAAM,OAAO,UAAU;GAE5D,MAAM,aAAa,MAAM,OAAO,UAAU;GAC1C,MAAM,SAAS,MAAM,OAAO,MAAM,MAAM;GACxC,MAAM,MAAM,GAAG,MAAM,KAAK,GAAG,aAAa,GAAG,WAAW,GAAG;AAG3D,OAAI,KAAK,IAAI,IAAI,CACf;AAIF,OAAI,eAAe,QAAQ,kBAAkB,WAC3C;AAIF,QAAK,MAAM,SAAS,QAClB,KAAI,kBAAkB,OAAO,MAAM,EAAE;AACnC,YAAQ,KAAK;KAAE;KAAO;KAAgB,CAAC;AACvC,SAAK,IAAI,IAAI;AACb;;;AAOR,UAAQ,MAAM,GAAG,MAAM,EAAE,iBAAiB,EAAE,eAAe;AAE3D,SAAO;GACL,SAAS,QAAQ,KAAK,MAAM,EAAE,MAAM;GACpC,eAAe,MAAM,KAAK,kBAAkB;GAC7C"}
@@ -0,0 +1,37 @@
1
+ import { Location } from "../ast/ast-types.js";
2
+ import { LinkDefinition } from "../semantic/analyzer.js";
3
+ import { Workspace } from "../model/workspace.js";
4
+
5
+ //#region src/services/definition.d.ts
6
+
7
+ /**
8
+ * Result of a find-definition query
9
+ */
10
+ interface DefinitionResult {
11
+ /** The file containing the definition */
12
+ file: string;
13
+ /** Location of the definition (file-absolute) */
14
+ location: Location;
15
+ /** The link definition details */
16
+ definition: LinkDefinition;
17
+ }
18
+ /**
19
+ * Find the definition for a link ID
20
+ *
21
+ * @param workspace - The workspace to search in
22
+ * @param linkId - The link ID to find (without ^ prefix)
23
+ * @returns The definition result with file-absolute location, or undefined if not found
24
+ */
25
+ declare function findDefinition(workspace: Workspace, linkId: string): DefinitionResult | undefined;
26
+ /**
27
+ * Find the definition at a given position in a file
28
+ *
29
+ * @param workspace - The workspace to search in
30
+ * @param file - The file path
31
+ * @param offset - The character offset in the file
32
+ * @returns The definition result, or undefined if no link at position or not found
33
+ */
34
+ declare function findDefinitionAtPosition(workspace: Workspace, file: string, offset: number): DefinitionResult | undefined;
35
+ //#endregion
36
+ export { DefinitionResult, findDefinition, findDefinitionAtPosition };
37
+ //# sourceMappingURL=definition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definition.d.ts","names":[],"sources":["../../src/services/definition.ts"],"sourcesContent":[],"mappings":";;;;;;;;AASA;AAgBgB,UAhBC,gBAAA,CAgByB;EA8B1B;;;YA1CJ;;cAEE;;;;;;;;;iBAUE,cAAA,YAA0B,4BAA4B;;;;;;;;;iBA8BtD,wBAAA,YACH,0CAGV"}
@@ -0,0 +1,43 @@
1
+ import { positionFromOffset, toFileLocation } from "../source-map.js";
2
+ import { findNodeAtPosition } from "../ast/node-at-position.js";
3
+
4
+ //#region src/services/definition.ts
5
+ /**
6
+ * Find the definition for a link ID
7
+ *
8
+ * @param workspace - The workspace to search in
9
+ * @param linkId - The link ID to find (without ^ prefix)
10
+ * @returns The definition result with file-absolute location, or undefined if not found
11
+ */
12
+ function findDefinition(workspace, linkId) {
13
+ const definition = workspace.getLinkDefinition(linkId);
14
+ if (!definition) return;
15
+ const model = workspace.getModel(definition.file);
16
+ if (!model) return;
17
+ const fileLocation = toFileLocation(model.sourceMap, definition.location);
18
+ return {
19
+ file: definition.file,
20
+ location: fileLocation,
21
+ definition
22
+ };
23
+ }
24
+ /**
25
+ * Find the definition at a given position in a file
26
+ *
27
+ * @param workspace - The workspace to search in
28
+ * @param file - The file path
29
+ * @param offset - The character offset in the file
30
+ * @returns The definition result, or undefined if no link at position or not found
31
+ */
32
+ function findDefinitionAtPosition(workspace, file, offset) {
33
+ const model = workspace.getModel(file);
34
+ if (!model) return;
35
+ const position = positionFromOffset(model.source, offset);
36
+ const context = findNodeAtPosition({ blocks: model.blocks }, position);
37
+ if (context.kind !== "link") return;
38
+ return findDefinition(workspace, context.linkId);
39
+ }
40
+
41
+ //#endregion
42
+ export { findDefinition, findDefinitionAtPosition };
43
+ //# sourceMappingURL=definition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definition.js","names":[],"sources":["../../src/services/definition.ts"],"sourcesContent":["import type { Workspace } from \"../model/workspace.js\";\nimport type { Location } from \"../ast/ast-types.js\";\nimport type { LinkDefinition } from \"../semantic/analyzer.js\";\nimport { toFileLocation, positionFromOffset } from \"../source-map.js\";\nimport { findNodeAtPosition } from \"../ast/node-at-position.js\";\n\n/**\n * Result of a find-definition query\n */\nexport interface DefinitionResult {\n /** The file containing the definition */\n file: string;\n /** Location of the definition (file-absolute) */\n location: Location;\n /** The link definition details */\n definition: LinkDefinition;\n}\n\n/**\n * Find the definition for a link ID\n *\n * @param workspace - The workspace to search in\n * @param linkId - The link ID to find (without ^ prefix)\n * @returns The definition result with file-absolute location, or undefined if not found\n */\nexport function findDefinition(workspace: Workspace, linkId: string): DefinitionResult | undefined {\n const definition = workspace.getLinkDefinition(linkId);\n if (!definition) {\n return undefined;\n }\n\n // Get the model to access the source map\n const model = workspace.getModel(definition.file);\n if (!model) {\n return undefined;\n }\n\n // Convert block-relative location to file-absolute location\n const fileLocation = toFileLocation(model.sourceMap, definition.location);\n\n return {\n file: definition.file,\n location: fileLocation,\n definition,\n };\n}\n\n/**\n * Find the definition at a given position in a file\n *\n * @param workspace - The workspace to search in\n * @param file - The file path\n * @param offset - The character offset in the file\n * @returns The definition result, or undefined if no link at position or not found\n */\nexport function findDefinitionAtPosition(\n workspace: Workspace,\n file: string,\n offset: number,\n): DefinitionResult | undefined {\n const model = workspace.getModel(file);\n if (!model) {\n return undefined;\n }\n\n // Convert offset to position\n const position = positionFromOffset(model.source, offset);\n\n // Use AST-based node detection\n const context = findNodeAtPosition({ blocks: model.blocks }, position);\n\n // Only handle link contexts\n if (context.kind !== \"link\") {\n return undefined;\n }\n\n return findDefinition(workspace, context.linkId);\n}\n"],"mappings":";;;;;;;;;;;AAyBA,SAAgB,eAAe,WAAsB,QAA8C;CACjG,MAAM,aAAa,UAAU,kBAAkB,OAAO;AACtD,KAAI,CAAC,WACH;CAIF,MAAM,QAAQ,UAAU,SAAS,WAAW,KAAK;AACjD,KAAI,CAAC,MACH;CAIF,MAAM,eAAe,eAAe,MAAM,WAAW,WAAW,SAAS;AAEzE,QAAO;EACL,MAAM,WAAW;EACjB,UAAU;EACV;EACD;;;;;;;;;;AAWH,SAAgB,yBACd,WACA,MACA,QAC8B;CAC9B,MAAM,QAAQ,UAAU,SAAS,KAAK;AACtC,KAAI,CAAC,MACH;CAIF,MAAM,WAAW,mBAAmB,MAAM,QAAQ,OAAO;CAGzD,MAAM,UAAU,mBAAmB,EAAE,QAAQ,MAAM,QAAQ,EAAE,SAAS;AAGtE,KAAI,QAAQ,SAAS,OACnB;AAGF,QAAO,eAAe,WAAW,QAAQ,OAAO"}