@indigoai-us/hq-cloud 6.14.17 → 6.14.18
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/active-company.d.ts +43 -0
- package/dist/active-company.d.ts.map +1 -0
- package/dist/active-company.js +132 -0
- package/dist/active-company.js.map +1 -0
- package/dist/active-company.test.d.ts +2 -0
- package/dist/active-company.test.d.ts.map +1 -0
- package/dist/active-company.test.js +149 -0
- package/dist/active-company.test.js.map +1 -0
- package/dist/bin/sync-runner-planning.d.ts +27 -1
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +33 -4
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.d.ts +2 -0
- package/dist/bin/sync-runner-planning.test.d.ts.map +1 -0
- package/dist/bin/sync-runner-planning.test.js +115 -0
- package/dist/bin/sync-runner-planning.test.js.map +1 -0
- package/dist/bin/sync-runner.d.ts +22 -7
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +92 -16
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +357 -5
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/manifest-reconcile.d.ts +118 -5
- package/dist/manifest-reconcile.d.ts.map +1 -1
- package/dist/manifest-reconcile.js +319 -62
- package/dist/manifest-reconcile.js.map +1 -1
- package/dist/manifest-reconcile.test.js +824 -2
- package/dist/manifest-reconcile.test.js.map +1 -1
- package/package.json +1 -1
- package/pnpm-workspace.yaml +1 -1
- package/src/active-company.test.ts +188 -0
- package/src/active-company.ts +168 -0
- package/src/bin/sync-runner-planning.test.ts +131 -0
- package/src/bin/sync-runner-planning.ts +60 -7
- package/src/bin/sync-runner.test.ts +430 -10
- package/src/bin/sync-runner.ts +129 -23
- package/src/manifest-reconcile.test.ts +1019 -3
- package/src/manifest-reconcile.ts +424 -66
- package/test/joiner-manifest-reconcile.integration.test.ts +283 -0
package/src/bin/sync-runner.ts
CHANGED
|
@@ -140,10 +140,18 @@ import {
|
|
|
140
140
|
buildFanoutPlan,
|
|
141
141
|
emitFanoutPlan,
|
|
142
142
|
resolveMembershipsForRun,
|
|
143
|
+
type RunnerTarget,
|
|
144
|
+
type SetupNeededReason,
|
|
143
145
|
} from "./sync-runner-planning.js";
|
|
146
|
+
import {
|
|
147
|
+
seedActiveCompany,
|
|
148
|
+
type SeedActiveCompanyOptions,
|
|
149
|
+
type SeedActiveCompanyResult,
|
|
150
|
+
} from "../active-company.js";
|
|
144
151
|
import {
|
|
145
152
|
reconcileCompanyManifest,
|
|
146
153
|
type ManifestReconcileOptions,
|
|
154
|
+
type ManifestReconcileResult,
|
|
147
155
|
} from "../manifest-reconcile.js";
|
|
148
156
|
import { executeCompanyFanout } from "./sync-runner-company.js";
|
|
149
157
|
import { rollupAllComplete } from "./sync-runner-rollup.js";
|
|
@@ -333,11 +341,28 @@ export { PERSONAL_VAULT_EXCLUDED_TOP_LEVEL, computePersonalVaultPaths };
|
|
|
333
341
|
* specific company".
|
|
334
342
|
*/
|
|
335
343
|
export type RunnerEvent =
|
|
336
|
-
| {
|
|
344
|
+
| {
|
|
345
|
+
/**
|
|
346
|
+
* The run cannot proceed. `reason` and `pendingInviteCount` are additive
|
|
347
|
+
* and optional by design: a consumer that only reads `type` behaves
|
|
348
|
+
* exactly as before, while /setup and /hq-sync can tell the user WHY they
|
|
349
|
+
* look solo — most usefully, that an invite is still waiting.
|
|
350
|
+
*/
|
|
351
|
+
type: "setup-needed";
|
|
352
|
+
reason?: SetupNeededReason;
|
|
353
|
+
pendingInviteCount?: number;
|
|
354
|
+
}
|
|
337
355
|
| { type: "auth-error"; message: string }
|
|
338
356
|
| {
|
|
357
|
+
/**
|
|
358
|
+
* The resolved fanout plan. Carries whole `RunnerTarget`s: the runtime
|
|
359
|
+
* payload has always been the plan entries verbatim, so the previous
|
|
360
|
+
* `{uid, slug, name?}` declaration understated the wire contract (it
|
|
361
|
+
* already omitted `bucketName`, `personalMode`, and `journalSlug`).
|
|
362
|
+
* Consumers deserialize leniently and ignore fields they do not know.
|
|
363
|
+
*/
|
|
339
364
|
type: "fanout-plan";
|
|
340
|
-
companies:
|
|
365
|
+
companies: RunnerTarget[];
|
|
341
366
|
}
|
|
342
367
|
| ({
|
|
343
368
|
/**
|
|
@@ -632,7 +657,9 @@ export interface RunnerDeps {
|
|
|
632
657
|
*/
|
|
633
658
|
reconcileManifest?: (
|
|
634
659
|
options: ManifestReconcileOptions,
|
|
635
|
-
) => Promise<
|
|
660
|
+
) => Promise<ManifestReconcileResult>;
|
|
661
|
+
/** Internal test seam for `.hq/config.json` activeCompany seeding. */
|
|
662
|
+
seedActiveCompany?: (options: SeedActiveCompanyOptions) => SeedActiveCompanyResult;
|
|
636
663
|
/** Internal: set when runRunner is invoked under the per-root operation lock. */
|
|
637
664
|
operationLockAlreadyHeld?: boolean;
|
|
638
665
|
/** Internal watch-loop result override; one-shot callers keep exit 0. */
|
|
@@ -698,10 +725,11 @@ async function runClaimDance(
|
|
|
698
725
|
client: VaultClientSurface,
|
|
699
726
|
claims: IdTokenClaims,
|
|
700
727
|
stderr: { write: (chunk: string) => boolean | void },
|
|
701
|
-
|
|
728
|
+
reportDiagnostic: RunnerDiagnosticReporter = () => undefined,
|
|
729
|
+
): Promise<number> {
|
|
702
730
|
try {
|
|
703
731
|
const pending = await client.listMyPendingInvitesByEmail();
|
|
704
|
-
if (pending.length === 0) return;
|
|
732
|
+
if (pending.length === 0) return 0;
|
|
705
733
|
|
|
706
734
|
const displayName =
|
|
707
735
|
claims.name ??
|
|
@@ -710,10 +738,15 @@ async function runClaimDance(
|
|
|
710
738
|
"";
|
|
711
739
|
const ownerSub = claims.sub ?? "";
|
|
712
740
|
if (!ownerSub || !displayName) {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
741
|
+
reportDiagnostic({
|
|
742
|
+
component: "claim-dance",
|
|
743
|
+
event: "runner.claim_dance.skipped",
|
|
744
|
+
message: "skipping claim-dance — idToken missing sub/name",
|
|
745
|
+
context: { pendingInviteCount: pending.length },
|
|
746
|
+
});
|
|
747
|
+
// The invites are real and still pending; the caller must be able to say
|
|
748
|
+
// so even though we could not claim them.
|
|
749
|
+
return pending.length;
|
|
717
750
|
}
|
|
718
751
|
|
|
719
752
|
const person = await client.ensureMyPersonEntity({
|
|
@@ -721,9 +754,19 @@ async function runClaimDance(
|
|
|
721
754
|
displayName,
|
|
722
755
|
});
|
|
723
756
|
await client.claimPendingInvitesByEmail(person.uid);
|
|
757
|
+
return pending.length;
|
|
724
758
|
} catch (err) {
|
|
725
|
-
|
|
726
|
-
|
|
759
|
+
// Non-throwing by contract: a claim-dance failure must never take down a
|
|
760
|
+
// sync. But it stops being invisible — a systematic failure here is exactly
|
|
761
|
+
// what makes a joiner look permanently solo.
|
|
762
|
+
reportDiagnostic({
|
|
763
|
+
component: "claim-dance",
|
|
764
|
+
event: "runner.claim_dance.skipped",
|
|
765
|
+
message: "claim-dance skipped",
|
|
766
|
+
err,
|
|
767
|
+
context: {},
|
|
768
|
+
});
|
|
769
|
+
return 0;
|
|
727
770
|
}
|
|
728
771
|
}
|
|
729
772
|
|
|
@@ -1276,11 +1319,18 @@ export async function runRunner(
|
|
|
1276
1319
|
client,
|
|
1277
1320
|
claims,
|
|
1278
1321
|
stderr,
|
|
1279
|
-
runClaimDance,
|
|
1322
|
+
runClaimDance: (client, claims, stderr) =>
|
|
1323
|
+
runClaimDance(client, claims, stderr, reportDiagnostic),
|
|
1280
1324
|
listMemberships: listMembershipsWithRetry,
|
|
1281
1325
|
});
|
|
1282
1326
|
if (resolution.status === "setup-needed") {
|
|
1283
|
-
emit({
|
|
1327
|
+
emit({
|
|
1328
|
+
type: "setup-needed",
|
|
1329
|
+
reason: resolution.reason,
|
|
1330
|
+
...(resolution.pendingInviteCount === undefined
|
|
1331
|
+
? {}
|
|
1332
|
+
: { pendingInviteCount: resolution.pendingInviteCount }),
|
|
1333
|
+
});
|
|
1284
1334
|
return 0;
|
|
1285
1335
|
}
|
|
1286
1336
|
memberships = resolution.memberships;
|
|
@@ -1335,7 +1385,7 @@ export async function runRunner(
|
|
|
1335
1385
|
return isTransientNetworkError(err) ? TRANSIENT_NETWORK_EXIT : 1;
|
|
1336
1386
|
}
|
|
1337
1387
|
if (targetPlan.status === "setup-needed") {
|
|
1338
|
-
emit({ type: "setup-needed" });
|
|
1388
|
+
emit({ type: "setup-needed", reason: targetPlan.reason });
|
|
1339
1389
|
return 0;
|
|
1340
1390
|
}
|
|
1341
1391
|
const plan = targetPlan.plan;
|
|
@@ -1386,19 +1436,43 @@ export async function runRunner(
|
|
|
1386
1436
|
// The personal-vault target runs last and can pull an older copy of the
|
|
1387
1437
|
// manifest. Reconcile only after the entire pull fanout has settled so the
|
|
1388
1438
|
// successfully materialized cloud directories restore their own entries.
|
|
1389
|
-
|
|
1439
|
+
//
|
|
1440
|
+
// `--companies` only. A single-company run (`--company <slug>`) and a
|
|
1441
|
+
// `--personal` run both see just one leg of the fanout, so neither has the
|
|
1442
|
+
// full picture the manifest is reconciled against; treating a partial view as
|
|
1443
|
+
// authoritative is how entries get lost. A push-only pass materializes
|
|
1444
|
+
// nothing new locally, so it has nothing to reconcile either.
|
|
1445
|
+
if (parsed.companies && parsed.direction !== "push") {
|
|
1446
|
+
const completedCompanySlugs = new Set(
|
|
1447
|
+
Array.from(fanout.stateByCompany.entries())
|
|
1448
|
+
.filter(([, state]) => state.status === "complete")
|
|
1449
|
+
.map(([slug]) => slug),
|
|
1450
|
+
);
|
|
1451
|
+
// The personal vault is never a manifest entry. The reconciler filters it
|
|
1452
|
+
// too, but that is its own internal invariant — excluding it here means
|
|
1453
|
+
// the personal target is never even handed across the seam.
|
|
1454
|
+
const companyTargets = plan.filter(
|
|
1455
|
+
(target) => target.personalMode !== true && target.slug !== "personal",
|
|
1456
|
+
);
|
|
1390
1457
|
try {
|
|
1391
|
-
const
|
|
1392
|
-
Array.from(fanout.stateByCompany.entries())
|
|
1393
|
-
.filter(([, state]) => state.status === "complete")
|
|
1394
|
-
.map(([slug]) => slug),
|
|
1395
|
-
);
|
|
1396
|
-
await (deps.reconcileManifest ?? reconcileCompanyManifest)({
|
|
1458
|
+
const result = await (deps.reconcileManifest ?? reconcileCompanyManifest)({
|
|
1397
1459
|
hqRoot: parsed.hqRoot,
|
|
1398
|
-
targets:
|
|
1460
|
+
targets: companyTargets,
|
|
1399
1461
|
completedCompanySlugs,
|
|
1400
|
-
|
|
1462
|
+
reportDiagnostic: (diagnostic) =>
|
|
1463
|
+
reportDiagnostic({ component: "manifest-reconcile", ...diagnostic }),
|
|
1401
1464
|
});
|
|
1465
|
+
// Only on an actual write. A steady-state sync stays silent, so an
|
|
1466
|
+
// unexpected diagnostic rate is itself the signal that skip-if-unchanged
|
|
1467
|
+
// has regressed.
|
|
1468
|
+
if (result.written) {
|
|
1469
|
+
reportDiagnostic({
|
|
1470
|
+
component: "manifest-reconcile",
|
|
1471
|
+
event: "manifest-reconciled",
|
|
1472
|
+
message: "restored company entries in companies/manifest.yaml",
|
|
1473
|
+
context: { added: result.added, updated: result.updated },
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1402
1476
|
} catch (err) {
|
|
1403
1477
|
reportDiagnostic({
|
|
1404
1478
|
component: "manifest-reconcile",
|
|
@@ -1408,6 +1482,38 @@ export async function runRunner(
|
|
|
1408
1482
|
context: { hqRoot: parsed.hqRoot },
|
|
1409
1483
|
});
|
|
1410
1484
|
}
|
|
1485
|
+
|
|
1486
|
+
// A joiner whose manifest entry was just restored still has nothing telling
|
|
1487
|
+
// HQ which company is current. Seed it only when the answer cannot be wrong
|
|
1488
|
+
// — deliberately its own try, so a manifest failure does not also cost the
|
|
1489
|
+
// user their routing.
|
|
1490
|
+
try {
|
|
1491
|
+
const localCompanySlugs = companyTargets
|
|
1492
|
+
.map((target) => target.slug)
|
|
1493
|
+
.filter((slug) => completedCompanySlugs.has(slug));
|
|
1494
|
+
const seeded = (deps.seedActiveCompany ?? seedActiveCompany)({
|
|
1495
|
+
hqRoot: parsed.hqRoot,
|
|
1496
|
+
companySlugs: localCompanySlugs,
|
|
1497
|
+
reportDiagnostic: (diagnostic) =>
|
|
1498
|
+
reportDiagnostic({ component: "active-company", ...diagnostic }),
|
|
1499
|
+
});
|
|
1500
|
+
if (seeded.written) {
|
|
1501
|
+
reportDiagnostic({
|
|
1502
|
+
component: "active-company",
|
|
1503
|
+
event: "active-company-seeded",
|
|
1504
|
+
message: "seeded activeCompany in .hq/config.json",
|
|
1505
|
+
context: { activeCompany: seeded.activeCompany },
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
reportDiagnostic({
|
|
1510
|
+
component: "active-company",
|
|
1511
|
+
event: "runner.active_company.failed",
|
|
1512
|
+
message: "seeding activeCompany failed",
|
|
1513
|
+
err,
|
|
1514
|
+
context: { hqRoot: parsed.hqRoot },
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1411
1517
|
}
|
|
1412
1518
|
|
|
1413
1519
|
if (rollup.needsReindex) {
|