@indigoai-us/hq-cloud 6.14.18 → 6.14.20
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/.github/workflows/ci.yml +35 -0
- package/dist/bin/sync-runner-company.d.ts +7 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +21 -1
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +8 -16
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.js +138 -0
- package/dist/bin/sync-runner-planning.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +10 -2
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +201 -25
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +14 -2
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +17 -2
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +36 -6
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +550 -3
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/rescue-core.js +27 -1
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/rescue-drop-dir-symlink.test.d.ts +2 -0
- package/dist/cli/rescue-drop-dir-symlink.test.d.ts.map +1 -0
- package/dist/cli/rescue-drop-dir-symlink.test.js +206 -0
- package/dist/cli/rescue-drop-dir-symlink.test.js.map +1 -0
- package/dist/cli/share.d.ts +15 -0
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +100 -55
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +119 -6
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +19 -25
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +121 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/journal.d.ts +3 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +13 -2
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +26 -1
- package/dist/journal.test.js.map +1 -1
- package/dist/personal-vault.d.ts +12 -0
- package/dist/personal-vault.d.ts.map +1 -1
- package/dist/personal-vault.js +20 -0
- package/dist/personal-vault.js.map +1 -1
- package/dist/qmd-reindex.d.ts +88 -11
- package/dist/qmd-reindex.d.ts.map +1 -1
- package/dist/qmd-reindex.js +354 -65
- package/dist/qmd-reindex.js.map +1 -1
- package/dist/qmd-reindex.test.d.ts +4 -0
- package/dist/qmd-reindex.test.d.ts.map +1 -1
- package/dist/qmd-reindex.test.js +285 -3
- package/dist/qmd-reindex.test.js.map +1 -1
- package/dist/s3.d.ts +13 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +34 -1
- package/dist/s3.js.map +1 -1
- package/dist/s3.test.js +60 -1
- package/dist/s3.test.js.map +1 -1
- package/dist/telemetry.d.ts +22 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +79 -0
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +117 -1
- package/dist/telemetry.test.js.map +1 -1
- package/dist/watcher.d.ts +26 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +75 -12
- package/dist/watcher.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-company.ts +29 -1
- package/src/bin/sync-runner-planning.test.ts +153 -0
- package/src/bin/sync-runner-planning.ts +10 -17
- package/src/bin/sync-runner-watch-loop.ts +322 -32
- package/src/bin/sync-runner-watch-routes.ts +14 -1
- package/src/bin/sync-runner.test.ts +662 -6
- package/src/bin/sync-runner.ts +65 -7
- package/src/cli/rescue-core.ts +26 -1
- package/src/cli/rescue-drop-dir-symlink.test.ts +224 -0
- package/src/cli/share.test.ts +150 -6
- package/src/cli/share.ts +143 -48
- package/src/cli/sync.test.ts +150 -1
- package/src/cli/sync.ts +25 -29
- package/src/journal.test.ts +45 -0
- package/src/journal.ts +18 -1
- package/src/personal-vault.ts +23 -0
- package/src/qmd-reindex.test.ts +324 -4
- package/src/qmd-reindex.ts +414 -76
- package/src/s3.test.ts +79 -0
- package/src/s3.ts +48 -1
- package/src/telemetry.test.ts +128 -0
- package/src/telemetry.ts +80 -0
- package/src/watcher.ts +111 -14
- package/test/e2e/watcher-real-chokidar.test.ts +62 -2
- package/test/e2e/watcher-recursive-backend.test.ts +67 -1
package/dist/cli/share.test.js
CHANGED
|
@@ -1653,6 +1653,11 @@ describe("share", () => {
|
|
|
1653
1653
|
propagateDeletePolicy: "owned-only",
|
|
1654
1654
|
});
|
|
1655
1655
|
expect(result.filesDeleted).toBe(1);
|
|
1656
|
+
expect(result.pathResults).toContainEqual({
|
|
1657
|
+
path: "knowledge",
|
|
1658
|
+
status: "accepted",
|
|
1659
|
+
operation: "delete",
|
|
1660
|
+
});
|
|
1656
1661
|
expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "knowledge");
|
|
1657
1662
|
const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
|
|
1658
1663
|
expect(journal.files["knowledge"]).toBeUndefined();
|
|
@@ -1794,6 +1799,34 @@ describe("share", () => {
|
|
|
1794
1799
|
expect(uploadSymlink).not.toHaveBeenCalled();
|
|
1795
1800
|
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), path.join(localSkill, "SKILL.md"), ".claude/skills/local-skill/SKILL.md", undefined, expect.anything());
|
|
1796
1801
|
});
|
|
1802
|
+
it("personalMode excludes generated core mirror symlinks but keeps canonical core files", async () => {
|
|
1803
|
+
const packagePolicy = path.join(tmpDir, "core", "packages", "hq-pack-example", "policies", "generated.md");
|
|
1804
|
+
const generatedMirror = path.join(tmpDir, "core", "policies", "generated.md");
|
|
1805
|
+
const canonicalCoreFile = path.join(tmpDir, "core", "policies", "shipped.md");
|
|
1806
|
+
fs.mkdirSync(path.dirname(packagePolicy), { recursive: true });
|
|
1807
|
+
fs.mkdirSync(path.dirname(generatedMirror), { recursive: true });
|
|
1808
|
+
fs.writeFileSync(packagePolicy, "generated package policy\n");
|
|
1809
|
+
fs.symlinkSync(packagePolicy, generatedMirror);
|
|
1810
|
+
fs.writeFileSync(canonicalCoreFile, "shipped core policy\n");
|
|
1811
|
+
const result = await share({
|
|
1812
|
+
paths: [path.join(tmpDir, "core")],
|
|
1813
|
+
entityContext: makeEntityContext({
|
|
1814
|
+
uid: "prs_01PERSONALTEST",
|
|
1815
|
+
slug: "__hq_personal_vault__",
|
|
1816
|
+
bucketName: "hq-vault-prs-01PERSONALTEST",
|
|
1817
|
+
}),
|
|
1818
|
+
hqRoot: tmpDir,
|
|
1819
|
+
personalMode: true,
|
|
1820
|
+
journalSlug: "personal-generated-core-mirror",
|
|
1821
|
+
});
|
|
1822
|
+
expect(result.filesExcludedByPolicy).toBe(1);
|
|
1823
|
+
expect(result.filesUploaded).toBe(2);
|
|
1824
|
+
expect(vi.mocked(uploadSymlink)).not.toHaveBeenCalled();
|
|
1825
|
+
expect(vi.mocked(uploadFile).mock.calls.map((call) => call[2]).sort()).toEqual([
|
|
1826
|
+
"core/packages/hq-pack-example/policies/generated.md",
|
|
1827
|
+
"core/policies/shipped.md",
|
|
1828
|
+
]);
|
|
1829
|
+
});
|
|
1797
1830
|
it("personal-vault override does NOT apply to company vaults: `owned-only` on cmp_ entity still " +
|
|
1798
1831
|
"refuses to tombstone a direction:'down' entry (multi-user curation intact)", async () => {
|
|
1799
1832
|
// Sibling guard for the previous test: the override is scoped strictly
|
|
@@ -2492,7 +2525,7 @@ describe("share", () => {
|
|
|
2492
2525
|
message: expect.stringContaining("tombstone"),
|
|
2493
2526
|
});
|
|
2494
2527
|
});
|
|
2495
|
-
it("
|
|
2528
|
+
it("daemon-stopped deletion has no watcher intent and is safely refused", async () => {
|
|
2496
2529
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
2497
2530
|
fs.mkdirSync(companyRoot, { recursive: true });
|
|
2498
2531
|
// Journal entry from before remoteEtag tracking — no etag to compare
|
|
@@ -2532,6 +2565,12 @@ describe("share", () => {
|
|
|
2532
2565
|
reason: "missing-delete-intent",
|
|
2533
2566
|
});
|
|
2534
2567
|
expect(result.filesRefusedStale).toBe(1);
|
|
2568
|
+
expect(result.pathResults).toContainEqual({
|
|
2569
|
+
path: "legacy-no-etag.md",
|
|
2570
|
+
status: "refused",
|
|
2571
|
+
operation: "delete",
|
|
2572
|
+
reason: "missing-delete-intent",
|
|
2573
|
+
});
|
|
2535
2574
|
});
|
|
2536
2575
|
it("currency-gated: real-world /update-hq scenario — direction:'down' delete propagates when current", async () => {
|
|
2537
2576
|
// Regression for the exact bug that motivated the 5.24 default flip:
|
|
@@ -2742,6 +2781,44 @@ describe("share", () => {
|
|
|
2742
2781
|
// exclusion list itself is enforced by the runner (sync-runner.ts) by only
|
|
2743
2782
|
// passing in the allowed top-level directories — share() trusts its
|
|
2744
2783
|
// `paths` input.
|
|
2784
|
+
it("plans an intent-backed personal delete from an explicit missing scope root", async () => {
|
|
2785
|
+
const key = "deleted-top-level.md";
|
|
2786
|
+
const journalSlug = "personal-scoped-delete";
|
|
2787
|
+
const journalPath = path.join(stateDir, `sync-journal.${journalSlug}.json`);
|
|
2788
|
+
fs.writeFileSync(journalPath, JSON.stringify({
|
|
2789
|
+
version: "2",
|
|
2790
|
+
lastSync: new Date().toISOString(),
|
|
2791
|
+
pulls: [],
|
|
2792
|
+
files: {
|
|
2793
|
+
[key]: {
|
|
2794
|
+
hash: "captured-hash",
|
|
2795
|
+
size: 8,
|
|
2796
|
+
syncedAt: new Date().toISOString(),
|
|
2797
|
+
direction: "up",
|
|
2798
|
+
remoteEtag: "captured-etag",
|
|
2799
|
+
kind: "file",
|
|
2800
|
+
localDeleteIntent: deleteIntent("captured-etag", "captured-hash"),
|
|
2801
|
+
},
|
|
2802
|
+
},
|
|
2803
|
+
}));
|
|
2804
|
+
const result = await share({
|
|
2805
|
+
// computePersonalVaultPaths() has no path to contribute after this
|
|
2806
|
+
// top-level file is gone. The watcher-captured root is the sole scope.
|
|
2807
|
+
paths: [],
|
|
2808
|
+
deleteScopeRoots: [key],
|
|
2809
|
+
company: "acme",
|
|
2810
|
+
vaultConfig: mockConfig,
|
|
2811
|
+
hqRoot: tmpDir,
|
|
2812
|
+
personalMode: true,
|
|
2813
|
+
journalSlug,
|
|
2814
|
+
skipUnchanged: true,
|
|
2815
|
+
propagateDeletes: true,
|
|
2816
|
+
propagateDeletePolicy: "owned-only",
|
|
2817
|
+
});
|
|
2818
|
+
expect(result.filesDeleted).toBe(1);
|
|
2819
|
+
expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), key);
|
|
2820
|
+
expect(JSON.parse(fs.readFileSync(journalPath, "utf8")).files[key]).toBeUndefined();
|
|
2821
|
+
});
|
|
2745
2822
|
// ── Decommission prefixes ──────────────────────────────────────────────────
|
|
2746
2823
|
//
|
|
2747
2824
|
// `decommissionPrefixes` is the "company was promoted to its own team
|
|
@@ -3312,7 +3389,7 @@ describe("share", () => {
|
|
|
3312
3389
|
// entries as absent locally (the rest get a real file on disk).
|
|
3313
3390
|
// `direction` default "up" so the owned-only branch accepts every
|
|
3314
3391
|
// candidate (direction:"down" silently skips and never trips the guard).
|
|
3315
|
-
function setupJournal({ total, missing, direction = "up", }) {
|
|
3392
|
+
function setupJournal({ total, missing, direction = "up", withIntents = true, }) {
|
|
3316
3393
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
3317
3394
|
fs.mkdirSync(companyRoot, { recursive: true });
|
|
3318
3395
|
const files = {};
|
|
@@ -3325,7 +3402,9 @@ describe("share", () => {
|
|
|
3325
3402
|
direction,
|
|
3326
3403
|
remoteEtag: `etag-${i}`,
|
|
3327
3404
|
kind: "file",
|
|
3328
|
-
|
|
3405
|
+
...(withIntents
|
|
3406
|
+
? { localDeleteIntent: deleteIntent(`etag-${i}`, "h") }
|
|
3407
|
+
: {}),
|
|
3329
3408
|
};
|
|
3330
3409
|
if (i >= missing) {
|
|
3331
3410
|
fs.writeFileSync(path.join(companyRoot, key), "hi");
|
|
@@ -3343,7 +3422,11 @@ describe("share", () => {
|
|
|
3343
3422
|
delete process.env.HQ_SYNC_DELETE_BULK_OVERRIDE;
|
|
3344
3423
|
});
|
|
3345
3424
|
it("trips at the 11/100 boundary (11% > 10%, abs >= 10): refuses every delete and emits bulk-asymmetry event", async () => {
|
|
3346
|
-
const { companyRoot, journalPath } = setupJournal({
|
|
3425
|
+
const { companyRoot, journalPath } = setupJournal({
|
|
3426
|
+
total: 100,
|
|
3427
|
+
missing: 11,
|
|
3428
|
+
withIntents: false,
|
|
3429
|
+
});
|
|
3347
3430
|
// currency-gated: remote HEAD would match every journal etag — guard
|
|
3348
3431
|
// must trip BEFORE any HEAD round-trip occurs.
|
|
3349
3432
|
vi.mocked(headRemoteFile).mockImplementation(async (_ctx, key) => ({
|
|
@@ -3392,6 +3475,28 @@ describe("share", () => {
|
|
|
3392
3475
|
const perKeyRefusals = events.filter((e) => e.type === "delete-refused-stale-etag" && e.reason === "bulk-asymmetry");
|
|
3393
3476
|
expect(perKeyRefusals.length).toBe(11);
|
|
3394
3477
|
});
|
|
3478
|
+
it("intent-backed 11/100 subtree disappearance bypasses the breaker and deletes every covered path", async () => {
|
|
3479
|
+
const { companyRoot } = setupJournal({ total: 100, missing: 11 });
|
|
3480
|
+
vi.mocked(headRemoteFile).mockImplementation(async (_ctx, key) => ({
|
|
3481
|
+
etag: `etag-${parseInt(key.replace(/[^0-9]/g, ""), 10)}`,
|
|
3482
|
+
lastModified: new Date(),
|
|
3483
|
+
size: 5,
|
|
3484
|
+
}));
|
|
3485
|
+
const events = [];
|
|
3486
|
+
const result = await share({
|
|
3487
|
+
paths: [companyRoot],
|
|
3488
|
+
company: "acme",
|
|
3489
|
+
vaultConfig: mockConfig,
|
|
3490
|
+
hqRoot: tmpDir,
|
|
3491
|
+
skipUnchanged: true,
|
|
3492
|
+
propagateDeletes: true,
|
|
3493
|
+
propagateDeletePolicy: "currency-gated",
|
|
3494
|
+
onEvent: (e) => events.push(e),
|
|
3495
|
+
});
|
|
3496
|
+
expect(result.filesDeleted).toBe(11);
|
|
3497
|
+
expect(deleteRemoteFile).toHaveBeenCalledTimes(11);
|
|
3498
|
+
expect(events.find((e) => e.type === "delete-refused-bulk-asymmetry")).toBeUndefined();
|
|
3499
|
+
});
|
|
3395
3500
|
it("does NOT trip at 9/100 (below ratio): all candidates delete normally", async () => {
|
|
3396
3501
|
const { companyRoot } = setupJournal({ total: 100, missing: 9 });
|
|
3397
3502
|
vi.mocked(headRemoteFile).mockImplementation(async (_ctx, key) => ({
|
|
@@ -3449,7 +3554,11 @@ describe("share", () => {
|
|
|
3449
3554
|
expect(deleteRemoteFile).toHaveBeenCalledTimes(11);
|
|
3450
3555
|
});
|
|
3451
3556
|
it("fully-empty mirror (100/100) trips the guard — the canonical mass-delete failure mode", async () => {
|
|
3452
|
-
const { companyRoot, journalPath } = setupJournal({
|
|
3557
|
+
const { companyRoot, journalPath } = setupJournal({
|
|
3558
|
+
total: 50,
|
|
3559
|
+
missing: 50,
|
|
3560
|
+
withIntents: false,
|
|
3561
|
+
});
|
|
3453
3562
|
vi.mocked(headRemoteFile).mockImplementation(async (_ctx, key) => ({
|
|
3454
3563
|
etag: `etag-${parseInt(key.replace(/[^0-9]/g, ""), 10)}`,
|
|
3455
3564
|
lastModified: new Date(),
|
|
@@ -3497,7 +3606,11 @@ describe("share", () => {
|
|
|
3497
3606
|
expect(deleteRemoteFile).toHaveBeenCalledTimes(9);
|
|
3498
3607
|
});
|
|
3499
3608
|
it("owned-only policy also goes through the guard: 11/100 missing direction:'up' entries refused", async () => {
|
|
3500
|
-
const { companyRoot, journalPath } = setupJournal({
|
|
3609
|
+
const { companyRoot, journalPath } = setupJournal({
|
|
3610
|
+
total: 100,
|
|
3611
|
+
missing: 11,
|
|
3612
|
+
withIntents: false,
|
|
3613
|
+
});
|
|
3501
3614
|
const events = [];
|
|
3502
3615
|
const result = await share({
|
|
3503
3616
|
paths: [companyRoot],
|