@rejot-dev/thalo 0.2.4 → 0.2.5

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.
@@ -1,4 +1,4 @@
1
- import { InstanceEntry } from "../../ast/ast-types.js";
1
+ import { InstanceEntry, SchemaEntry } from "../../ast/ast-types.js";
2
2
  import { Workspace } from "../../model/workspace.js";
3
3
  import { Query } from "../query.js";
4
4
  import { TimestampChangeTracker } from "./timestamp-tracker.js";
@@ -88,6 +88,19 @@ interface ChangeTracker {
88
88
  * @returns Changed entries and the current marker to store
89
89
  */
90
90
  getChangedEntries(workspace: Workspace, queries: Query[], marker: ChangeMarker | null): Promise<ChangedEntriesResult>;
91
+ /**
92
+ * Get schema entries that have changed since a marker.
93
+ *
94
+ * For git tracker: Finds schema entries in files modified since the commit
95
+ * and compares entry content to identify changes.
96
+ *
97
+ * For timestamp tracker: Filters schema entries with timestamps after the marker.
98
+ *
99
+ * @param workspace - The workspace containing current entries
100
+ * @param marker - The marker to compare against (from previous actualization)
101
+ * @returns Changed schema entries
102
+ */
103
+ getChangedSchemaEntries?(workspace: Workspace, marker: ChangeMarker | null): Promise<SchemaEntry[]>;
91
104
  }
92
105
  /**
93
106
  * Parse a checkpoint value into a ChangeMarker.
@@ -1 +1 @@
1
- {"version":3,"file":"change-tracker.d.ts","names":[],"sources":["../../../src/services/change-tracker/change-tracker.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AAaA;AAOA;AAUA;AAUA;AAgBA;AAoBiB,KA/DL,kBAAA,GA+DkB,KAAA,GAAA,IAAA;;;;;;AA6BlB,UArFK,YAAA,CAqFL;EACC;EAAR,IAAA,EApFG,kBAoFH;EAAO;EA0BI,KAAA,EAAA,MAAA;AAiChB;;;;UAvIiB,oBAAA;;WAEN;;iBAEM;;;;;UAMA,oBAAA;;;;;;;;;;;;;;;cAgBJ,uBAAA,SAAgC,KAAA;;;;;;;;;;;UAoB5B,aAAA;;;;iBAIA;;;;;;sBAOK,QAAQ;;;;;;;;;;;;;;+BAgBf,oBACF,iBACD,sBACP,QAAQ;;;;;;;;;;;iBA0BG,eAAA,qCAAoD;;;;;;;iBAiCpD,gBAAA,SAAyB"}
1
+ {"version":3,"file":"change-tracker.d.ts","names":[],"sources":["../../../src/services/change-tracker/change-tracker.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AAaA;AAOA;AAUA;AAUA;AAgBA;AAoBiB,KA/DL,kBAAA,GA+DkB,KAAA,GAAA,IAAA;;;;;;AA6BlB,UArFK,YAAA,CAqFL;EACC;EAAR,IAAA,EApFG,kBAoFH;EAeU;EACH,KAAA,EAAA,MAAA;;;;AA2BZ;AAiCgB,UAxJC,oBAAA,CAwJwB;;WAtJ9B;;iBAEM;;;;;UAMA,oBAAA;;;;;;;;;;;;;;;cAgBJ,uBAAA,SAAgC,KAAA;;;;;;;;;;;UAoB5B,aAAA;;;;iBAIA;;;;;;sBAOK,QAAQ;;;;;;;;;;;;;;+BAgBf,oBACF,iBACD,sBACP,QAAQ;;;;;;;;;;;;;sCAeE,mBACH,sBACP,QAAQ;;;;;;;;;;;iBA0BG,eAAA,qCAAoD;;;;;;;iBAiCpD,gBAAA,SAAyB"}
@@ -1 +1 @@
1
- {"version":3,"file":"change-tracker.js","names":[],"sources":["../../../src/services/change-tracker/change-tracker.ts"],"sourcesContent":["import type { InstanceEntry } from \"../../ast/ast-types.js\";\nimport type { Query } from \"../query.js\";\nimport type { Workspace } from \"../../model/workspace.js\";\nimport { TimestampChangeTracker } from \"./timestamp-tracker.js\";\n\n/**\n * Known checkpoint provider types.\n *\n * - \"git\": Git commit hash\n * - \"ts\": ISO timestamp\n *\n * This is extensible - future providers could include \"db\" for database, etc.\n */\nexport type CheckpointProvider = \"git\" | \"ts\";\n\n/**\n * A marker representing a point in time for change tracking.\n *\n * Stored as `checkpoint: \"git:abc123\"` or `checkpoint: \"ts:2026-01-08T15:00Z\"`.\n */\nexport interface ChangeMarker {\n /** Provider type (git, ts, etc.) */\n type: CheckpointProvider;\n /** The marker value - commit hash or ISO timestamp */\n value: string;\n}\n\n/**\n * Result of getting changed entries, including the marker to store\n */\nexport interface ChangedEntriesResult {\n /** Entries that have changed since the marker */\n entries: InstanceEntry[];\n /** The current marker to store for next comparison */\n currentMarker: ChangeMarker;\n}\n\n/**\n * Options for creating a ChangeTracker\n */\nexport interface ChangeTrackerOptions {\n /** Working directory for git operations */\n cwd?: string;\n /**\n * Force operation even if there are uncommitted changes.\n * When false (default), git tracker will error if source files have uncommitted changes.\n */\n force?: boolean;\n}\n\n/**\n * Error thrown when source files have uncommitted changes.\n *\n * This prevents incorrect change tracking since uncommitted changes\n * are not captured in the checkpoint.\n */\nexport class UncommittedChangesError extends Error {\n /** Files with uncommitted changes */\n readonly files: string[];\n\n constructor(files: string[]) {\n super(\n `Source files have uncommitted changes: ${files.join(\", \")}. ` +\n `Commit your changes or use --force to proceed anyway.`,\n );\n this.name = \"UncommittedChangesError\";\n this.files = files;\n }\n}\n\n/**\n * Interface for tracking changes to entries.\n *\n * Implementations can use different backends (git, timestamps, database, etc.)\n * to determine which entries have changed since a marker point.\n */\nexport interface ChangeTracker {\n /**\n * Type of tracker - matches the checkpoint provider type.\n */\n readonly type: CheckpointProvider;\n\n /**\n * Get the current position marker (HEAD commit or current time).\n *\n * @returns Current marker to store for future comparisons\n */\n getCurrentMarker(): Promise<ChangeMarker>;\n\n /**\n * Get entries that have changed since a marker.\n *\n * For git tracker: Finds files modified since the commit, parses them,\n * and compares entry content to identify changed entries.\n *\n * For timestamp tracker: Filters entries with timestamps after the marker.\n *\n * @param workspace - The workspace containing current entries\n * @param queries - Queries to filter which entries to consider\n * @param marker - The marker to compare against (from previous actualization)\n * @returns Changed entries and the current marker to store\n */\n getChangedEntries(\n workspace: Workspace,\n queries: Query[],\n marker: ChangeMarker | null,\n ): Promise<ChangedEntriesResult>;\n}\n\n/**\n * Strip quotes from a string value if present.\n * Handles both single and double quotes.\n */\nfunction stripQuotes(value: string): string {\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n return value.slice(1, -1);\n }\n return value;\n}\n\n/**\n * Parse a checkpoint value into a ChangeMarker.\n *\n * Expected format: \"git:abc123\" or \"ts:2026-01-08T15:00Z\"\n * Strips quotes from the raw value as metadata raw values include quotes.\n *\n * @param checkpointRaw - Raw checkpoint metadata value (may include quotes)\n * @returns Parsed marker or null if invalid/missing\n */\nexport function parseCheckpoint(checkpointRaw: string | undefined): ChangeMarker | null {\n if (!checkpointRaw) {\n return null;\n }\n\n const value = stripQuotes(checkpointRaw);\n const colonIndex = value.indexOf(\":\");\n\n if (colonIndex === -1) {\n return null;\n }\n\n const provider = value.slice(0, colonIndex);\n const markerValue = value.slice(colonIndex + 1);\n\n if (!markerValue) {\n return null;\n }\n\n // Validate known providers\n if (provider === \"git\" || provider === \"ts\") {\n return { type: provider, value: markerValue };\n }\n\n return null;\n}\n\n/**\n * Format a ChangeMarker as a checkpoint string.\n *\n * @param marker - The marker to format\n * @returns Formatted string like \"git:abc123\" or \"ts:2026-01-08T15:00Z\"\n */\nexport function formatCheckpoint(marker: ChangeMarker): string {\n return `${marker.type}:${marker.value}`;\n}\n\nexport { TimestampChangeTracker } from \"./timestamp-tracker.js\";\n\n// For Node.js environments that need git-based tracking, use:\n// import { createChangeTracker, GitChangeTracker } from \"@rejot-dev/thalo/change-tracker/node\";\n"],"mappings":";;;;;;;;;AAwDA,IAAa,0BAAb,cAA6C,MAAM;;CAEjD,AAAS;CAET,YAAY,OAAiB;AAC3B,QACE,0CAA0C,MAAM,KAAK,KAAK,CAAC,yDAE5D;AACD,OAAK,OAAO;AACZ,OAAK,QAAQ;;;;;;;AA+CjB,SAAS,YAAY,OAAuB;AAC1C,KACG,MAAM,WAAW,KAAI,IAAI,MAAM,SAAS,KAAI,IAC5C,MAAM,WAAW,IAAI,IAAI,MAAM,SAAS,IAAI,CAE7C,QAAO,MAAM,MAAM,GAAG,GAAG;AAE3B,QAAO;;;;;;;;;;;AAYT,SAAgB,gBAAgB,eAAwD;AACtF,KAAI,CAAC,cACH,QAAO;CAGT,MAAM,QAAQ,YAAY,cAAc;CACxC,MAAM,aAAa,MAAM,QAAQ,IAAI;AAErC,KAAI,eAAe,GACjB,QAAO;CAGT,MAAM,WAAW,MAAM,MAAM,GAAG,WAAW;CAC3C,MAAM,cAAc,MAAM,MAAM,aAAa,EAAE;AAE/C,KAAI,CAAC,YACH,QAAO;AAIT,KAAI,aAAa,SAAS,aAAa,KACrC,QAAO;EAAE,MAAM;EAAU,OAAO;EAAa;AAG/C,QAAO;;;;;;;;AAST,SAAgB,iBAAiB,QAA8B;AAC7D,QAAO,GAAG,OAAO,KAAK,GAAG,OAAO"}
1
+ {"version":3,"file":"change-tracker.js","names":[],"sources":["../../../src/services/change-tracker/change-tracker.ts"],"sourcesContent":["import type { InstanceEntry, SchemaEntry } from \"../../ast/ast-types.js\";\nimport type { Query } from \"../query.js\";\nimport type { Workspace } from \"../../model/workspace.js\";\nimport { TimestampChangeTracker } from \"./timestamp-tracker.js\";\n\n/**\n * Known checkpoint provider types.\n *\n * - \"git\": Git commit hash\n * - \"ts\": ISO timestamp\n *\n * This is extensible - future providers could include \"db\" for database, etc.\n */\nexport type CheckpointProvider = \"git\" | \"ts\";\n\n/**\n * A marker representing a point in time for change tracking.\n *\n * Stored as `checkpoint: \"git:abc123\"` or `checkpoint: \"ts:2026-01-08T15:00Z\"`.\n */\nexport interface ChangeMarker {\n /** Provider type (git, ts, etc.) */\n type: CheckpointProvider;\n /** The marker value - commit hash or ISO timestamp */\n value: string;\n}\n\n/**\n * Result of getting changed entries, including the marker to store\n */\nexport interface ChangedEntriesResult {\n /** Entries that have changed since the marker */\n entries: InstanceEntry[];\n /** The current marker to store for next comparison */\n currentMarker: ChangeMarker;\n}\n\n/**\n * Options for creating a ChangeTracker\n */\nexport interface ChangeTrackerOptions {\n /** Working directory for git operations */\n cwd?: string;\n /**\n * Force operation even if there are uncommitted changes.\n * When false (default), git tracker will error if source files have uncommitted changes.\n */\n force?: boolean;\n}\n\n/**\n * Error thrown when source files have uncommitted changes.\n *\n * This prevents incorrect change tracking since uncommitted changes\n * are not captured in the checkpoint.\n */\nexport class UncommittedChangesError extends Error {\n /** Files with uncommitted changes */\n readonly files: string[];\n\n constructor(files: string[]) {\n super(\n `Source files have uncommitted changes: ${files.join(\", \")}. ` +\n `Commit your changes or use --force to proceed anyway.`,\n );\n this.name = \"UncommittedChangesError\";\n this.files = files;\n }\n}\n\n/**\n * Interface for tracking changes to entries.\n *\n * Implementations can use different backends (git, timestamps, database, etc.)\n * to determine which entries have changed since a marker point.\n */\nexport interface ChangeTracker {\n /**\n * Type of tracker - matches the checkpoint provider type.\n */\n readonly type: CheckpointProvider;\n\n /**\n * Get the current position marker (HEAD commit or current time).\n *\n * @returns Current marker to store for future comparisons\n */\n getCurrentMarker(): Promise<ChangeMarker>;\n\n /**\n * Get entries that have changed since a marker.\n *\n * For git tracker: Finds files modified since the commit, parses them,\n * and compares entry content to identify changed entries.\n *\n * For timestamp tracker: Filters entries with timestamps after the marker.\n *\n * @param workspace - The workspace containing current entries\n * @param queries - Queries to filter which entries to consider\n * @param marker - The marker to compare against (from previous actualization)\n * @returns Changed entries and the current marker to store\n */\n getChangedEntries(\n workspace: Workspace,\n queries: Query[],\n marker: ChangeMarker | null,\n ): Promise<ChangedEntriesResult>;\n\n /**\n * Get schema entries that have changed since a marker.\n *\n * For git tracker: Finds schema entries in files modified since the commit\n * and compares entry content to identify changes.\n *\n * For timestamp tracker: Filters schema entries with timestamps after the marker.\n *\n * @param workspace - The workspace containing current entries\n * @param marker - The marker to compare against (from previous actualization)\n * @returns Changed schema entries\n */\n getChangedSchemaEntries?(\n workspace: Workspace,\n marker: ChangeMarker | null,\n ): Promise<SchemaEntry[]>;\n}\n\n/**\n * Strip quotes from a string value if present.\n * Handles both single and double quotes.\n */\nfunction stripQuotes(value: string): string {\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n return value.slice(1, -1);\n }\n return value;\n}\n\n/**\n * Parse a checkpoint value into a ChangeMarker.\n *\n * Expected format: \"git:abc123\" or \"ts:2026-01-08T15:00Z\"\n * Strips quotes from the raw value as metadata raw values include quotes.\n *\n * @param checkpointRaw - Raw checkpoint metadata value (may include quotes)\n * @returns Parsed marker or null if invalid/missing\n */\nexport function parseCheckpoint(checkpointRaw: string | undefined): ChangeMarker | null {\n if (!checkpointRaw) {\n return null;\n }\n\n const value = stripQuotes(checkpointRaw);\n const colonIndex = value.indexOf(\":\");\n\n if (colonIndex === -1) {\n return null;\n }\n\n const provider = value.slice(0, colonIndex);\n const markerValue = value.slice(colonIndex + 1);\n\n if (!markerValue) {\n return null;\n }\n\n // Validate known providers\n if (provider === \"git\" || provider === \"ts\") {\n return { type: provider, value: markerValue };\n }\n\n return null;\n}\n\n/**\n * Format a ChangeMarker as a checkpoint string.\n *\n * @param marker - The marker to format\n * @returns Formatted string like \"git:abc123\" or \"ts:2026-01-08T15:00Z\"\n */\nexport function formatCheckpoint(marker: ChangeMarker): string {\n return `${marker.type}:${marker.value}`;\n}\n\nexport { TimestampChangeTracker } from \"./timestamp-tracker.js\";\n\n// For Node.js environments that need git-based tracking, use:\n// import { createChangeTracker, GitChangeTracker } from \"@rejot-dev/thalo/change-tracker/node\";\n"],"mappings":";;;;;;;;;AAwDA,IAAa,0BAAb,cAA6C,MAAM;;CAEjD,AAAS;CAET,YAAY,OAAiB;AAC3B,QACE,0CAA0C,MAAM,KAAK,KAAK,CAAC,yDAE5D;AACD,OAAK,OAAO;AACZ,OAAK,QAAQ;;;;;;;AAgEjB,SAAS,YAAY,OAAuB;AAC1C,KACG,MAAM,WAAW,KAAI,IAAI,MAAM,SAAS,KAAI,IAC5C,MAAM,WAAW,IAAI,IAAI,MAAM,SAAS,IAAI,CAE7C,QAAO,MAAM,MAAM,GAAG,GAAG;AAE3B,QAAO;;;;;;;;;;;AAYT,SAAgB,gBAAgB,eAAwD;AACtF,KAAI,CAAC,cACH,QAAO;CAGT,MAAM,QAAQ,YAAY,cAAc;CACxC,MAAM,aAAa,MAAM,QAAQ,IAAI;AAErC,KAAI,eAAe,GACjB,QAAO;CAGT,MAAM,WAAW,MAAM,MAAM,GAAG,WAAW;CAC3C,MAAM,cAAc,MAAM,MAAM,aAAa,EAAE;AAE/C,KAAI,CAAC,YACH,QAAO;AAIT,KAAI,aAAa,SAAS,aAAa,KACrC,QAAO;EAAE,MAAM;EAAU,OAAO;EAAa;AAG/C,QAAO;;;;;;;;AAST,SAAgB,iBAAiB,QAA8B;AAC7D,QAAO,GAAG,OAAO,KAAK,GAAG,OAAO"}
@@ -1,3 +1,4 @@
1
+ import { SchemaEntry } from "../../ast/ast-types.js";
1
2
  import { Workspace } from "../../model/workspace.js";
2
3
  import { Query } from "../query.js";
3
4
  import { ChangeMarker, ChangeTracker, ChangeTrackerOptions, ChangedEntriesResult } from "./change-tracker.js";
@@ -27,14 +28,23 @@ declare class GitChangeTracker implements ChangeTracker {
27
28
  private getIgnoreRevs;
28
29
  getCurrentMarker(): Promise<ChangeMarker>;
29
30
  getChangedEntries(workspace: Workspace, queries: Query[], marker: ChangeMarker | null): Promise<ChangedEntriesResult>;
31
+ getChangedSchemaEntries(workspace: Workspace, marker: ChangeMarker | null): Promise<SchemaEntry[]>;
30
32
  /**
31
33
  * Get all files that contain entries matching the queries.
32
34
  */
33
35
  private getSourceFiles;
36
+ /**
37
+ * Get all files that contain schema entries.
38
+ */
39
+ private getSchemaSourceFiles;
34
40
  /**
35
41
  * Get all entries matching the queries (for first run or fallback)
36
42
  */
37
43
  private getAllMatchingEntries;
44
+ /**
45
+ * Get all schema entries in the workspace.
46
+ */
47
+ private getAllSchemaEntries;
38
48
  /**
39
49
  * Get changed entries in a specific file.
40
50
  *
@@ -44,6 +54,10 @@ declare class GitChangeTracker implements ChangeTracker {
44
54
  * and therefore don't need to be synthesized.
45
55
  */
46
56
  private getChangedEntriesInFile;
57
+ /**
58
+ * Get changed schema entries in a specific file.
59
+ */
60
+ private getChangedSchemaEntriesInFile;
47
61
  /**
48
62
  * Find a model in the workspace by relative path.
49
63
  * Normalizes path separators for cross-platform compatibility.
@@ -1 +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"}
1
+ {"version":3,"file":"git-tracker.d.ts","names":[],"sources":["../../../src/services/change-tracker/git-tracker.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AA0CA;;;;;;;;;;;;AA2HK,cA3HQ,gBAAA,YAA4B,aA2HpC,CAAA;EA3HoC,SAAA,IAAA,EAAA,KAAA;EAAa,QAAA,GAAA;;;wBAM/B;;sBAaK,QAAQ;+BAYrB,oBACF,iBACD,sBACP,QAAQ;qCAuFE,mBACH,sBACP,QAAQ"}
@@ -85,6 +85,31 @@ var GitChangeTracker = class {
85
85
  currentMarker
86
86
  };
87
87
  }
88
+ async getChangedSchemaEntries(workspace, marker) {
89
+ if (!(await detectGitContext(this.cwd)).isGitRepo) throw new Error("Not in a git repository");
90
+ const sourceFiles = this.getSchemaSourceFiles(workspace);
91
+ if (!this.force && sourceFiles.length > 0) {
92
+ const uncommitted = await getUncommittedFiles(this.cwd, sourceFiles);
93
+ if (uncommitted.length > 0) throw new UncommittedChangesError(uncommitted);
94
+ }
95
+ if (!marker || marker.type === "ts") return this.getAllSchemaEntries(workspace);
96
+ if (!await commitExists(marker.value, this.cwd)) return this.getAllSchemaEntries(workspace);
97
+ const thaloChanges = (await getFilesChangedSince(marker.value, this.cwd)).filter((f) => f.path.endsWith(".thalo") || f.path.endsWith(".md"));
98
+ if (thaloChanges.length === 0) return [];
99
+ const changedEntries = [];
100
+ const seenKeys = /* @__PURE__ */ new Set();
101
+ for (const change of thaloChanges) {
102
+ const entries = await this.getChangedSchemaEntriesInFile(workspace, change, marker.value);
103
+ for (const entry of entries) {
104
+ const key = `${change.path}:${serializeIdentity(getEntryIdentity(entry))}`;
105
+ if (!seenKeys.has(key)) {
106
+ seenKeys.add(key);
107
+ changedEntries.push(entry);
108
+ }
109
+ }
110
+ }
111
+ return changedEntries;
112
+ }
88
113
  /**
89
114
  * Get all files that contain entries matching the queries.
90
115
  */
@@ -100,6 +125,17 @@ var GitChangeTracker = class {
100
125
  return Array.from(files);
101
126
  }
102
127
  /**
128
+ * Get all files that contain schema entries.
129
+ */
130
+ getSchemaSourceFiles(workspace) {
131
+ const files = /* @__PURE__ */ new Set();
132
+ for (const model of workspace.allModels()) for (const entry of model.ast.entries) if (entry.type === "schema_entry") {
133
+ files.add(model.file);
134
+ break;
135
+ }
136
+ return Array.from(files);
137
+ }
138
+ /**
103
139
  * Get all entries matching the queries (for first run or fallback)
104
140
  */
105
141
  getAllMatchingEntries(workspace, queries) {
@@ -118,6 +154,21 @@ var GitChangeTracker = class {
118
154
  return results;
119
155
  }
120
156
  /**
157
+ * Get all schema entries in the workspace.
158
+ */
159
+ getAllSchemaEntries(workspace) {
160
+ const results = [];
161
+ const seen = /* @__PURE__ */ new Set();
162
+ for (const model of workspace.allModels()) for (const entry of model.ast.entries) {
163
+ if (entry.type !== "schema_entry") continue;
164
+ const key = `${model.file}:${serializeIdentity(getEntryIdentity(entry))}`;
165
+ if (seen.has(key)) continue;
166
+ seen.add(key);
167
+ results.push(entry);
168
+ }
169
+ return results;
170
+ }
171
+ /**
121
172
  * Get changed entries in a specific file.
122
173
  *
123
174
  * Note: This method only returns entries that exist in the current version of the file.
@@ -189,6 +240,61 @@ var GitChangeTracker = class {
189
240
  return changed;
190
241
  }
191
242
  /**
243
+ * Get changed schema entries in a specific file.
244
+ */
245
+ async getChangedSchemaEntriesInFile(workspace, change, markerCommit) {
246
+ const model = this.findModelByRelativePath(workspace, change.path);
247
+ if (!model) return [];
248
+ const currentEntries = model.ast.entries.filter((e) => e.type === "schema_entry");
249
+ const ignoreRevs = await this.getIgnoreRevs();
250
+ if (ignoreRevs) {
251
+ const changed$1 = [];
252
+ function locationToLineRange(entry) {
253
+ const startRow = entry.location.startPosition.row;
254
+ const endRow = entry.location.endPosition.row;
255
+ const endCol = entry.location.endPosition.column;
256
+ const startLine = startRow + 1;
257
+ const endLine = endCol === 0 ? endRow : endRow + 1;
258
+ return {
259
+ startLine,
260
+ endLine: Math.max(startLine, endLine)
261
+ };
262
+ }
263
+ for (const entry of currentEntries) {
264
+ const { startLine, endLine } = locationToLineRange(entry);
265
+ const blamedCommits = await getBlameCommitsForLineRange(change.path, startLine, endLine, this.cwd, ignoreRevs);
266
+ let isChanged = false;
267
+ for (const commit of blamedCommits) if (!await isCommitAncestorOf(commit, markerCommit, this.cwd)) {
268
+ isChanged = true;
269
+ break;
270
+ }
271
+ if (isChanged) changed$1.push(entry);
272
+ }
273
+ return changed$1;
274
+ }
275
+ const pathAtCommit = change.oldPath ?? change.path;
276
+ const oldContent = await getFileAtCommit(pathAtCommit, markerCommit, this.cwd);
277
+ const oldEntryMap = /* @__PURE__ */ new Map();
278
+ if (oldContent) try {
279
+ const oldDoc = parseDocument(oldContent, { fileType: pathAtCommit.endsWith(".md") ? "markdown" : "thalo" });
280
+ for (const block of oldDoc.blocks) {
281
+ const oldAst = extractSourceFile(block.tree.rootNode);
282
+ for (const entry of oldAst.entries) {
283
+ if (entry.type !== "schema_entry") continue;
284
+ const key = serializeIdentity(getEntryIdentity(entry));
285
+ oldEntryMap.set(key, entry);
286
+ }
287
+ }
288
+ } catch {}
289
+ const changed = [];
290
+ for (const entry of currentEntries) {
291
+ const key = serializeIdentity(getEntryIdentity(entry));
292
+ const oldEntry = oldEntryMap.get(key);
293
+ if (!oldEntry || !entriesEqual(oldEntry, entry)) changed.push(entry);
294
+ }
295
+ return changed;
296
+ }
297
+ /**
192
298
  * Find a model in the workspace by relative path.
193
299
  * Normalizes path separators for cross-platform compatibility.
194
300
  *
@@ -1 +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.node.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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"}
1
+ {"version":3,"file":"git-tracker.js","names":["changedEntries: InstanceEntry[]","changedEntries: SchemaEntry[]","results: InstanceEntry[]","results: SchemaEntry[]","changed: InstanceEntry[]","changed","changed: SchemaEntry[]"],"sources":["../../../src/services/change-tracker/git-tracker.ts"],"sourcesContent":["import type { Entry, InstanceEntry, SchemaEntry } 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.node.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 async getChangedSchemaEntries(\n workspace: Workspace,\n marker: ChangeMarker | null,\n ): Promise<SchemaEntry[]> {\n const gitContext = await detectGitContext(this.cwd);\n if (!gitContext.isGitRepo) {\n throw new Error(\"Not in a git repository\");\n }\n\n // Check for uncommitted changes in schema source files (unless force is set)\n const sourceFiles = this.getSchemaSourceFiles(workspace);\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 or switching from timestamp, return all schema entries\n if (!marker || marker.type === \"ts\") {\n return this.getAllSchemaEntries(workspace);\n }\n\n const exists = await commitExists(marker.value, this.cwd);\n if (!exists) {\n return this.getAllSchemaEntries(workspace);\n }\n\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 (thaloChanges.length === 0) {\n return [];\n }\n\n const changedEntries: SchemaEntry[] = [];\n const seenKeys = new Set<string>();\n\n for (const change of thaloChanges) {\n const entries = await this.getChangedSchemaEntriesInFile(workspace, change, marker.value);\n\n for (const entry of entries) {\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 changedEntries;\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 files that contain schema entries.\n */\n private getSchemaSourceFiles(workspace: Workspace): 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 === \"schema_entry\") {\n files.add(model.file);\n break;\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 all schema entries in the workspace.\n */\n private getAllSchemaEntries(workspace: Workspace): SchemaEntry[] {\n const results: SchemaEntry[] = [];\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 !== \"schema_entry\") {\n continue;\n }\n\n const key = `${model.file}:${serializeIdentity(getEntryIdentity(entry))}`;\n if (seen.has(key)) {\n continue;\n }\n\n seen.add(key);\n results.push(entry);\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 * Get changed schema entries in a specific file.\n */\n private async getChangedSchemaEntriesInFile(\n workspace: Workspace,\n change: FileChange,\n markerCommit: string,\n ): Promise<SchemaEntry[]> {\n const model = this.findModelByRelativePath(workspace, change.path);\n if (!model) {\n return [];\n }\n\n const currentEntries = model.ast.entries.filter(\n (e): e is SchemaEntry => e.type === \"schema_entry\",\n );\n\n const ignoreRevs = await this.getIgnoreRevs();\n if (ignoreRevs) {\n const changed: SchemaEntry[] = [];\n\n function locationToLineRange(entry: SchemaEntry): { 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 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 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 const pathAtCommit = change.oldPath ?? change.path;\n const oldContent = await getFileAtCommit(pathAtCommit, markerCommit, this.cwd);\n\n const oldEntryMap = new Map<string, Entry>();\n if (oldContent) {\n try {\n const fileType = pathAtCommit.endsWith(\".md\") ? \"markdown\" : \"thalo\";\n const oldDoc = parseDocument(oldContent, { fileType });\n\n for (const block of oldDoc.blocks) {\n const oldAst = extractSourceFile(block.tree.rootNode);\n for (const entry of oldAst.entries) {\n if (entry.type !== \"schema_entry\") {\n continue;\n }\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 const changed: SchemaEntry[] = [];\n\n for (const entry of currentEntries) {\n const key = serializeIdentity(getEntryIdentity(entry));\n const oldEntry = oldEntryMap.get(key);\n\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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"}
@@ -1,3 +1,4 @@
1
+ import { SchemaEntry } from "../../ast/ast-types.js";
1
2
  import { Workspace } from "../../model/workspace.js";
2
3
  import { Query } from "../query.js";
3
4
  import { ChangeMarker, ChangeTracker, ChangedEntriesResult } from "./change-tracker.js";
@@ -16,6 +17,7 @@ declare class TimestampChangeTracker implements ChangeTracker {
16
17
  readonly type: "ts";
17
18
  getCurrentMarker(): Promise<ChangeMarker>;
18
19
  getChangedEntries(workspace: Workspace, queries: Query[], marker: ChangeMarker | null): Promise<ChangedEntriesResult>;
20
+ getChangedSchemaEntries(workspace: Workspace, marker: ChangeMarker | null): Promise<SchemaEntry[]>;
19
21
  }
20
22
  //#endregion
21
23
  export { TimestampChangeTracker };
@@ -1 +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"}
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;EAsDU,gBAAA,CAAA,CAAA,EAjEa,OAiEb,CAjEqB,YAiErB,CAAA;EACH,iBAAA,CAAA,SAAA,EA1DG,SA0DH,EAAA,OAAA,EAzDC,KAyDD,EAAA,EAAA,MAAA,EAxDA,YAwDA,GAAA,IAAA,CAAA,EAvDP,OAuDO,CAvDC,oBAuDD,CAAA;EACC,uBAAA,CAAA,SAAA,EAFE,SAEF,EAAA,MAAA,EADD,YACC,GAAA,IAAA,CAAA,EAAR,OAAQ,CAAA,WAAA,EAAA,CAAA"}
@@ -67,6 +67,29 @@ var TimestampChangeTracker = class {
67
67
  currentMarker: await this.getCurrentMarker()
68
68
  };
69
69
  }
70
+ async getChangedSchemaEntries(workspace, marker) {
71
+ const afterTimestamp = marker?.type === "ts" ? marker.value : null;
72
+ const afterEpoch = afterTimestamp ? parseTimestampToEpoch(afterTimestamp) : null;
73
+ const results = [];
74
+ const seen = /* @__PURE__ */ new Set();
75
+ for (const model of workspace.allModels()) for (const entry of model.ast.entries) {
76
+ if (entry.type !== "schema_entry") continue;
77
+ const timestampEpoch = timestampToEpoch(entry.header.timestamp);
78
+ const timestampStr = formatTimestamp(entry.header.timestamp);
79
+ const entityName = entry.header.entityName?.value ?? "unknown";
80
+ const directive = entry.header.directive ?? "define-entity";
81
+ const key = `${model.file}:${timestampStr}:${directive}:${entityName}`;
82
+ if (seen.has(key)) continue;
83
+ if (afterEpoch !== null && timestampEpoch <= afterEpoch) continue;
84
+ results.push({
85
+ entry,
86
+ timestampEpoch
87
+ });
88
+ seen.add(key);
89
+ }
90
+ results.sort((a, b) => a.timestampEpoch - b.timestampEpoch);
91
+ return results.map((r) => r.entry);
92
+ }
70
93
  };
71
94
 
72
95
  //#endregion
@@ -1 +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"}
1
+ {"version":3,"file":"timestamp-tracker.js","names":["results: { entry: InstanceEntry; timestampEpoch: number }[]","results: { entry: SchemaEntry; timestampEpoch: number }[]"],"sources":["../../../src/services/change-tracker/timestamp-tracker.ts"],"sourcesContent":["import type { Timestamp, InstanceEntry, SchemaEntry } 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 async getChangedSchemaEntries(\n workspace: Workspace,\n marker: ChangeMarker | null,\n ): Promise<SchemaEntry[]> {\n const afterTimestamp = marker?.type === \"ts\" ? marker.value : null;\n const afterEpoch = afterTimestamp ? parseTimestampToEpoch(afterTimestamp) : null;\n\n const results: { entry: SchemaEntry; 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 !== \"schema_entry\") {\n continue;\n }\n\n const timestampEpoch = timestampToEpoch(entry.header.timestamp);\n const timestampStr = formatTimestamp(entry.header.timestamp);\n const entityName = entry.header.entityName?.value ?? \"unknown\";\n const directive = entry.header.directive ?? \"define-entity\";\n const key = `${model.file}:${timestampStr}:${directive}:${entityName}`;\n\n if (seen.has(key)) {\n continue;\n }\n\n if (afterEpoch !== null && timestampEpoch <= afterEpoch) {\n continue;\n }\n\n results.push({ entry, timestampEpoch });\n seen.add(key);\n }\n }\n\n results.sort((a, b) => a.timestampEpoch - b.timestampEpoch);\n return results.map((r) => r.entry);\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;;CAGH,MAAM,wBACJ,WACA,QACwB;EACxB,MAAM,iBAAiB,QAAQ,SAAS,OAAO,OAAO,QAAQ;EAC9D,MAAM,aAAa,iBAAiB,sBAAsB,eAAe,GAAG;EAE5E,MAAMC,UAA4D,EAAE;EACpE,MAAM,uBAAO,IAAI,KAAa;AAE9B,OAAK,MAAM,SAAS,UAAU,WAAW,CACvC,MAAK,MAAM,SAAS,MAAM,IAAI,SAAS;AACrC,OAAI,MAAM,SAAS,eACjB;GAGF,MAAM,iBAAiB,iBAAiB,MAAM,OAAO,UAAU;GAC/D,MAAM,eAAe,gBAAgB,MAAM,OAAO,UAAU;GAC5D,MAAM,aAAa,MAAM,OAAO,YAAY,SAAS;GACrD,MAAM,YAAY,MAAM,OAAO,aAAa;GAC5C,MAAM,MAAM,GAAG,MAAM,KAAK,GAAG,aAAa,GAAG,UAAU,GAAG;AAE1D,OAAI,KAAK,IAAI,IAAI,CACf;AAGF,OAAI,eAAe,QAAQ,kBAAkB,WAC3C;AAGF,WAAQ,KAAK;IAAE;IAAO;IAAgB,CAAC;AACvC,QAAK,IAAI,IAAI;;AAIjB,UAAQ,MAAM,GAAG,MAAM,EAAE,iBAAiB,EAAE,eAAe;AAC3D,SAAO,QAAQ,KAAK,MAAM,EAAE,MAAM"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rejot-dev/thalo",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -103,7 +103,7 @@
103
103
  "main": "./dist/mod.js",
104
104
  "types": "./dist/mod.d.ts",
105
105
  "dependencies": {
106
- "@rejot-dev/tree-sitter-thalo": "0.2.4"
106
+ "@rejot-dev/tree-sitter-thalo": "0.2.5"
107
107
  },
108
108
  "peerDependencies": {
109
109
  "web-tree-sitter": "^0.25.0"