@gmickel/gno 1.34.4 → 1.34.6

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 (37) hide show
  1. package/README.md +11 -1
  2. package/THIRD_PARTY_NOTICES.md +16 -0
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.34.4.zip → gno-browser-clipper-v1.34.6.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.34.6.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +5 -1
  7. package/spec/cli.md +16 -10
  8. package/src/cli/pager.ts +29 -14
  9. package/src/ingestion/sync.ts +368 -84
  10. package/src/serve/public/globals.built.css +1 -1
  11. package/src/serve/watch-reconciliation-fallback-disk.ts +220 -0
  12. package/src/serve/watch-reconciliation-fallback.ts +404 -0
  13. package/src/serve/watch-reconciliation-shared.ts +343 -0
  14. package/src/serve/watch-reconciliation.ts +122 -0
  15. package/src/serve/watch-service-events.ts +261 -0
  16. package/src/serve/watch-service-flush-generation.ts +140 -0
  17. package/src/serve/watch-service-flush-helpers.ts +147 -0
  18. package/src/serve/watch-service-flush.ts +433 -0
  19. package/src/serve/watch-service-hosts.ts +109 -0
  20. package/src/serve/watch-service-lifecycle.ts +219 -0
  21. package/src/serve/watch-service-run-flush.ts +236 -0
  22. package/src/serve/watch-service-snapshot.ts +101 -0
  23. package/src/serve/watch-service-state.ts +146 -0
  24. package/src/serve/watch-service.ts +265 -306
  25. package/src/serve/watch-snapshot-handles.ts +285 -0
  26. package/src/serve/watch-snapshot-libc.ts +391 -0
  27. package/src/serve/watch-snapshot-ops.ts +399 -0
  28. package/src/serve/watch-snapshot-resolve.ts +246 -0
  29. package/src/serve/watch-snapshot-scan.ts +297 -0
  30. package/src/serve/watch-snapshot-types.ts +350 -0
  31. package/src/serve/watch-snapshot.ts +51 -0
  32. package/src/store/index.ts +1 -1
  33. package/src/store/sqlite/adapter.ts +191 -0
  34. package/src/store/types.ts +66 -0
  35. package/vendor/fts5-snowball/README.md +5 -1
  36. package/vendor/fts5-snowball/darwin-x64/fts5stemmer.dylib +0 -0
  37. package/browser-extension/artifacts/gno-browser-clipper-v1.34.4.zip.sha256 +0 -1
@@ -211,6 +211,51 @@ const SINGLE_LINE_QUERY_PATTERN = /[\r\n]/;
211
211
  const DOUBLE_QUOTE_PATTERN = /"/g;
212
212
  const DOC_EDGE_TYPE_PATTERN = /^[a-z][a-z0-9_]*$/;
213
213
  const SQLITE_SAFE_PARAMETER_BATCH_SIZE = 900;
214
+
215
+ /**
216
+ * Effective physical source path for watcher fallback queries.
217
+ * Record-container logical rows resolve to their source container path.
218
+ */
219
+ const WATCHER_SOURCE_PATH_SQL =
220
+ "COALESCE(NULLIF(record_source_path, ''), rel_path)";
221
+
222
+ /**
223
+ * Parent directory of the effective source path (POSIX), with the collection
224
+ * root represented as the empty string.
225
+ */
226
+ const WATCHER_SOURCE_PARENT_SQL = `CASE WHEN instr(${WATCHER_SOURCE_PATH_SQL}, '/') = 0 THEN '' ELSE substr(${WATCHER_SOURCE_PATH_SQL}, 1, length(rtrim(${WATCHER_SOURCE_PATH_SQL}, replace(${WATCHER_SOURCE_PATH_SQL}, '/', ''))) - 1) END`;
227
+
228
+ /**
229
+ * Normalize a collection-relative directory argument for watcher source-path
230
+ * queries. Returns null for absolute, drive-shaped, or escaping paths.
231
+ */
232
+ function normalizeWatcherSourceDirRelPath(dirRelPath: string): string | null {
233
+ const normalized = dirRelPath.replaceAll("\\", "/");
234
+ if (normalized.startsWith("/")) {
235
+ return null;
236
+ }
237
+ const segments: string[] = [];
238
+ for (const segment of normalized.split("/")) {
239
+ if (segment === "" || segment === ".") {
240
+ continue;
241
+ }
242
+ if (segment === "..") {
243
+ return null;
244
+ }
245
+ segments.push(segment);
246
+ }
247
+ const canonical = segments.join("/");
248
+ // `C:` / `C:/foo` after stripping `.` is a Windows absolute escape.
249
+ // A single segment like `a:notes` (no slash after the colon) stays legal.
250
+ if (
251
+ /^[A-Za-z]:(\/|$)/.test(canonical) &&
252
+ (canonical.length === 2 || canonical[2] === "/")
253
+ ) {
254
+ return null;
255
+ }
256
+ return canonical;
257
+ }
258
+
214
259
  const FTS5_FIELD_WEIGHTS = {
215
260
  filepath: 1.5,
216
261
  title: 4.0,
@@ -1664,6 +1709,152 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1664
1709
  }
1665
1710
  }
1666
1711
 
1712
+ async listActiveDirectChildSourcePaths(
1713
+ collection: string,
1714
+ dirRelPath: string,
1715
+ max: number
1716
+ ): Promise<StoreResult<string[]>> {
1717
+ if (!Number.isInteger(max) || max <= 0) {
1718
+ return err("INVALID_INPUT", "max must be a positive integer");
1719
+ }
1720
+ const parentPath = normalizeWatcherSourceDirRelPath(dirRelPath);
1721
+ if (parentPath === null) {
1722
+ return err(
1723
+ "INVALID_INPUT",
1724
+ `Directory path escapes the collection root: ${dirRelPath}`
1725
+ );
1726
+ }
1727
+
1728
+ try {
1729
+ const db = this.ensureOpen();
1730
+ // Effective source path: record containers resolve to physical container.
1731
+ // Parent key: empty string for root-level sources.
1732
+ // LIMIT max+1 detects overflow without returning a truncated success.
1733
+ const rows = db
1734
+ .query<{ source_path: string }, [string, string, number]>(
1735
+ `SELECT DISTINCT ${WATCHER_SOURCE_PATH_SQL} AS source_path
1736
+ FROM documents
1737
+ WHERE collection = ?
1738
+ AND active = 1
1739
+ AND ${WATCHER_SOURCE_PARENT_SQL} = ?
1740
+ ORDER BY source_path ASC
1741
+ LIMIT ?`
1742
+ )
1743
+ .all(collection, parentPath, max + 1);
1744
+
1745
+ if (rows.length > max) {
1746
+ return err(
1747
+ "OVERFLOW",
1748
+ `Active direct-child source paths exceed max=${max}`
1749
+ );
1750
+ }
1751
+ return ok(rows.map((row) => row.source_path));
1752
+ } catch (cause) {
1753
+ return err(
1754
+ "QUERY_FAILED",
1755
+ cause instanceof Error
1756
+ ? cause.message
1757
+ : "Failed to list active direct child source paths",
1758
+ cause
1759
+ );
1760
+ }
1761
+ }
1762
+
1763
+ async listActiveDescendantSourcePaths(
1764
+ collection: string,
1765
+ dirRelPath: string,
1766
+ max: number
1767
+ ): Promise<StoreResult<string[]>> {
1768
+ if (!Number.isInteger(max) || max <= 0) {
1769
+ return err("INVALID_INPUT", "max must be a positive integer");
1770
+ }
1771
+ const directory = normalizeWatcherSourceDirRelPath(dirRelPath);
1772
+ if (directory === null) {
1773
+ return err(
1774
+ "INVALID_INPUT",
1775
+ `Directory path escapes the collection root: ${dirRelPath}`
1776
+ );
1777
+ }
1778
+ if (directory === "") {
1779
+ return err(
1780
+ "INVALID_INPUT",
1781
+ "Descendant lookup requires a directory below the collection root"
1782
+ );
1783
+ }
1784
+
1785
+ try {
1786
+ const db = this.ensureOpen();
1787
+ // Exact prefix boundary: `dir1/` never matches `dir10/...`.
1788
+ // length(?) is code-point-safe (JS prefix.length is UTF-16 and breaks non-BMP).
1789
+ // LIMIT max+1 detects overflow without returning a truncated success.
1790
+ const prefix = `${directory}/`;
1791
+ const rows = db
1792
+ .query<{ source_path: string }, [string, string, string, number]>(
1793
+ `SELECT DISTINCT ${WATCHER_SOURCE_PATH_SQL} AS source_path
1794
+ FROM documents
1795
+ WHERE collection = ?
1796
+ AND active = 1
1797
+ AND substr(${WATCHER_SOURCE_PATH_SQL}, 1, length(?)) = ?
1798
+ ORDER BY source_path ASC
1799
+ LIMIT ?`
1800
+ )
1801
+ .all(collection, prefix, prefix, max + 1);
1802
+
1803
+ if (rows.length > max) {
1804
+ return err(
1805
+ "OVERFLOW",
1806
+ `Active descendant source paths exceed max=${max}`
1807
+ );
1808
+ }
1809
+ return ok(rows.map((row) => row.source_path));
1810
+ } catch (cause) {
1811
+ return err(
1812
+ "QUERY_FAILED",
1813
+ cause instanceof Error
1814
+ ? cause.message
1815
+ : "Failed to list active descendant source paths",
1816
+ cause
1817
+ );
1818
+ }
1819
+ }
1820
+
1821
+ async listActiveSourcePaths(
1822
+ collection: string,
1823
+ max: number
1824
+ ): Promise<StoreResult<string[]>> {
1825
+ if (!Number.isInteger(max) || max <= 0) {
1826
+ return err("INVALID_INPUT", "max must be a positive integer");
1827
+ }
1828
+
1829
+ try {
1830
+ const db = this.ensureOpen();
1831
+ // Root-wide DISTINCT physical sources; overflow after collapse (max+1).
1832
+ const rows = db
1833
+ .query<{ source_path: string }, [string, number]>(
1834
+ `SELECT DISTINCT ${WATCHER_SOURCE_PATH_SQL} AS source_path
1835
+ FROM documents
1836
+ WHERE collection = ?
1837
+ AND active = 1
1838
+ ORDER BY source_path ASC
1839
+ LIMIT ?`
1840
+ )
1841
+ .all(collection, max + 1);
1842
+
1843
+ if (rows.length > max) {
1844
+ return err("OVERFLOW", `Active source paths exceed max=${max}`);
1845
+ }
1846
+ return ok(rows.map((row) => row.source_path));
1847
+ } catch (cause) {
1848
+ return err(
1849
+ "QUERY_FAILED",
1850
+ cause instanceof Error
1851
+ ? cause.message
1852
+ : "Failed to list active source paths",
1853
+ cause
1854
+ );
1855
+ }
1856
+ }
1857
+
1667
1858
  async listActiveDocumentsForBrowse(
1668
1859
  collection?: string
1669
1860
  ): Promise<StoreResult<DocumentRow[]>> {
@@ -24,6 +24,13 @@ import type {
24
24
  // Error Types
25
25
  // ─────────────────────────────────────────────────────────────────────────────
26
26
 
27
+ /**
28
+ * Fixed service default maximum for watcher active source-path queries.
29
+ * Callers (watcher fallback later) pass this explicitly; methods never invent
30
+ * an unbounded success path.
31
+ */
32
+ export const WATCHER_ACTIVE_SOURCE_PATH_MAX = 100_000;
33
+
27
34
  /** Store error codes */
28
35
  export type StoreErrorCode =
29
36
  | "NOT_FOUND"
@@ -38,6 +45,8 @@ export type StoreErrorCode =
38
45
  | "IO_ERROR"
39
46
  | "INTERNAL"
40
47
  | "EGRESS_DENIED"
48
+ /** Result set exceeded the caller-supplied positive maximum (never truncated). */
49
+ | "OVERFLOW"
41
50
  // Vector-specific error codes (EPIC 7)
42
51
  | "VECTOR_WRITE_FAILED"
43
52
  | "VECTOR_DELETE_FAILED"
@@ -1688,6 +1697,63 @@ export interface StorePort {
1688
1697
  sourcePath: string
1689
1698
  ): Promise<StoreResult<DocumentRow[]>>;
1690
1699
 
1700
+ /**
1701
+ * List distinct effective source paths of ACTIVE documents that are direct
1702
+ * children of `dirRelPath` within `collection`.
1703
+ *
1704
+ * Effective source path is `COALESCE(NULLIF(record_source_path, ''), rel_path)`
1705
+ * so record-container logical documents resolve to their physical container
1706
+ * path. Deeper descendants, inactive rows, and other collections are excluded.
1707
+ *
1708
+ * `dirRelPath` is collection-relative and POSIX-style; the collection root is
1709
+ * `""`. Paths that escape the collection root are rejected with
1710
+ * `INVALID_INPUT`. An empty successful result is distinct from query failure.
1711
+ *
1712
+ * `max` must be a positive integer (see `WATCHER_ACTIVE_SOURCE_PATH_MAX` for
1713
+ * the fixed service default the watcher will pass). Matching rows beyond
1714
+ * `max` yield `OVERFLOW` — never a truncated successful list.
1715
+ */
1716
+ listActiveDirectChildSourcePaths(
1717
+ collection: string,
1718
+ dirRelPath: string,
1719
+ max: number
1720
+ ): Promise<StoreResult<string[]>>;
1721
+
1722
+ /**
1723
+ * List distinct effective source paths of ACTIVE documents anywhere beneath
1724
+ * `dirRelPath` (direct children and deeper descendants).
1725
+ *
1726
+ * Used when a directory is gone from disk so the whole removed subtree can be
1727
+ * reconciled. Prefix containment is exact (`dir1` never matches `dir10/x.md`).
1728
+ *
1729
+ * `dirRelPath` must name a directory below the collection root: `""` is
1730
+ * rejected with `INVALID_INPUT` because a root-wide scan is intentionally out
1731
+ * of scope for this bounded seam. Escaping paths are also `INVALID_INPUT`.
1732
+ *
1733
+ * `max` must be a positive integer (see `WATCHER_ACTIVE_SOURCE_PATH_MAX` for
1734
+ * the fixed service default the watcher will pass). Matching rows beyond
1735
+ * `max` yield `OVERFLOW` — never a truncated successful list.
1736
+ */
1737
+ listActiveDescendantSourcePaths(
1738
+ collection: string,
1739
+ dirRelPath: string,
1740
+ max: number
1741
+ ): Promise<StoreResult<string[]>>;
1742
+
1743
+ /**
1744
+ * Root-wide bounded DISTINCT active physical source paths for one collection.
1745
+ *
1746
+ * Effective source path is `COALESCE(NULLIF(record_source_path, ''), rel_path)`.
1747
+ * Overflow is decided after DISTINCT collapse (`LIMIT max+1`), never on raw
1748
+ * logical document row counts. Inactive rows and other collections are excluded.
1749
+ * Results are ordered ascending. Matching unique sources beyond `max` yield
1750
+ * `OVERFLOW` — never a truncated successful list.
1751
+ */
1752
+ listActiveSourcePaths(
1753
+ collection: string,
1754
+ max: number
1755
+ ): Promise<StoreResult<string[]>>;
1756
+
1691
1757
  /**
1692
1758
  * Fetch documents by mirror hashes in batch.
1693
1759
  * Useful for retrieval pipelines to avoid full document scans.
@@ -15,7 +15,11 @@ Prebuilt [fts5-snowball](https://github.com/abiliojr/fts5-snowball) SQLite exten
15
15
 
16
16
  Built via GitHub Actions: `.github/workflows/build-fts5-snowball.yml`
17
17
 
18
- Source: https://github.com/abiliojr/fts5-snowball (commit from main branch)
18
+ Source: https://github.com/abiliojr/fts5-snowball at commit
19
+ `f2a5450af5ce810cc3f981ff1ea8de2b4ff82a67`.
20
+
21
+ The macOS build jobs verify the runner architecture, the emitted Mach-O
22
+ architecture, and a real stemming query through Homebrew SQLite before upload.
19
23
 
20
24
  ## Supported Languages
21
25
 
@@ -1 +0,0 @@
1
- 645e1362d74ccaf55611a0d81d085de321106ee8befc6c3639870365655bd942 gno-browser-clipper-v1.34.4.zip