@indigoai-us/hq-cloud 5.40.0 → 5.42.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 (47) hide show
  1. package/dist/bin/sync-runner.d.ts +26 -1
  2. package/dist/bin/sync-runner.d.ts.map +1 -1
  3. package/dist/bin/sync-runner.js +105 -0
  4. package/dist/bin/sync-runner.js.map +1 -1
  5. package/dist/bin/sync-runner.test.js +168 -1
  6. package/dist/bin/sync-runner.test.js.map +1 -1
  7. package/dist/cli/sync-scope.test.d.ts +22 -0
  8. package/dist/cli/sync-scope.test.d.ts.map +1 -0
  9. package/dist/cli/sync-scope.test.js +273 -0
  10. package/dist/cli/sync-scope.test.js.map +1 -0
  11. package/dist/cli/sync.d.ts +64 -0
  12. package/dist/cli/sync.d.ts.map +1 -1
  13. package/dist/cli/sync.js +152 -4
  14. package/dist/cli/sync.js.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/prefix-coalesce.d.ts +29 -0
  20. package/dist/prefix-coalesce.d.ts.map +1 -1
  21. package/dist/prefix-coalesce.js +48 -0
  22. package/dist/prefix-coalesce.js.map +1 -1
  23. package/dist/prefix-coalesce.test.js +51 -1
  24. package/dist/prefix-coalesce.test.js.map +1 -1
  25. package/dist/qmd-reindex.d.ts +59 -0
  26. package/dist/qmd-reindex.d.ts.map +1 -0
  27. package/dist/qmd-reindex.js +128 -0
  28. package/dist/qmd-reindex.js.map +1 -0
  29. package/dist/qmd-reindex.test.d.ts +10 -0
  30. package/dist/qmd-reindex.test.d.ts.map +1 -0
  31. package/dist/qmd-reindex.test.js +129 -0
  32. package/dist/qmd-reindex.test.js.map +1 -0
  33. package/dist/scope-shrink.d.ts +18 -0
  34. package/dist/scope-shrink.d.ts.map +1 -1
  35. package/dist/scope-shrink.js +28 -0
  36. package/dist/scope-shrink.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/bin/sync-runner.test.ts +222 -0
  39. package/src/bin/sync-runner.ts +124 -0
  40. package/src/cli/sync-scope.test.ts +307 -0
  41. package/src/cli/sync.ts +240 -1
  42. package/src/index.ts +1 -0
  43. package/src/prefix-coalesce.test.ts +76 -1
  44. package/src/prefix-coalesce.ts +45 -0
  45. package/src/qmd-reindex.test.ts +143 -0
  46. package/src/qmd-reindex.ts +151 -0
  47. package/src/scope-shrink.ts +28 -0
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Unit tests for reindexAfterSync.
3
+ *
4
+ * The function shells out to the global `qmd` binary, so every test injects a
5
+ * fake `exec` that records the calls and returns canned results — no real qmd
6
+ * is invoked. We assert on the *sequence of qmd commands* the function would
7
+ * run, which is the contract that keeps a teammate's index fresh after sync.
8
+ */
9
+ import { describe, it, expect } from "vitest";
10
+ import * as fs from "fs";
11
+ import * as os from "os";
12
+ import * as path from "path";
13
+ import { reindexAfterSync } from "./qmd-reindex.js";
14
+ /** Build a fake exec that returns status 0 for everything and logs calls. */
15
+ function fakeExec(opts) {
16
+ const calls = [];
17
+ const exec = (args) => {
18
+ calls.push(args);
19
+ if (opts?.failOn && args[0] === opts.failOn)
20
+ return { status: 1, stdout: "" };
21
+ if (args[0] === "collection" && args[1] === "list") {
22
+ return { status: 0, stdout: opts?.listStdout ?? "" };
23
+ }
24
+ return { status: 0, stdout: "" };
25
+ };
26
+ return { exec, calls };
27
+ }
28
+ describe("reindexAfterSync", () => {
29
+ it("no-ops when the path is not an HQ root", () => {
30
+ const { exec, calls } = fakeExec();
31
+ const r = reindexAfterSync("/tmp/not-hq", {
32
+ exec,
33
+ existsSync: () => false, // core/core.yaml absent
34
+ });
35
+ expect(r.qmdAvailable).toBe(false);
36
+ expect(calls).toHaveLength(0);
37
+ });
38
+ it("no-ops when qmd is unavailable (collection list errors)", () => {
39
+ const { exec, calls } = fakeExec({ failOn: "collection" });
40
+ const r = reindexAfterSync("/hq", {
41
+ exec,
42
+ existsSync: () => true, // core.yaml present
43
+ });
44
+ expect(r.qmdAvailable).toBe(false);
45
+ expect(r.updated).toBe(false);
46
+ // Only the availability probe ran, then bailed.
47
+ expect(calls).toEqual([["collection", "list"]]);
48
+ });
49
+ it("registers a missing company collection then runs an incremental update", () => {
50
+ const { exec, calls } = fakeExec({ listStdout: "Collections (1):\n\nHQ (qmd://HQ/)\n" });
51
+ const r = reindexAfterSync("/hq", {
52
+ exec,
53
+ existsSync: () => true,
54
+ readCompanies: () => ["acme"],
55
+ hasIndexableMarkdown: () => true,
56
+ });
57
+ expect(r.qmdAvailable).toBe(true);
58
+ expect(r.collectionsAdded).toEqual(["acme"]);
59
+ expect(r.updated).toBe(true);
60
+ expect(r.embedded).toBe(false);
61
+ // Expected command sequence: probe, add, context, update.
62
+ expect(calls).toEqual([
63
+ ["collection", "list"],
64
+ ["collection", "add", path.join("/hq", "companies", "acme", "knowledge"), "--name", "acme", "--mask", "**/*.md"],
65
+ ["context", "add", "qmd://acme", "Knowledge base for acme."],
66
+ ["update"],
67
+ ]);
68
+ });
69
+ it("skips collections that already exist", () => {
70
+ const { exec, calls } = fakeExec({ listStdout: "acme (qmd://acme/)\n" });
71
+ const r = reindexAfterSync("/hq", {
72
+ exec,
73
+ existsSync: () => true,
74
+ readCompanies: () => ["acme"],
75
+ hasIndexableMarkdown: () => true,
76
+ });
77
+ expect(r.collectionsAdded).toEqual([]);
78
+ // No `collection add` — straight to update.
79
+ expect(calls).toEqual([["collection", "list"], ["update"]]);
80
+ });
81
+ it("skips company dirs with no indexable markdown", () => {
82
+ const { exec, calls } = fakeExec({ listStdout: "" });
83
+ const r = reindexAfterSync("/hq", {
84
+ exec,
85
+ existsSync: () => true,
86
+ readCompanies: () => ["empty"],
87
+ hasIndexableMarkdown: () => false,
88
+ });
89
+ expect(r.collectionsAdded).toEqual([]);
90
+ expect(calls).toEqual([["collection", "list"], ["update"]]);
91
+ });
92
+ it("runs embed only when embed:true", () => {
93
+ const { exec, calls } = fakeExec({ listStdout: "" });
94
+ const r = reindexAfterSync("/hq", {
95
+ exec,
96
+ existsSync: () => true,
97
+ readCompanies: () => [],
98
+ embed: true,
99
+ });
100
+ expect(r.embedded).toBe(true);
101
+ expect(calls).toEqual([["collection", "list"], ["update"], ["embed"]]);
102
+ });
103
+ it("never throws even if exec misbehaves", () => {
104
+ const exec = () => {
105
+ throw new Error("boom");
106
+ };
107
+ expect(() => reindexAfterSync("/hq", { exec, existsSync: () => true })).not.toThrow();
108
+ });
109
+ it("hasIndexableMarkdown finds a real .md and ignores INDEX.md", () => {
110
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-qmd-test-"));
111
+ try {
112
+ const hq = path.join(dir, "hq");
113
+ const kdir = path.join(hq, "companies", "acme", "knowledge");
114
+ fs.mkdirSync(kdir, { recursive: true });
115
+ fs.mkdirSync(path.join(hq, "core"), { recursive: true });
116
+ fs.writeFileSync(path.join(hq, "core", "core.yaml"), "hqVersion: 1\n");
117
+ fs.writeFileSync(path.join(kdir, "INDEX.md"), "# index\n");
118
+ fs.writeFileSync(path.join(kdir, "note.md"), "# real\n");
119
+ const { exec, calls } = fakeExec({ listStdout: "" });
120
+ const r = reindexAfterSync(hq, { exec }); // real fs walk for md detection
121
+ expect(r.collectionsAdded).toEqual(["acme"]);
122
+ expect(calls.some((c) => c[0] === "collection" && c[1] === "add")).toBe(true);
123
+ }
124
+ finally {
125
+ fs.rmSync(dir, { recursive: true, force: true });
126
+ }
127
+ });
128
+ });
129
+ //# sourceMappingURL=qmd-reindex.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qmd-reindex.test.js","sourceRoot":"","sources":["../src/qmd-reindex.test.ts"],"names":[],"mappings":"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"}
@@ -79,6 +79,24 @@ export declare class ScopeShrinkBlockedError extends Error {
79
79
  readonly code = "SCOPE_SHRINK_BLOCKED";
80
80
  constructor(companyUid: string, fromMode: PullRecord["syncMode"] | "unknown", toMode: PullRecord["syncMode"], dirty: OrphanClassification[], clean: OrphanClassification[]);
81
81
  }
82
+ /**
83
+ * Structured error thrown when an AUTOMATIC scope shrink would prune more
84
+ * CLEAN local files than the configured safety cap in a single pull. This is
85
+ * the bulk-delete guard: a routine background sync should never silently
86
+ * delete a large local tree — whether from a deliberate-but-abrupt first
87
+ * narrow, a server bug returning an unexpectedly small grant set, or a
88
+ * mis-resolved scope. The operator runs the explicit `hq sync narrow` ritual
89
+ * (which has its own confirmation + dirty gate) or passes `--force-scope-shrink`
90
+ * to proceed. The engine never deletes anything when it throws this.
91
+ */
92
+ export declare class ScopeShrinkLargePruneError extends Error {
93
+ readonly companyUid: string;
94
+ readonly toMode: PullRecord["syncMode"];
95
+ readonly cleanCount: number;
96
+ readonly cap: number;
97
+ readonly code = "SCOPE_SHRINK_LARGE_PRUNE";
98
+ constructor(companyUid: string, toMode: PullRecord["syncMode"], cleanCount: number, cap: number);
99
+ }
82
100
  export interface ApplyScopeShrinkInput {
83
101
  journal: SyncJournal;
84
102
  plan: ScopeShrinkPlan;
@@ -1 +1 @@
1
- {"version":3,"file":"scope-shrink.d.ts","sourceRoot":"","sources":["../src/scope-shrink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,WAAW,EACZ,MAAM,YAAY,CAAC;AAIpB,MAAM,WAAW,oBAAoB;IACnC,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,WAAW,CAAC,EACR,qBAAqB,GACrB,eAAe,GACf,YAAY,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,iDAAiD;IACjD,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,kDAAkD;IAClD,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,8CAA8C;IAC9C,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,oDAAoD;IACpD,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,yBAAyB,GAC/B,eAAe,CAoBjB;AA8ED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAG9B,UAAU,EAAE,MAAM;aAClB,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,GAAG,SAAS;aAC5C,MAAM,EAAE,UAAU,CAAC,UAAU,CAAC;aAC9B,KAAK,EAAE,oBAAoB,EAAE;aAC7B,KAAK,EAAE,oBAAoB,EAAE;IAN/C,QAAQ,CAAC,IAAI,0BAA0B;gBAErB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,GAAG,SAAS,EAC5C,MAAM,EAAE,UAAU,CAAC,UAAU,CAAC,EAC9B,KAAK,EAAE,oBAAoB,EAAE,EAC7B,KAAK,EAAE,oBAAoB,EAAE;CAShD;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,gBAAgB,EAAE,OAAO,CAAC;IAC1B,MAAM,CAAC,EAAE,cAAc,GAAG,cAAc,GAAG,QAAQ,CAAC;CACrD;AAED,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,qBAAqB,GAC3B,sBAAsB,CA4BxB"}
1
+ {"version":3,"file":"scope-shrink.d.ts","sourceRoot":"","sources":["../src/scope-shrink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,WAAW,EACZ,MAAM,YAAY,CAAC;AAIpB,MAAM,WAAW,oBAAoB;IACnC,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,WAAW,CAAC,EACR,qBAAqB,GACrB,eAAe,GACf,YAAY,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,iDAAiD;IACjD,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,kDAAkD;IAClD,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,8CAA8C;IAC9C,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,oDAAoD;IACpD,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,yBAAyB,GAC/B,eAAe,CAoBjB;AA8ED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAG9B,UAAU,EAAE,MAAM;aAClB,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,GAAG,SAAS;aAC5C,MAAM,EAAE,UAAU,CAAC,UAAU,CAAC;aAC9B,KAAK,EAAE,oBAAoB,EAAE;aAC7B,KAAK,EAAE,oBAAoB,EAAE;IAN/C,QAAQ,CAAC,IAAI,0BAA0B;gBAErB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,GAAG,SAAS,EAC5C,MAAM,EAAE,UAAU,CAAC,UAAU,CAAC,EAC9B,KAAK,EAAE,oBAAoB,EAAE,EAC7B,KAAK,EAAE,oBAAoB,EAAE;CAShD;AAED;;;;;;;;;GASG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;aAGjC,UAAU,EAAE,MAAM;aAClB,MAAM,EAAE,UAAU,CAAC,UAAU,CAAC;aAC9B,UAAU,EAAE,MAAM;aAClB,GAAG,EAAE,MAAM;IAL7B,QAAQ,CAAC,IAAI,8BAA8B;gBAEzB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,UAAU,CAAC,UAAU,CAAC,EAC9B,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM;CAU9B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,gBAAgB,EAAE,OAAO,CAAC;IAC1B,MAAM,CAAC,EAAE,cAAc,GAAG,cAAc,GAAG,QAAQ,CAAC;CACrD;AAED,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,qBAAqB,GAC3B,sBAAsB,CA4BxB"}
@@ -157,6 +157,34 @@ export class ScopeShrinkBlockedError extends Error {
157
157
  this.name = "ScopeShrinkBlockedError";
158
158
  }
159
159
  }
160
+ /**
161
+ * Structured error thrown when an AUTOMATIC scope shrink would prune more
162
+ * CLEAN local files than the configured safety cap in a single pull. This is
163
+ * the bulk-delete guard: a routine background sync should never silently
164
+ * delete a large local tree — whether from a deliberate-but-abrupt first
165
+ * narrow, a server bug returning an unexpectedly small grant set, or a
166
+ * mis-resolved scope. The operator runs the explicit `hq sync narrow` ritual
167
+ * (which has its own confirmation + dirty gate) or passes `--force-scope-shrink`
168
+ * to proceed. The engine never deletes anything when it throws this.
169
+ */
170
+ export class ScopeShrinkLargePruneError extends Error {
171
+ companyUid;
172
+ toMode;
173
+ cleanCount;
174
+ cap;
175
+ code = "SCOPE_SHRINK_LARGE_PRUNE";
176
+ constructor(companyUid, toMode, cleanCount, cap) {
177
+ super(`Refusing to auto-prune ${cleanCount} local file(s) for ${companyUid} ` +
178
+ `(${toMode} scope) in one sync — exceeds the safety cap of ${cap}. ` +
179
+ `Run \`hq sync narrow --apply\` to migrate with confirmation, raise ` +
180
+ `HQ_SYNC_MAX_AUTO_PRUNE, or pass --force-scope-shrink.`);
181
+ this.companyUid = companyUid;
182
+ this.toMode = toMode;
183
+ this.cleanCount = cleanCount;
184
+ this.cap = cap;
185
+ this.name = "ScopeShrinkLargePruneError";
186
+ }
187
+ }
160
188
  /**
161
189
  * Apply a scope-shrink plan: delete clean orphans on disk + tombstone their
162
190
  * journal entries. With `forceScopeShrink: true`, dirty orphans are
@@ -1 +1 @@
1
- {"version":3,"file":"scope-shrink.js","sourceRoot":"","sources":["../src/scope-shrink.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scope-shrink.js","sourceRoot":"","sources":["../src/scope-shrink.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cloud",
3
- "version": "5.40.0",
3
+ "version": "5.42.0",
4
4
  "description": "HQ by Indigo cloud sync engine — bidirectional S3 sync for mobile access",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,6 +18,7 @@ import {
18
18
  resolveSkipPersonal,
19
19
  routeChangeToTarget,
20
20
  buildTargetedPushArgv,
21
+ resolvePullScope,
21
22
  } from "./sync-runner.js";
22
23
  import type {
23
24
  RunnerEvent,
@@ -100,6 +101,9 @@ function defaultSyncResult(overrides: Partial<SyncResult> = {}): SyncResult {
100
101
  newFilesCount: 0,
101
102
  filesExcludedByPolicy: 0,
102
103
  filesTombstoned: 0,
104
+ filesOutOfScope: 0,
105
+ scopeOrphansRemoved: 0,
106
+ scopeOrphansBlocked: 0,
103
107
  ...overrides,
104
108
  };
105
109
  }
@@ -1075,6 +1079,9 @@ describe("per-company fanout", () => {
1075
1079
  filesExcludedByPolicy: 0,
1076
1080
  newFiles: result.newFiles,
1077
1081
  newFilesCount: result.newFilesCount,
1082
+ filesOutOfScope: result.filesOutOfScope,
1083
+ scopeOrphansRemoved: result.scopeOrphansRemoved,
1084
+ scopeOrphansBlocked: result.scopeOrphansBlocked,
1078
1085
  });
1079
1086
  });
1080
1087
 
@@ -3117,3 +3124,218 @@ describe("resolveSkipPersonal", () => {
3117
3124
  },
3118
3125
  );
3119
3126
  });
3127
+
3128
+ // ---------------------------------------------------------------------------
3129
+ // resolvePullScope (US-005) — effective download scope per company leg
3130
+ // ---------------------------------------------------------------------------
3131
+
3132
+ describe("resolvePullScope", () => {
3133
+ function stubClient(
3134
+ overrides: Partial<VaultClientSurface>,
3135
+ ): VaultClientSurface {
3136
+ return {
3137
+ listMyMemberships: async () => [],
3138
+ listMyPendingInvitesByEmail: async () => [],
3139
+ claimPendingInvitesByEmail: async () => {},
3140
+ ensureMyPersonEntity: async () => ({ uid: "p", slug: "p" }) as never,
3141
+ entity: {
3142
+ get: async (uid: string) => ({ uid, slug: uid }) as never,
3143
+ listByType: async () => [],
3144
+ },
3145
+ ...overrides,
3146
+ };
3147
+ }
3148
+
3149
+ const membership = (companyUid: string, membershipKey: string) =>
3150
+ ({
3151
+ membershipKey,
3152
+ personUid: "prs_1",
3153
+ companyUid,
3154
+ role: "member",
3155
+ status: "active",
3156
+ invitedBy: "prs_0",
3157
+ invitedAt: "2026-01-01T00:00:00.000Z",
3158
+ createdAt: "2026-01-01T00:00:00.000Z",
3159
+ updatedAt: "2026-01-01T00:00:00.000Z",
3160
+ }) as never;
3161
+
3162
+ it("returns all when the client has no getMembershipSyncConfig", async () => {
3163
+ const scope = await resolvePullScope(stubClient({}), "cmp_a", "acme");
3164
+ expect(scope).toEqual({ syncMode: "all" });
3165
+ });
3166
+
3167
+ it("returns all for an all-mode membership (no prefixSet)", async () => {
3168
+ const scope = await resolvePullScope(
3169
+ stubClient({
3170
+ listMyMemberships: async () => [membership("cmp_a", "mk_a")],
3171
+ getMembershipSyncConfig: async () => ({
3172
+ membershipId: "mk_a",
3173
+ syncMode: "all",
3174
+ isDefault: true,
3175
+ }),
3176
+ }),
3177
+ "cmp_a",
3178
+ "acme",
3179
+ );
3180
+ expect(scope).toEqual({ syncMode: "all" });
3181
+ });
3182
+
3183
+ it("coalesces explicit grants for a shared-mode membership", async () => {
3184
+ const scope = await resolvePullScope(
3185
+ stubClient({
3186
+ listMyMemberships: async () => [membership("cmp_a", "mk_a")],
3187
+ getMembershipSyncConfig: async () => ({
3188
+ membershipId: "mk_a",
3189
+ syncMode: "shared",
3190
+ isDefault: false,
3191
+ }),
3192
+ listMyExplicitGrants: async () => [
3193
+ { companyUid: "cmp_a", path: "knowledge/", permission: "read", source: "person" },
3194
+ { companyUid: "cmp_a", path: "knowledge/sub/", permission: "read", source: "group" },
3195
+ { companyUid: "cmp_a", path: "shared/", permission: "read", source: "open" },
3196
+ ] as never,
3197
+ }),
3198
+ "cmp_a",
3199
+ "acme",
3200
+ );
3201
+ // knowledge/sub/ is covered by knowledge/ → coalesced away.
3202
+ expect(scope.syncMode).toBe("shared");
3203
+ expect(scope.prefixSet).toEqual(["knowledge/", "shared/"]);
3204
+ });
3205
+
3206
+ it("normalizes real-world mixed/glob grant paths into company-relative prefixes", async () => {
3207
+ // The exact shapes observed in the live hq-pro vault for `indigo`:
3208
+ // bare glob, full-anchored + /*, full-anchored exact file, slug-anchored
3209
+ // + /*, company-relative + /*, and a company-relative exact file.
3210
+ const scope = await resolvePullScope(
3211
+ stubClient({
3212
+ listMyMemberships: async () => [membership("cmp_indigo", "mk_i")],
3213
+ getMembershipSyncConfig: async () => ({
3214
+ membershipId: "mk_i",
3215
+ syncMode: "shared",
3216
+ isDefault: false,
3217
+ }),
3218
+ listMyExplicitGrants: async () =>
3219
+ [
3220
+ { companyUid: "cmp_indigo", path: "companies/indigo/design-pack/*", permission: "write", source: "person" },
3221
+ { companyUid: "cmp_indigo", path: "companies/indigo/knowledge/README.md", permission: "write", source: "person" },
3222
+ { companyUid: "cmp_indigo", path: "indigo/data/vyg/old-meetings/*", permission: "write", source: "person" },
3223
+ { companyUid: "cmp_indigo", path: "data/vyg/*", permission: "write", source: "person" },
3224
+ { companyUid: "cmp_indigo", path: "company.yaml", permission: "read", source: "open" },
3225
+ ] as never,
3226
+ }),
3227
+ "cmp_indigo",
3228
+ "indigo",
3229
+ );
3230
+ expect(scope.syncMode).toBe("shared");
3231
+ // All anchors stripped, globs folded to startsWith-prefixes, sorted —
3232
+ // and `data/vyg/old-meetings/` is subsumed by the broader `data/vyg/`.
3233
+ expect(scope.prefixSet).toEqual([
3234
+ "company.yaml",
3235
+ "data/vyg/",
3236
+ "design-pack/",
3237
+ "knowledge/README.md",
3238
+ ]);
3239
+ });
3240
+
3241
+ it("a bare '*' grant resolves to everything (empty-string prefix)", async () => {
3242
+ const scope = await resolvePullScope(
3243
+ stubClient({
3244
+ listMyMemberships: async () => [membership("cmp_a", "mk_a")],
3245
+ getMembershipSyncConfig: async () => ({
3246
+ membershipId: "mk_a",
3247
+ syncMode: "shared",
3248
+ isDefault: false,
3249
+ }),
3250
+ listMyExplicitGrants: async () =>
3251
+ [{ companyUid: "cmp_a", path: "*", permission: "admin", source: "group" }] as never,
3252
+ }),
3253
+ "cmp_a",
3254
+ "acme",
3255
+ );
3256
+ // A `*` grant = everything. Because coalescePrefixes drops the empty
3257
+ // prefix (which would otherwise collapse to "nothing" and prune the
3258
+ // tree), an everything-scope resolves to full-access `all`.
3259
+ expect(scope).toEqual({ syncMode: "all" });
3260
+ });
3261
+
3262
+ it("uses customPaths for a custom-mode membership", async () => {
3263
+ const scope = await resolvePullScope(
3264
+ stubClient({
3265
+ listMyMemberships: async () => [membership("cmp_a", "mk_a")],
3266
+ getMembershipSyncConfig: async () => ({
3267
+ membershipId: "mk_a",
3268
+ syncMode: "custom",
3269
+ customPaths: ["projects/x/", "projects/x/deep/"],
3270
+ isDefault: false,
3271
+ }),
3272
+ }),
3273
+ "cmp_a",
3274
+ "acme",
3275
+ );
3276
+ expect(scope.syncMode).toBe("custom");
3277
+ expect(scope.prefixSet).toEqual(["projects/x/"]);
3278
+ });
3279
+
3280
+ it("degrades to all for shared mode when listMyExplicitGrants is unavailable (never empty-prune)", async () => {
3281
+ const scope = await resolvePullScope(
3282
+ stubClient({
3283
+ listMyMemberships: async () => [membership("cmp_a", "mk_a")],
3284
+ getMembershipSyncConfig: async () => ({
3285
+ membershipId: "mk_a",
3286
+ syncMode: "shared",
3287
+ isDefault: false,
3288
+ }),
3289
+ // listMyExplicitGrants intentionally absent.
3290
+ }),
3291
+ "cmp_a",
3292
+ "acme",
3293
+ );
3294
+ expect(scope).toEqual({ syncMode: "all" });
3295
+ });
3296
+
3297
+ it("allows a genuinely-empty shared scope (grants method present, returns [])", async () => {
3298
+ const scope = await resolvePullScope(
3299
+ stubClient({
3300
+ listMyMemberships: async () => [membership("cmp_a", "mk_a")],
3301
+ getMembershipSyncConfig: async () => ({
3302
+ membershipId: "mk_a",
3303
+ syncMode: "shared",
3304
+ isDefault: false,
3305
+ }),
3306
+ listMyExplicitGrants: async () => [],
3307
+ }),
3308
+ "cmp_a",
3309
+ "acme",
3310
+ );
3311
+ expect(scope).toEqual({ syncMode: "shared", prefixSet: [] });
3312
+ });
3313
+
3314
+ it("degrades to all when the membership is not found", async () => {
3315
+ const scope = await resolvePullScope(
3316
+ stubClient({
3317
+ listMyMemberships: async () => [membership("cmp_other", "mk_o")],
3318
+ getMembershipSyncConfig: async () => {
3319
+ throw new Error("should not be called");
3320
+ },
3321
+ }),
3322
+ "cmp_a",
3323
+ "acme",
3324
+ );
3325
+ expect(scope).toEqual({ syncMode: "all" });
3326
+ });
3327
+
3328
+ it("degrades to all when sync-config resolution throws (never prune on error)", async () => {
3329
+ const scope = await resolvePullScope(
3330
+ stubClient({
3331
+ listMyMemberships: async () => [membership("cmp_a", "mk_a")],
3332
+ getMembershipSyncConfig: async () => {
3333
+ throw new Error("network blip");
3334
+ },
3335
+ }),
3336
+ "cmp_a",
3337
+ "acme",
3338
+ );
3339
+ expect(scope).toEqual({ syncMode: "all" });
3340
+ });
3341
+ });