@stigmer/server 3.25.0 → 3.26.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 (142) hide show
  1. package/dist/boot/compose.d.ts.map +1 -1
  2. package/dist/boot/compose.js +11 -8
  3. package/dist/boot/compose.js.map +1 -1
  4. package/dist/boot/list-indexes.d.ts +3 -0
  5. package/dist/boot/list-indexes.d.ts.map +1 -0
  6. package/dist/boot/list-indexes.js +24 -0
  7. package/dist/boot/list-indexes.js.map +1 -0
  8. package/dist/domain/agentexecution/controller.d.ts.map +1 -1
  9. package/dist/domain/agentexecution/controller.js +6 -8
  10. package/dist/domain/agentexecution/controller.js.map +1 -1
  11. package/dist/domain/agentexecution/list-index.d.ts +2 -0
  12. package/dist/domain/agentexecution/list-index.d.ts.map +1 -0
  13. package/dist/domain/agentexecution/list-index.js +23 -0
  14. package/dist/domain/agentexecution/list-index.js.map +1 -0
  15. package/dist/domain/agentexecution/steps.d.ts +17 -33
  16. package/dist/domain/agentexecution/steps.d.ts.map +1 -1
  17. package/dist/domain/agentexecution/steps.js +94 -95
  18. package/dist/domain/agentexecution/steps.js.map +1 -1
  19. package/dist/domain/agentexecution/usage.d.ts.map +1 -1
  20. package/dist/domain/agentexecution/usage.js +37 -14
  21. package/dist/domain/agentexecution/usage.js.map +1 -1
  22. package/dist/domain/artifact/controller.d.ts.map +1 -1
  23. package/dist/domain/artifact/controller.js +27 -15
  24. package/dist/domain/artifact/controller.js.map +1 -1
  25. package/dist/domain/artifact/list-index.d.ts +2 -0
  26. package/dist/domain/artifact/list-index.d.ts.map +1 -0
  27. package/dist/domain/artifact/list-index.js +21 -0
  28. package/dist/domain/artifact/list-index.js.map +1 -0
  29. package/dist/domain/session/list-index.d.ts +4 -0
  30. package/dist/domain/session/list-index.d.ts.map +1 -0
  31. package/dist/domain/session/list-index.js +26 -0
  32. package/dist/domain/session/list-index.js.map +1 -0
  33. package/dist/domain/session/steps.d.ts +7 -8
  34. package/dist/domain/session/steps.d.ts.map +1 -1
  35. package/dist/domain/session/steps.js +76 -78
  36. package/dist/domain/session/steps.js.map +1 -1
  37. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  38. package/dist/domain/workflowexecution/controller.js +53 -55
  39. package/dist/domain/workflowexecution/controller.js.map +1 -1
  40. package/dist/domain/workflowexecution/execution-filter.d.ts +4 -0
  41. package/dist/domain/workflowexecution/execution-filter.d.ts.map +1 -1
  42. package/dist/domain/workflowexecution/execution-filter.js +11 -5
  43. package/dist/domain/workflowexecution/execution-filter.js.map +1 -1
  44. package/dist/domain/workflowexecution/get-execution-summary.d.ts.map +1 -1
  45. package/dist/domain/workflowexecution/get-execution-summary.js +23 -17
  46. package/dist/domain/workflowexecution/get-execution-summary.js.map +1 -1
  47. package/dist/domain/workflowexecution/list-index.d.ts +2 -0
  48. package/dist/domain/workflowexecution/list-index.d.ts.map +1 -0
  49. package/dist/domain/workflowexecution/list-index.js +21 -0
  50. package/dist/domain/workflowexecution/list-index.js.map +1 -0
  51. package/dist/domain/workflowexecution/list-pending-approvals.d.ts.map +1 -1
  52. package/dist/domain/workflowexecution/list-pending-approvals.js +69 -32
  53. package/dist/domain/workflowexecution/list-pending-approvals.js.map +1 -1
  54. package/dist/domain/workflowexecution/queries.d.ts +30 -4
  55. package/dist/domain/workflowexecution/queries.d.ts.map +1 -1
  56. package/dist/domain/workflowexecution/queries.js +76 -22
  57. package/dist/domain/workflowexecution/queries.js.map +1 -1
  58. package/dist/extensions/list-read-scope.d.ts +9 -12
  59. package/dist/extensions/list-read-scope.d.ts.map +1 -1
  60. package/dist/extensions/list-read-scope.js.map +1 -1
  61. package/dist/index.d.ts +2 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js.map +1 -1
  64. package/dist/pipeline/steps/list-page.d.ts +63 -0
  65. package/dist/pipeline/steps/list-page.d.ts.map +1 -0
  66. package/dist/pipeline/steps/list-page.js +187 -0
  67. package/dist/pipeline/steps/list-page.js.map +1 -0
  68. package/dist/query/activity/handler.d.ts +6 -7
  69. package/dist/query/activity/handler.d.ts.map +1 -1
  70. package/dist/query/activity/handler.js +42 -59
  71. package/dist/query/activity/handler.js.map +1 -1
  72. package/dist/store/interface.d.ts +29 -0
  73. package/dist/store/interface.d.ts.map +1 -1
  74. package/dist/store/interface.js.map +1 -1
  75. package/dist/store/list-index.d.ts +123 -0
  76. package/dist/store/list-index.d.ts.map +1 -0
  77. package/dist/store/list-index.js +277 -0
  78. package/dist/store/list-index.js.map +1 -0
  79. package/dist/store/logger.d.ts +3 -0
  80. package/dist/store/logger.d.ts.map +1 -1
  81. package/dist/store/logger.js +5 -1
  82. package/dist/store/logger.js.map +1 -1
  83. package/dist/store/postgres/migrations.d.ts +10 -6
  84. package/dist/store/postgres/migrations.d.ts.map +1 -1
  85. package/dist/store/postgres/migrations.js +64 -6
  86. package/dist/store/postgres/migrations.js.map +1 -1
  87. package/dist/store/postgres/store.d.ts +26 -5
  88. package/dist/store/postgres/store.d.ts.map +1 -1
  89. package/dist/store/postgres/store.js +377 -22
  90. package/dist/store/postgres/store.js.map +1 -1
  91. package/dist/store/sqlite/migrations.d.ts +11 -5
  92. package/dist/store/sqlite/migrations.d.ts.map +1 -1
  93. package/dist/store/sqlite/migrations.js +50 -4
  94. package/dist/store/sqlite/migrations.js.map +1 -1
  95. package/dist/store/sqlite/store.d.ts +18 -3
  96. package/dist/store/sqlite/store.d.ts.map +1 -1
  97. package/dist/store/sqlite/store.js +321 -15
  98. package/dist/store/sqlite/store.js.map +1 -1
  99. package/package.json +6 -6
  100. package/src/authorization/__tests__/drivers.ts +6 -1
  101. package/src/boot/__tests__/list-indexes.test.ts +65 -0
  102. package/src/boot/compose.ts +23 -13
  103. package/src/boot/list-indexes.ts +25 -0
  104. package/src/domain/agentexecution/__tests__/agentexecution.test.ts +85 -3
  105. package/src/domain/agentexecution/controller.ts +8 -10
  106. package/src/domain/agentexecution/list-index.ts +24 -0
  107. package/src/domain/agentexecution/steps.ts +118 -115
  108. package/src/domain/agentexecution/usage.ts +38 -17
  109. package/src/domain/artifact/controller.ts +28 -18
  110. package/src/domain/artifact/list-index.ts +22 -0
  111. package/src/domain/session/list-index.ts +28 -0
  112. package/src/domain/session/steps.ts +108 -123
  113. package/src/domain/workflowexecution/__tests__/list-read-scope-summaries.test.ts +155 -99
  114. package/src/domain/workflowexecution/__tests__/workflowexecution.test.ts +193 -31
  115. package/src/domain/workflowexecution/controller.ts +54 -82
  116. package/src/domain/workflowexecution/execution-filter.ts +20 -7
  117. package/src/domain/workflowexecution/get-execution-summary.ts +31 -24
  118. package/src/domain/workflowexecution/list-index.ts +22 -0
  119. package/src/domain/workflowexecution/list-pending-approvals.ts +82 -25
  120. package/src/domain/workflowexecution/queries.ts +134 -23
  121. package/src/extensions/list-read-scope.ts +9 -12
  122. package/src/index.ts +10 -1
  123. package/src/pipeline/steps/__tests__/list-page.test.ts +186 -0
  124. package/src/pipeline/steps/list-page.ts +284 -0
  125. package/src/query/activity/handler.ts +57 -78
  126. package/src/store/README.md +51 -32
  127. package/src/store/__tests__/list-index.measure.test.ts +293 -0
  128. package/src/store/__tests__/list-index.test.ts +328 -0
  129. package/src/store/__tests__/store-contract.ts +346 -3
  130. package/src/store/interface.ts +39 -0
  131. package/src/store/list-index.ts +465 -0
  132. package/src/store/logger.ts +8 -1
  133. package/src/store/postgres/__tests__/list-index-repair.test.ts +133 -0
  134. package/src/store/postgres/__tests__/migrations.test.ts +27 -19
  135. package/src/store/postgres/__tests__/store-contract.test.ts +50 -3
  136. package/src/store/postgres/migrations.ts +69 -7
  137. package/src/store/postgres/store.ts +485 -29
  138. package/src/store/sqlite/__tests__/migrations.test.ts +51 -7
  139. package/src/store/sqlite/__tests__/store-contract.test.ts +70 -16
  140. package/src/store/sqlite/__tests__/support.ts +12 -3
  141. package/src/store/sqlite/migrations.ts +57 -6
  142. package/src/store/sqlite/store.ts +462 -21
@@ -18,14 +18,24 @@
18
18
  * list-mode newest-first at exactly 1.0 — search-mode ranking ORDER is
19
19
  * deliberately NOT asserted here, it is driver-relative), the two-phase
20
20
  * signal-dedupe hold (oss#442), OAuth grants, once-only pending-state
21
- * redemption with its 10-minute TTL, and the closed-store failure mode.
21
+ * redemption with its 10-minute TTL, the closed-store failure mode, and
22
+ * the list index (../list-index.ts): one organization's or one parent's
23
+ * rows newest first, a cursor walk with no gap and no duplicate, keys
24
+ * that follow an update and vanish with a delete, and exactness whoever
25
+ * wrote the row — a row written the way a binary that does not know the
26
+ * index writes it is read from its bytes and repaired, the reconciliation
27
+ * at open leaves nothing unproven, a row written under another revision
28
+ * of a declaration is re-derived, and an undecodable one is skipped.
22
29
  *
23
30
  * Driver-physical behavior (column layouts, FTS5/tsvector internals,
24
- * migration chains) stays in each driver's own tests.
31
+ * migration chains, the repair's lock ordering) stays in each driver's own
32
+ * tests.
25
33
  */
26
- import { fromBinary, toBinary } from "@bufbuild/protobuf";
34
+ import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
27
35
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
28
36
 
37
+ import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
38
+ import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
29
39
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
30
40
  import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
31
41
 
@@ -39,13 +49,53 @@ import type {
39
49
  PendingOAuthState,
40
50
  SearchIndexEntry,
41
51
  Store,
52
+ StoreOpenOptions,
42
53
  WorkflowExecutionEventRecord,
43
54
  } from "../interface.js";
55
+ import { declareListIndex, field, label } from "../list-index.js";
56
+ import type { ListIndexRow } from "../list-index.js";
44
57
  import { makeOrganization } from "./support.js";
45
58
 
59
+ /** The kit's own declaration, so the arms do not depend on the server's list. */
60
+ export const CONTRACT_SESSION_INDEX = declareListIndex({
61
+ kind: ApiResourceKind.session,
62
+ schema: SessionSchema,
63
+ revision: 1,
64
+ keys: {
65
+ agent_instance: field("spec.agent_instance_id"),
66
+ channel: label("stigmer.ai/channel-id"),
67
+ },
68
+ });
69
+
70
+ /** What every contract store opens with. */
71
+ export const CONTRACT_STORE_OPTIONS: StoreOpenOptions = {
72
+ listIndexes: [CONTRACT_SESSION_INDEX],
73
+ };
74
+
46
75
  /** One fresh, isolated store per test, plus the driver escape hatches. */
47
76
  export interface StoreContractFixture {
48
77
  store: Store;
78
+ /**
79
+ * Writes a resource exactly as a binary that does not know the list
80
+ * index writes it — the row and `updated_at`, nothing else — the way
81
+ * the old pod writes while a roll overlaps it with the new one.
82
+ */
83
+ writeAsOlderBinary(
84
+ kind: ApiResourceKind,
85
+ id: string,
86
+ data: Uint8Array,
87
+ ): Promise<void>;
88
+ /**
89
+ * Counts a kind's rows whose list facts are not proven current under
90
+ * `revision`, as the driver judges it; without a revision (an undeclared
91
+ * kind carries none), its unstamped rows.
92
+ */
93
+ countUnproven(kind: ApiResourceKind, revision?: number): Promise<number>;
94
+ /**
95
+ * Opens a second store over the same database (running its
96
+ * reconciliation at open); the fixture closes it at cleanup.
97
+ */
98
+ openAnother(options: StoreOpenOptions): Promise<Store>;
49
99
  /**
50
100
  * Ages a signal-dedupe row directly (crash-recovery arm: a hold whose
51
101
  * delivery died must self-heal at the next claim). The interface has no
@@ -78,6 +128,10 @@ function event(
78
128
  };
79
129
  }
80
130
 
131
+ /**
132
+ * Every driver passes its fixture factory; the factory opens its store
133
+ * with `CONTRACT_STORE_OPTIONS`.
134
+ */
81
135
  export function describeStoreContract(
82
136
  makeFixture: () => Promise<StoreContractFixture>,
83
137
  ): void {
@@ -1523,6 +1577,295 @@ export function describeStoreContract(
1523
1577
  });
1524
1578
  });
1525
1579
 
1580
+ describe("list index", () => {
1581
+ const SESSION = ApiResourceKind.session;
1582
+
1583
+ function session(
1584
+ id: string,
1585
+ org: string,
1586
+ createdSeconds: number | undefined,
1587
+ extras: { agentInstanceId?: string; channel?: string } = {},
1588
+ ): Session {
1589
+ return create(SessionSchema, {
1590
+ metadata: {
1591
+ id,
1592
+ org,
1593
+ labels:
1594
+ extras.channel === undefined
1595
+ ? {}
1596
+ : { "stigmer.ai/channel-id": extras.channel },
1597
+ },
1598
+ spec: { agentInstanceId: extras.agentInstanceId ?? "" },
1599
+ status:
1600
+ createdSeconds === undefined
1601
+ ? {}
1602
+ : {
1603
+ audit: {
1604
+ specAudit: {
1605
+ createdAt: { seconds: BigInt(createdSeconds), nanos: 0 },
1606
+ },
1607
+ },
1608
+ },
1609
+ });
1610
+ }
1611
+
1612
+ async function save(s: Session): Promise<void> {
1613
+ await fx.store.saveResource(SESSION, s.metadata!.id, SessionSchema, s);
1614
+ }
1615
+
1616
+ function ids(rows: ReadonlyArray<ListIndexRow>): string[] {
1617
+ return rows.map((r) => r.id);
1618
+ }
1619
+
1620
+ it("reads one organization's rows newest first, ids breaking ties, unstamped last", async () => {
1621
+ await save(session("ses_a", "acme", 100));
1622
+ await save(session("ses_b", "acme", 300));
1623
+ await save(session("ses_c", "acme", 300));
1624
+ await save(session("ses_d", "acme", undefined));
1625
+ await save(session("ses_e", "other", 500));
1626
+
1627
+ const rows = await fx.store.queryResources(CONTRACT_SESSION_INDEX, {
1628
+ org: "acme",
1629
+ });
1630
+ expect(ids(rows)).toEqual(["ses_c", "ses_b", "ses_a", "ses_d"]);
1631
+ expect(
1632
+ fromBinary(SessionSchema, rows[0]!.data).metadata?.id,
1633
+ "the row's bytes ride the result",
1634
+ ).toBe("ses_c");
1635
+ expect(
1636
+ ids(await fx.store.queryResources(CONTRACT_SESSION_INDEX, {})),
1637
+ "no organization reads every organization",
1638
+ ).toEqual(["ses_e", "ses_c", "ses_b", "ses_a", "ses_d"]);
1639
+ });
1640
+
1641
+ it("walks every row by cursor with no gap and no duplicate", async () => {
1642
+ for (let i = 0; i < 7; i++) {
1643
+ await save(session(`ses_${i}`, "acme", i % 3 === 0 ? 10 : 100 + i));
1644
+ }
1645
+ const all = ids(
1646
+ await fx.store.queryResources(CONTRACT_SESSION_INDEX, { org: "acme" }),
1647
+ );
1648
+
1649
+ const walked: string[] = [];
1650
+ let after: ListIndexRow["cursor"] | undefined;
1651
+ for (;;) {
1652
+ const page = await fx.store.queryResources(CONTRACT_SESSION_INDEX, {
1653
+ org: "acme",
1654
+ limit: 3,
1655
+ ...(after === undefined ? {} : { after }),
1656
+ });
1657
+ walked.push(...ids(page));
1658
+ if (page.length < 3) {
1659
+ break;
1660
+ }
1661
+ after = page[page.length - 1]!.cursor;
1662
+ }
1663
+ expect(walked).toEqual(all);
1664
+ expect(new Set(walked).size).toBe(7);
1665
+ });
1666
+
1667
+ it("reads a parent's rows through a key, or through any of several", async () => {
1668
+ await save(session("ses_1", "acme", 1, { agentInstanceId: "ain_1" }));
1669
+ await save(session("ses_2", "acme", 2, { channel: "ach_1" }));
1670
+ await save(session("ses_3", "acme", 3, { agentInstanceId: "ain_2" }));
1671
+
1672
+ expect(
1673
+ ids(
1674
+ await fx.store.queryResources(CONTRACT_SESSION_INDEX, {
1675
+ anyKey: [{ name: "agent_instance", value: "ain_1" }],
1676
+ }),
1677
+ ),
1678
+ ).toEqual(["ses_1"]);
1679
+ expect(
1680
+ ids(
1681
+ await fx.store.queryResources(CONTRACT_SESSION_INDEX, {
1682
+ anyKey: [
1683
+ { name: "agent_instance", value: "ain_1" },
1684
+ { name: "channel", value: "ach_1" },
1685
+ ],
1686
+ }),
1687
+ ),
1688
+ ).toEqual(["ses_2", "ses_1"]);
1689
+ expect(
1690
+ await fx.store.queryResources(CONTRACT_SESSION_INDEX, { anyKey: [] }),
1691
+ "an empty key list matches nothing",
1692
+ ).toEqual([]);
1693
+ });
1694
+
1695
+ it("keeps rows created at or after a bound, and rows with no stamp", async () => {
1696
+ await save(session("ses_old", "acme", 100));
1697
+ await save(session("ses_new", "acme", 200));
1698
+ await save(session("ses_unstamped", "acme", undefined));
1699
+ expect(
1700
+ ids(
1701
+ await fx.store.queryResources(CONTRACT_SESSION_INDEX, {
1702
+ createdAtOrAfter: "1970-01-01T00:02:30.000000000Z",
1703
+ }),
1704
+ ),
1705
+ ).toEqual(["ses_new", "ses_unstamped"]);
1706
+ });
1707
+
1708
+ it("follows a key an update changes, and forgets a deleted row", async () => {
1709
+ await save(session("ses_1", "acme", 1, { agentInstanceId: "ain_1" }));
1710
+ await fx.store.updateResource(SESSION, "ses_1", SessionSchema, (s) => {
1711
+ s.spec!.agentInstanceId = "ain_2";
1712
+ });
1713
+ const byInstance = (value: string) =>
1714
+ fx.store.queryResources(CONTRACT_SESSION_INDEX, {
1715
+ anyKey: [{ name: "agent_instance", value }],
1716
+ });
1717
+ expect(ids(await byInstance("ain_1"))).toEqual([]);
1718
+ expect(ids(await byInstance("ain_2"))).toEqual(["ses_1"]);
1719
+
1720
+ await fx.store.deleteResource(SESSION, "ses_1");
1721
+ expect(ids(await byInstance("ain_2"))).toEqual([]);
1722
+ expect(await fx.store.queryResources(CONTRACT_SESSION_INDEX, {})).toEqual(
1723
+ [],
1724
+ );
1725
+ });
1726
+
1727
+ it("reads a row written by a binary that does not know the index from its bytes, and repairs it", async () => {
1728
+ await save(
1729
+ session("ses_moved", "acme", 100, { agentInstanceId: "ain_1" }),
1730
+ );
1731
+ // The older binary rewrites one row into another organization and
1732
+ // instance, and creates another row outright.
1733
+ await fx.writeAsOlderBinary(
1734
+ SESSION,
1735
+ "ses_moved",
1736
+ toBinary(
1737
+ SessionSchema,
1738
+ session("ses_moved", "other", 100, { agentInstanceId: "ain_2" }),
1739
+ ),
1740
+ );
1741
+ await fx.writeAsOlderBinary(
1742
+ SESSION,
1743
+ "ses_created",
1744
+ toBinary(SessionSchema, session("ses_created", "acme", 50)),
1745
+ );
1746
+ await save(session("ses_proven", "acme", 75));
1747
+ expect(await fx.countUnproven(SESSION, 1)).toBe(2);
1748
+
1749
+ expect(
1750
+ ids(
1751
+ await fx.store.queryResources(CONTRACT_SESSION_INDEX, {
1752
+ org: "acme",
1753
+ }),
1754
+ ),
1755
+ ).toEqual(["ses_proven", "ses_created"]);
1756
+ expect(
1757
+ ids(
1758
+ await fx.store.queryResources(CONTRACT_SESSION_INDEX, {
1759
+ anyKey: [{ name: "agent_instance", value: "ain_2" }],
1760
+ }),
1761
+ ),
1762
+ ).toEqual(["ses_moved"]);
1763
+ expect(
1764
+ await fx.countUnproven(SESSION, 1),
1765
+ "both rows repaired by the read",
1766
+ ).toBe(0);
1767
+ expect(
1768
+ ids(
1769
+ await fx.store.queryResources(CONTRACT_SESSION_INDEX, {
1770
+ org: "other",
1771
+ }),
1772
+ ),
1773
+ "and read through the index once repaired",
1774
+ ).toEqual(["ses_moved"]);
1775
+ });
1776
+
1777
+ it("merges an unproven row into the right page of a cursor walk", async () => {
1778
+ for (let i = 0; i < 4; i++) {
1779
+ await save(session(`ses_${i}`, "acme", 100 + i));
1780
+ }
1781
+ await fx.writeAsOlderBinary(
1782
+ SESSION,
1783
+ "ses_mid",
1784
+ toBinary(SessionSchema, session("ses_mid", "acme", 102)),
1785
+ );
1786
+ const first = await fx.store.queryResources(CONTRACT_SESSION_INDEX, {
1787
+ org: "acme",
1788
+ limit: 2,
1789
+ });
1790
+ const second = await fx.store.queryResources(CONTRACT_SESSION_INDEX, {
1791
+ org: "acme",
1792
+ limit: 2,
1793
+ after: first[1]!.cursor,
1794
+ });
1795
+ expect([...ids(first), ...ids(second)]).toEqual([
1796
+ "ses_3",
1797
+ "ses_mid",
1798
+ "ses_2",
1799
+ "ses_1",
1800
+ ]);
1801
+ });
1802
+
1803
+ it("skips an unproven row that does not decode", async () => {
1804
+ await save(session("ses_good", "acme", 1));
1805
+ await fx.writeAsOlderBinary(
1806
+ SESSION,
1807
+ "ses_garbage",
1808
+ new Uint8Array([0xff, 0xff, 0xff]),
1809
+ );
1810
+ expect(
1811
+ ids(await fx.store.queryResources(CONTRACT_SESSION_INDEX, {})),
1812
+ ).toEqual(["ses_good"]);
1813
+ });
1814
+
1815
+ it("leaves nothing unproven once a store has opened", async () => {
1816
+ await fx.writeAsOlderBinary(
1817
+ SESSION,
1818
+ "ses_1",
1819
+ toBinary(SessionSchema, session("ses_1", "acme", 1)),
1820
+ );
1821
+ await fx.writeAsOlderBinary(
1822
+ KIND,
1823
+ "acme",
1824
+ toBinary(OrganizationSchema, makeOrganization({ id: "acme" })),
1825
+ );
1826
+ expect(await fx.countUnproven(SESSION, 1)).toBe(1);
1827
+
1828
+ await fx.openAnother(CONTRACT_STORE_OPTIONS);
1829
+ expect(
1830
+ await fx.countUnproven(SESSION, 1),
1831
+ "a declared kind is derived",
1832
+ ).toBe(0);
1833
+ expect(
1834
+ await fx.countUnproven(KIND),
1835
+ "an undeclared kind is stamped, so it never widens the unproven set",
1836
+ ).toBe(0);
1837
+ });
1838
+
1839
+ it("re-derives rows written under another revision of a declaration", async () => {
1840
+ await save(session("ses_1", "acme", 1));
1841
+ const revised = declareListIndex({
1842
+ ...CONTRACT_SESSION_INDEX,
1843
+ revision: 2,
1844
+ keys: { org_copy: field("metadata.org") },
1845
+ });
1846
+ expect(await fx.countUnproven(SESSION, 2)).toBe(1);
1847
+ const other = await fx.openAnother({ listIndexes: [revised] });
1848
+ expect(await fx.countUnproven(SESSION, 2)).toBe(0);
1849
+ expect(
1850
+ ids(
1851
+ await other.queryResources(revised, {
1852
+ anyKey: [{ name: "org_copy", value: "acme" }],
1853
+ }),
1854
+ ),
1855
+ ).toEqual(["ses_1"]);
1856
+ });
1857
+
1858
+ it("refuses a declaration it was not opened with, and a limit no caller means", async () => {
1859
+ const stranger = declareListIndex({ ...CONTRACT_SESSION_INDEX });
1860
+ await expect(fx.store.queryResources(stranger, {})).rejects.toThrow(
1861
+ "list index for session is not registered with this store",
1862
+ );
1863
+ await expect(
1864
+ fx.store.queryResources(CONTRACT_SESSION_INDEX, { limit: 0 }),
1865
+ ).rejects.toThrow("list index limit must be a positive integer, got 0");
1866
+ });
1867
+ });
1868
+
1526
1869
  describe("lifecycle", () => {
1527
1870
  it("every method fails with 'store is closed' after close; close is idempotent", async () => {
1528
1871
  await fx.store.close();
@@ -30,6 +30,12 @@ import type { DescMessage, MessageShape } from "@bufbuild/protobuf";
30
30
 
31
31
  import type { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
32
32
 
33
+ import type {
34
+ ListIndexDeclaration,
35
+ ListIndexQuery,
36
+ ListIndexRow,
37
+ } from "./list-index.js";
38
+
33
39
  // =============================================================================
34
40
  // Sentinel errors
35
41
  // =============================================================================
@@ -434,6 +440,16 @@ export interface RawResourceDocument {
434
440
  readonly data: Uint8Array;
435
441
  }
436
442
 
443
+ /**
444
+ * What a driver opens with beyond its connection. `listIndexes` is the
445
+ * composition root's one list of list-index declarations
446
+ * (boot/list-indexes.ts); a store opened without it keeps no list index
447
+ * and refuses every `queryResources`.
448
+ */
449
+ export interface StoreOpenOptions {
450
+ readonly listIndexes?: ReadonlyArray<ListIndexDeclaration>;
451
+ }
452
+
437
453
  // =============================================================================
438
454
  // The store contract
439
455
  // =============================================================================
@@ -492,6 +508,29 @@ export interface Store {
492
508
  */
493
509
  listResources(kind: ApiResourceKind): Promise<Uint8Array[]>;
494
510
 
511
+ /**
512
+ * The rows of a list-indexed kind that match the query, newest first on
513
+ * (creation instant, id) compared as bytes, strictly after
514
+ * `query.after`, at most `query.limit` of them (list-index.ts states the
515
+ * order and the predicates). Fewer than `limit` rows means there are no
516
+ * more.
517
+ *
518
+ * EXACT whoever wrote the rows: a row whose index facts are proven
519
+ * current is answered through the index; every other row of the kind —
520
+ * one written by a binary that does not know the index, or under
521
+ * another revision of the declaration — is evaluated from its bytes with
522
+ * the declaration, and repaired by a compare-and-set on those bytes so a
523
+ * newer write is never overwritten. An unproven row that cannot be
524
+ * decoded is skipped and logged, as the list lanes skip one.
525
+ *
526
+ * Throws when `declaration` is not the one this store was opened with,
527
+ * or on a limit that is not a positive integer.
528
+ */
529
+ queryResources<K extends string>(
530
+ declaration: ListIndexDeclaration<K>,
531
+ query: ListIndexQuery<K>,
532
+ ): Promise<ListIndexRow[]>;
533
+
495
534
  /** Removes a resource; NO error if it does not exist. */
496
535
  deleteResource(kind: ApiResourceKind, id: string): Promise<void>;
497
536