@indigoai-us/hq-cloud 6.14.31 → 6.14.32
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.
- package/dist/bin/sync-runner.d.ts +1 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +11 -7
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +6 -3
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/qmd-reindex.d.ts +6 -3
- package/dist/qmd-reindex.d.ts.map +1 -1
- package/dist/qmd-reindex.js +84 -7
- package/dist/qmd-reindex.js.map +1 -1
- package/dist/qmd-reindex.test.js +171 -5
- package/dist/qmd-reindex.test.js.map +1 -1
- package/dist/telemetry.d.ts +6 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +6 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +23 -0
- package/dist/telemetry.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner.test.ts +6 -3
- package/src/bin/sync-runner.ts +13 -5
- package/src/qmd-reindex.test.ts +183 -5
- package/src/qmd-reindex.ts +89 -7
- package/src/telemetry.test.ts +29 -0
- package/src/telemetry.ts +16 -4
package/src/qmd-reindex.test.ts
CHANGED
|
@@ -103,7 +103,7 @@ describe("reindexAfterSync", () => {
|
|
|
103
103
|
acquireReindexLock: noLock,
|
|
104
104
|
existsSync: () => true,
|
|
105
105
|
readCompanies: () => ["acme"],
|
|
106
|
-
hasIndexableMarkdown: () =>
|
|
106
|
+
hasIndexableMarkdown: (p) => p === path.join("/hq", "companies", "acme", "knowledge"),
|
|
107
107
|
});
|
|
108
108
|
expect(r.qmdAvailable).toBe(true);
|
|
109
109
|
expect(r.collectionsAdded).toEqual(["acme"]);
|
|
@@ -126,7 +126,7 @@ describe("reindexAfterSync", () => {
|
|
|
126
126
|
acquireReindexLock: noLock,
|
|
127
127
|
existsSync: () => true,
|
|
128
128
|
readCompanies: () => ["acme"],
|
|
129
|
-
hasIndexableMarkdown: () =>
|
|
129
|
+
hasIndexableMarkdown: (p) => p === path.join("/hq", "companies", "acme", "knowledge"),
|
|
130
130
|
realpathSync: identityRealpath,
|
|
131
131
|
});
|
|
132
132
|
expect(r.collectionsAdded).toEqual([]);
|
|
@@ -156,7 +156,7 @@ describe("reindexAfterSync", () => {
|
|
|
156
156
|
acquireReindexLock: noLock,
|
|
157
157
|
existsSync: () => true,
|
|
158
158
|
readCompanies: () => ["acme"],
|
|
159
|
-
hasIndexableMarkdown: () =>
|
|
159
|
+
hasIndexableMarkdown: (p) => p === path.join("/hq", "companies", "acme", "knowledge"),
|
|
160
160
|
realpathSync: identityRealpath,
|
|
161
161
|
env: {},
|
|
162
162
|
embed: true,
|
|
@@ -183,7 +183,7 @@ describe("reindexAfterSync", () => {
|
|
|
183
183
|
acquireReindexLock: noLock,
|
|
184
184
|
existsSync: () => true,
|
|
185
185
|
readCompanies: () => ["acme"],
|
|
186
|
-
hasIndexableMarkdown: () =>
|
|
186
|
+
hasIndexableMarkdown: (p) => p === path.join("/hq", "companies", "acme", "knowledge"),
|
|
187
187
|
realpathSync: identityRealpath,
|
|
188
188
|
env: { HQ_QMD_REPAIR_PATH_DRIFT: "1" },
|
|
189
189
|
embed: true,
|
|
@@ -238,6 +238,184 @@ describe("reindexAfterSync", () => {
|
|
|
238
238
|
expect(calls).toEqual([["status"], ["collection", "list"], ["update"]]);
|
|
239
239
|
});
|
|
240
240
|
|
|
241
|
+
it("registers populated company projects with the shell name and mask", () => {
|
|
242
|
+
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
243
|
+
const r = reindexAfterSync("/hq", {
|
|
244
|
+
exec,
|
|
245
|
+
acquireReindexLock: noLock,
|
|
246
|
+
existsSync: () => true,
|
|
247
|
+
readCompanies: () => ["acme"],
|
|
248
|
+
hasIndexableMarkdown: () => false,
|
|
249
|
+
hasIndexableProjectContent: () => true,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(r.collectionsAdded).toEqual(["acme-projects"]);
|
|
253
|
+
expect(calls).toEqual([
|
|
254
|
+
["status"],
|
|
255
|
+
["collection", "list"],
|
|
256
|
+
[
|
|
257
|
+
"collection",
|
|
258
|
+
"add",
|
|
259
|
+
path.join("/hq", "companies", "acme", "projects"),
|
|
260
|
+
"--name",
|
|
261
|
+
"acme-projects",
|
|
262
|
+
"--mask",
|
|
263
|
+
"**/*.{md,json}",
|
|
264
|
+
],
|
|
265
|
+
["context", "add", "qmd://acme-projects", "Project PRDs and documentation for acme."],
|
|
266
|
+
["update"],
|
|
267
|
+
]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("skips missing and empty company projects dirs", () => {
|
|
271
|
+
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
272
|
+
const emptyProjects = path.join("/hq", "companies", "empty", "projects");
|
|
273
|
+
const r = reindexAfterSync("/hq", {
|
|
274
|
+
exec,
|
|
275
|
+
acquireReindexLock: noLock,
|
|
276
|
+
existsSync: (p) => p !== path.join("/hq", "companies", "missing", "projects"),
|
|
277
|
+
readCompanies: () => ["missing", "empty"],
|
|
278
|
+
hasIndexableMarkdown: () => false,
|
|
279
|
+
hasIndexableProjectContent: (p) => p !== emptyProjects,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(r.collectionsAdded).toEqual([]);
|
|
283
|
+
expect(calls).toEqual([["status"], ["collection", "list"], ["update"]]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("registers populated personal knowledge with the shell name and mask", () => {
|
|
287
|
+
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
288
|
+
const personalKnowledge = path.join("/hq", "personal", "knowledge");
|
|
289
|
+
const r = reindexAfterSync("/hq", {
|
|
290
|
+
exec,
|
|
291
|
+
acquireReindexLock: noLock,
|
|
292
|
+
existsSync: () => true,
|
|
293
|
+
readCompanies: () => [],
|
|
294
|
+
hasIndexableMarkdown: (p) => p === personalKnowledge,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
expect(r.collectionsAdded).toEqual(["personal-knowledge"]);
|
|
298
|
+
expect(calls).toEqual([
|
|
299
|
+
["status"],
|
|
300
|
+
["collection", "list"],
|
|
301
|
+
[
|
|
302
|
+
"collection",
|
|
303
|
+
"add",
|
|
304
|
+
personalKnowledge,
|
|
305
|
+
"--name",
|
|
306
|
+
"personal-knowledge",
|
|
307
|
+
"--mask",
|
|
308
|
+
"**/*.md",
|
|
309
|
+
],
|
|
310
|
+
["context", "add", "qmd://personal-knowledge", "Personal knowledge base (owner overlay)."],
|
|
311
|
+
["update"],
|
|
312
|
+
]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("skips absent and empty personal knowledge", () => {
|
|
316
|
+
const absent = fakeExec({ listStdout: "" });
|
|
317
|
+
const absentResult = reindexAfterSync("/hq", {
|
|
318
|
+
exec: absent.exec,
|
|
319
|
+
acquireReindexLock: noLock,
|
|
320
|
+
existsSync: (p) => p !== path.join("/hq", "personal", "knowledge"),
|
|
321
|
+
readCompanies: () => [],
|
|
322
|
+
});
|
|
323
|
+
expect(absentResult.collectionsAdded).toEqual([]);
|
|
324
|
+
expect(absent.calls).toEqual([["status"], ["collection", "list"], ["update"]]);
|
|
325
|
+
|
|
326
|
+
const empty = fakeExec({ listStdout: "" });
|
|
327
|
+
const emptyResult = reindexAfterSync("/hq", {
|
|
328
|
+
exec: empty.exec,
|
|
329
|
+
acquireReindexLock: noLock,
|
|
330
|
+
existsSync: () => true,
|
|
331
|
+
readCompanies: () => [],
|
|
332
|
+
hasIndexableMarkdown: () => false,
|
|
333
|
+
});
|
|
334
|
+
expect(emptyResult.collectionsAdded).toEqual([]);
|
|
335
|
+
expect(empty.calls).toEqual([["status"], ["collection", "list"], ["update"]]);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("does not add projects or personal knowledge a second time", () => {
|
|
339
|
+
let listCount = 0;
|
|
340
|
+
const { exec, calls } = fakeExec({
|
|
341
|
+
resultFor: (args) => {
|
|
342
|
+
if (args[0] !== "collection" || args[1] !== "list") return undefined;
|
|
343
|
+
listCount++;
|
|
344
|
+
return {
|
|
345
|
+
status: 0,
|
|
346
|
+
stdout:
|
|
347
|
+
listCount === 1
|
|
348
|
+
? ""
|
|
349
|
+
: "acme (qmd://acme/)\nacme-projects (qmd://acme-projects/)\npersonal-knowledge (qmd://personal-knowledge/)\n",
|
|
350
|
+
};
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
const options = {
|
|
354
|
+
exec,
|
|
355
|
+
acquireReindexLock: noLock,
|
|
356
|
+
existsSync: () => true,
|
|
357
|
+
readCompanies: () => ["acme"],
|
|
358
|
+
hasIndexableMarkdown: () => true,
|
|
359
|
+
hasIndexableProjectContent: () => true,
|
|
360
|
+
realpathSync: identityRealpath,
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
expect(reindexAfterSync("/hq", options).collectionsAdded).toEqual([
|
|
364
|
+
"acme",
|
|
365
|
+
"acme-projects",
|
|
366
|
+
"personal-knowledge",
|
|
367
|
+
]);
|
|
368
|
+
expect(reindexAfterSync("/hq", options).collectionsAdded).toEqual([]);
|
|
369
|
+
expect(calls.filter((c) => c[0] === "collection" && c[1] === "add")).toHaveLength(3);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("emits the same collection-add commands as the shell for a fixture tree", () => {
|
|
373
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-qmd-parity-"));
|
|
374
|
+
try {
|
|
375
|
+
const hq = path.join(dir, "hq");
|
|
376
|
+
const acmeKnowledge = path.join(hq, "companies", "acme", "knowledge");
|
|
377
|
+
const acmeProjects = path.join(hq, "companies", "acme", "projects");
|
|
378
|
+
const emptyProjects = path.join(hq, "companies", "empty", "projects");
|
|
379
|
+
const personalKnowledge = path.join(hq, "personal", "knowledge");
|
|
380
|
+
fs.mkdirSync(path.join(hq, "core"), { recursive: true });
|
|
381
|
+
fs.mkdirSync(acmeKnowledge, { recursive: true });
|
|
382
|
+
fs.mkdirSync(acmeProjects, { recursive: true });
|
|
383
|
+
fs.mkdirSync(emptyProjects, { recursive: true });
|
|
384
|
+
fs.mkdirSync(personalKnowledge, { recursive: true });
|
|
385
|
+
fs.writeFileSync(path.join(hq, "core", "core.yaml"), "hqVersion: 1\n");
|
|
386
|
+
fs.writeFileSync(path.join(acmeKnowledge, "note.md"), "# knowledge\n");
|
|
387
|
+
fs.writeFileSync(path.join(acmeProjects, "prd.json"), "{}\n");
|
|
388
|
+
fs.writeFileSync(path.join(personalKnowledge, "note.md"), "# personal\n");
|
|
389
|
+
|
|
390
|
+
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
391
|
+
reindexAfterSync(hq, { exec, acquireReindexLock: noLock });
|
|
392
|
+
|
|
393
|
+
expect(calls.filter((c) => c[0] === "collection" && c[1] === "add")).toEqual([
|
|
394
|
+
["collection", "add", acmeKnowledge, "--name", "acme", "--mask", "**/*.md"],
|
|
395
|
+
[
|
|
396
|
+
"collection",
|
|
397
|
+
"add",
|
|
398
|
+
acmeProjects,
|
|
399
|
+
"--name",
|
|
400
|
+
"acme-projects",
|
|
401
|
+
"--mask",
|
|
402
|
+
"**/*.{md,json}",
|
|
403
|
+
],
|
|
404
|
+
[
|
|
405
|
+
"collection",
|
|
406
|
+
"add",
|
|
407
|
+
personalKnowledge,
|
|
408
|
+
"--name",
|
|
409
|
+
"personal-knowledge",
|
|
410
|
+
"--mask",
|
|
411
|
+
"**/*.md",
|
|
412
|
+
],
|
|
413
|
+
]);
|
|
414
|
+
} finally {
|
|
415
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
241
419
|
it("skips qmd entirely when provided changed paths do not touch knowledge markdown", () => {
|
|
242
420
|
const { exec, calls } = fakeExec({ listStdout: "" });
|
|
243
421
|
const r = reindexAfterSync("/hq", {
|
|
@@ -262,7 +440,7 @@ describe("reindexAfterSync", () => {
|
|
|
262
440
|
acquireReindexLock: noLock,
|
|
263
441
|
existsSync: () => true,
|
|
264
442
|
readCompanies: () => ["acme"],
|
|
265
|
-
hasIndexableMarkdown: () =>
|
|
443
|
+
hasIndexableMarkdown: (p) => p === path.join("/hq", "companies", "acme", "knowledge"),
|
|
266
444
|
changedPaths: ["companies/acme/knowledge/note.md"],
|
|
267
445
|
debounceMs: 60_000,
|
|
268
446
|
nowMs: 100_000,
|
package/src/qmd-reindex.ts
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
* script inside the synced HQ tree (which may be stale).
|
|
16
16
|
*
|
|
17
17
|
* What it does, best-effort and idempotent:
|
|
18
|
-
* 1. Auto-registers
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* 1. Auto-registers populated company knowledge, company project, and
|
|
19
|
+
* personal knowledge dirs that aren't yet qmd collections (kills the
|
|
20
|
+
* manual "map" step). Company knowledge detects path drift when the name
|
|
21
|
+
* exists but points elsewhere; repairs only when
|
|
21
22
|
* `HQ_QMD_REPAIR_PATH_DRIFT=1` (non-breaking default: detect-only).
|
|
22
23
|
* 2. Runs an incremental lexical `qmd update` (fast — qmd skips unchanged
|
|
23
24
|
* files by mtime).
|
|
@@ -395,6 +396,8 @@ export interface ReindexOptions {
|
|
|
395
396
|
readCompanies?: (companiesDir: string) => string[];
|
|
396
397
|
/** Returns true if the knowledge dir has at least one indexable .md file. */
|
|
397
398
|
hasIndexableMarkdown?: (knowledgeDir: string) => boolean;
|
|
399
|
+
/** Returns true if the projects dir has at least one indexable .md or .json file. */
|
|
400
|
+
hasIndexableProjectContent?: (projectsDir: string) => boolean;
|
|
398
401
|
/** Reindex-lock acquirer override for tests. */
|
|
399
402
|
acquireReindexLock?: AcquireReindexLock;
|
|
400
403
|
/** Corrupt-index quarantine override for tests. */
|
|
@@ -474,7 +477,7 @@ export function reindexAfterSync(
|
|
|
474
477
|
const changedPaths = opts.changedPaths;
|
|
475
478
|
const dirtyFromChanges =
|
|
476
479
|
changedPaths === undefined ||
|
|
477
|
-
changedPaths.some(
|
|
480
|
+
changedPaths.some(isQmdIndexableContentPath);
|
|
478
481
|
const registrationMayBeStale =
|
|
479
482
|
opts.forceCollectionRefresh === true ||
|
|
480
483
|
changedPaths === undefined ||
|
|
@@ -528,6 +531,9 @@ export function reindexAfterSync(
|
|
|
528
531
|
result.qmdAvailable = true;
|
|
529
532
|
const existingCollections = list.stdout;
|
|
530
533
|
let repairedThisCycle = false;
|
|
534
|
+
const hasIndexableMarkdown = opts.hasIndexableMarkdown ?? defaultHasIndexableMarkdown;
|
|
535
|
+
const hasIndexableProjectContent =
|
|
536
|
+
opts.hasIndexableProjectContent ?? defaultHasIndexableProjectContent;
|
|
531
537
|
|
|
532
538
|
// 1. Auto-register missing / reconcile drifted company knowledge collections.
|
|
533
539
|
if (registrationMayBeStale) {
|
|
@@ -537,7 +543,7 @@ export function reindexAfterSync(
|
|
|
537
543
|
for (const slug of slugs) {
|
|
538
544
|
const knowledgeDir = path.join(companiesDir, slug, "knowledge");
|
|
539
545
|
if (!existsSync(knowledgeDir)) continue;
|
|
540
|
-
const hasMd =
|
|
546
|
+
const hasMd = hasIndexableMarkdown(knowledgeDir);
|
|
541
547
|
if (!hasMd) continue;
|
|
542
548
|
|
|
543
549
|
const namePresent = existingCollections.includes(`qmd://${slug}/`);
|
|
@@ -601,6 +607,58 @@ export function reindexAfterSync(
|
|
|
601
607
|
result.collectionsRepaired.push(slug);
|
|
602
608
|
repairedThisCycle = true;
|
|
603
609
|
}
|
|
610
|
+
|
|
611
|
+
// 1b. Auto-register company projects collections. Match the shell
|
|
612
|
+
// post-sync convention exactly so prd.json and project docs are searchable.
|
|
613
|
+
for (const slug of slugs) {
|
|
614
|
+
const projectsDir = path.join(companiesDir, slug, "projects");
|
|
615
|
+
if (!existsSync(projectsDir)) continue;
|
|
616
|
+
if (!hasIndexableProjectContent(projectsDir)) continue;
|
|
617
|
+
|
|
618
|
+
const name = `${slug}-projects`;
|
|
619
|
+
if (existingCollections.includes(`qmd://${name}/`)) continue;
|
|
620
|
+
const add = exec([
|
|
621
|
+
"collection",
|
|
622
|
+
"add",
|
|
623
|
+
projectsDir,
|
|
624
|
+
"--name",
|
|
625
|
+
name,
|
|
626
|
+
"--mask",
|
|
627
|
+
"**/*.{md,json}",
|
|
628
|
+
]);
|
|
629
|
+
if (add.status === 0) {
|
|
630
|
+
exec(["context", "add", `qmd://${name}`, `Project PRDs and documentation for ${slug}.`]);
|
|
631
|
+
result.collectionsAdded.push(name);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// 1c. Auto-register the owner overlay's personal knowledge collection.
|
|
636
|
+
const personalKnowledgeDir = path.join(hqRoot, "personal", "knowledge");
|
|
637
|
+
const personalKnowledgeName = "personal-knowledge";
|
|
638
|
+
if (
|
|
639
|
+
existsSync(personalKnowledgeDir) &&
|
|
640
|
+
hasIndexableMarkdown(personalKnowledgeDir) &&
|
|
641
|
+
!existingCollections.includes(`qmd://${personalKnowledgeName}/`)
|
|
642
|
+
) {
|
|
643
|
+
const add = exec([
|
|
644
|
+
"collection",
|
|
645
|
+
"add",
|
|
646
|
+
personalKnowledgeDir,
|
|
647
|
+
"--name",
|
|
648
|
+
personalKnowledgeName,
|
|
649
|
+
"--mask",
|
|
650
|
+
"**/*.md",
|
|
651
|
+
]);
|
|
652
|
+
if (add.status === 0) {
|
|
653
|
+
exec([
|
|
654
|
+
"context",
|
|
655
|
+
"add",
|
|
656
|
+
`qmd://${personalKnowledgeName}`,
|
|
657
|
+
"Personal knowledge base (owner overlay).",
|
|
658
|
+
]);
|
|
659
|
+
result.collectionsAdded.push(personalKnowledgeName);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
604
662
|
}
|
|
605
663
|
|
|
606
664
|
// 2. Incremental lexical reindex.
|
|
@@ -721,9 +779,13 @@ function writeState(statePath: string, state: QmdReindexState): void {
|
|
|
721
779
|
}
|
|
722
780
|
}
|
|
723
781
|
|
|
724
|
-
function
|
|
782
|
+
function isQmdIndexableContentPath(relPath: string): boolean {
|
|
725
783
|
const normalized = relPath.split(path.sep).join("/");
|
|
726
|
-
return
|
|
784
|
+
return (
|
|
785
|
+
/^companies\/[^/]+\/knowledge\/.+\.md$/i.test(normalized) ||
|
|
786
|
+
/^companies\/[^/]+\/projects\/.+\.(?:md|json)$/i.test(normalized) ||
|
|
787
|
+
/^personal\/knowledge\/.+\.md$/i.test(normalized)
|
|
788
|
+
);
|
|
727
789
|
}
|
|
728
790
|
|
|
729
791
|
function defaultReadCompanies(companiesDir: string): string[] {
|
|
@@ -758,3 +820,23 @@ function defaultHasIndexableMarkdown(knowledgeDir: string): boolean {
|
|
|
758
820
|
}
|
|
759
821
|
return false;
|
|
760
822
|
}
|
|
823
|
+
|
|
824
|
+
function defaultHasIndexableProjectContent(projectsDir: string): boolean {
|
|
825
|
+
// Match the shell's `find -type f \( -name '*.md' -o -name '*.json' \)`
|
|
826
|
+
// guard: project documentation and PRD JSON both make the collection useful.
|
|
827
|
+
const stack = [projectsDir];
|
|
828
|
+
while (stack.length) {
|
|
829
|
+
const dir = stack.pop()!;
|
|
830
|
+
let entries: fs.Dirent[];
|
|
831
|
+
try {
|
|
832
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
833
|
+
} catch {
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
for (const e of entries) {
|
|
837
|
+
if (e.isFile() && (e.name.endsWith(".md") || e.name.endsWith(".json"))) return true;
|
|
838
|
+
if (e.isDirectory()) stack.push(path.join(dir, e.name));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return false;
|
|
842
|
+
}
|
package/src/telemetry.test.ts
CHANGED
|
@@ -978,6 +978,35 @@ describe("collectAndSendTelemetry — companyUid attribution", () => {
|
|
|
978
978
|
expect("companyUid" in client.posts[0].events[0]).toBe(false);
|
|
979
979
|
});
|
|
980
980
|
|
|
981
|
+
it("uses the explicit single-company fallback when cwd is the shared HQ root", async () => {
|
|
982
|
+
const client = makeClient();
|
|
983
|
+
writeJsonl(env, "proj", "s.jsonl", [rowWithCwd("u1", hqRoot)]);
|
|
984
|
+
|
|
985
|
+
await collectAndSendTelemetry({
|
|
986
|
+
...makeOpts(env, client),
|
|
987
|
+
hqRoot,
|
|
988
|
+
fallbackCompany: "indigo",
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
expect(client.posts).toHaveLength(1);
|
|
992
|
+
expect(client.posts[0].events[0].companyUid).toBe(COMPANY_UID);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
it("prefers the cwd-resolved company over the single-company fallback", async () => {
|
|
996
|
+
const client = makeClient();
|
|
997
|
+
const inRepo = path.join(hqRoot, "repos/private/hq-cloud");
|
|
998
|
+
writeJsonl(env, "proj", "s.jsonl", [rowWithCwd("u1", inRepo)]);
|
|
999
|
+
|
|
1000
|
+
await collectAndSendTelemetry({
|
|
1001
|
+
...makeOpts(env, client),
|
|
1002
|
+
hqRoot,
|
|
1003
|
+
fallbackCompany: "cmp_01OTHER",
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
expect(client.posts).toHaveLength(1);
|
|
1007
|
+
expect(client.posts[0].events[0].companyUid).toBe(COMPANY_UID);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
981
1010
|
it("never sends the reserved 'unattributed' value", async () => {
|
|
982
1011
|
const client = makeClient();
|
|
983
1012
|
writeJsonl(env, "proj", "s.jsonl", [
|
package/src/telemetry.ts
CHANGED
|
@@ -73,6 +73,12 @@ export interface CollectTelemetryOptions {
|
|
|
73
73
|
* server treats the event as unattributed/personal.
|
|
74
74
|
*/
|
|
75
75
|
hqRoot?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Explicit single-company sync scope (`--company <slug-or-uid>`). Used only
|
|
78
|
+
* when an event cwd does not resolve through the manifest. This is intentionally
|
|
79
|
+
* absent for multi-company and personal runs to prevent cross-tenant attribution.
|
|
80
|
+
*/
|
|
81
|
+
fallbackCompany?: string;
|
|
76
82
|
/** Override `~/.claude/projects` for tests. */
|
|
77
83
|
claudeProjectsRoot?: string;
|
|
78
84
|
/** Override `~/.codex` for tests. Both live and archived rollouts are scanned. */
|
|
@@ -687,6 +693,11 @@ export async function collectAndSendTelemetry(
|
|
|
687
693
|
const repoCompanyMap: RepoCompanyMap = opts.hqRoot
|
|
688
694
|
? await buildRepoCompanyMap(opts.hqRoot)
|
|
689
695
|
: { entries: [], bySlug: new Map(), foldsCase: false, ambiguous: new Set<string>() };
|
|
696
|
+
const fallbackCompanyUid = opts.fallbackCompany?.startsWith("cmp_")
|
|
697
|
+
? opts.fallbackCompany
|
|
698
|
+
: opts.fallbackCompany
|
|
699
|
+
? repoCompanyMap.bySlug.get(opts.fallbackCompany)
|
|
700
|
+
: undefined;
|
|
690
701
|
|
|
691
702
|
// 1. Opt-in check (server-authoritative, with local fallback + self-heal).
|
|
692
703
|
let enabled: boolean;
|
|
@@ -1012,10 +1023,11 @@ export async function collectAndSendTelemetry(
|
|
|
1012
1023
|
sourceRow && typeof sourceRow === "object" && !Array.isArray(sourceRow)
|
|
1013
1024
|
? (sourceRow as Record<string, unknown>).cwd
|
|
1014
1025
|
: undefined;
|
|
1015
|
-
const companyUid =
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1026
|
+
const companyUid =
|
|
1027
|
+
resolveCompanyForCwd(
|
|
1028
|
+
typeof rowCwd === "string" ? rowCwd : undefined,
|
|
1029
|
+
repoCompanyMap,
|
|
1030
|
+
) ?? fallbackCompanyUid;
|
|
1019
1031
|
const sanitized = sanitizeRow(sourceRow, companyUid);
|
|
1020
1032
|
if (!sanitized) {
|
|
1021
1033
|
batchSources.push(source());
|