@indigoai-us/hq-cloud 6.15.0 → 6.15.2
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-mutation.d.ts +16 -0
- package/dist/bin/sync-mutation.d.ts.map +1 -0
- package/dist/bin/sync-mutation.js +60 -0
- package/dist/bin/sync-mutation.js.map +1 -0
- package/dist/bin/sync-mutation.test.d.ts +2 -0
- package/dist/bin/sync-mutation.test.d.ts.map +1 -0
- package/dist/bin/sync-mutation.test.js +165 -0
- package/dist/bin/sync-mutation.test.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts +8 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +16 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.d.ts +2 -0
- package/dist/bin/sync-runner-company.test.d.ts.map +1 -0
- package/dist/bin/sync-runner-company.test.js +36 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +98 -8
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +17 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +109 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/conflict-recovery.test.d.ts +2 -0
- package/dist/cli/conflict-recovery.test.d.ts.map +1 -0
- package/dist/cli/conflict-recovery.test.js +201 -0
- package/dist/cli/conflict-recovery.test.js.map +1 -0
- package/dist/cli/conflict.d.ts +60 -0
- package/dist/cli/conflict.d.ts.map +1 -1
- package/dist/cli/conflict.js +333 -0
- package/dist/cli/conflict.js.map +1 -1
- package/dist/cli/sync.d.ts +35 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +100 -0
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +85 -1
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/skill-telemetry.d.ts +6 -0
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +14 -2
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +79 -0
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/sync/candidate-uploader.d.ts +88 -0
- package/dist/sync/candidate-uploader.d.ts.map +1 -0
- package/dist/sync/candidate-uploader.js +212 -0
- package/dist/sync/candidate-uploader.js.map +1 -0
- package/dist/sync/candidate-uploader.test.d.ts +2 -0
- package/dist/sync/candidate-uploader.test.d.ts.map +1 -0
- package/dist/sync/candidate-uploader.test.js +132 -0
- package/dist/sync/candidate-uploader.test.js.map +1 -0
- package/dist/sync/delta-client.d.ts +73 -0
- package/dist/sync/delta-client.d.ts.map +1 -0
- package/dist/sync/delta-client.js +201 -0
- package/dist/sync/delta-client.js.map +1 -0
- package/dist/sync/delta-client.test.d.ts +2 -0
- package/dist/sync/delta-client.test.d.ts.map +1 -0
- package/dist/sync/delta-client.test.js +97 -0
- package/dist/sync/delta-client.test.js.map +1 -0
- package/dist/sync/durable-apply.d.ts +76 -0
- package/dist/sync/durable-apply.d.ts.map +1 -0
- package/dist/sync/durable-apply.js +530 -0
- package/dist/sync/durable-apply.js.map +1 -0
- package/dist/sync/durable-apply.test.d.ts +2 -0
- package/dist/sync/durable-apply.test.d.ts.map +1 -0
- package/dist/sync/durable-apply.test.js +180 -0
- package/dist/sync/durable-apply.test.js.map +1 -0
- package/dist/sync/event-sync.d.ts +33 -1
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +149 -1
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +142 -1
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/multipart-uploader.d.ts +99 -0
- package/dist/sync/multipart-uploader.d.ts.map +1 -0
- package/dist/sync/multipart-uploader.js +447 -0
- package/dist/sync/multipart-uploader.js.map +1 -0
- package/dist/sync/multipart-uploader.test.d.ts +2 -0
- package/dist/sync/multipart-uploader.test.d.ts.map +1 -0
- package/dist/sync/multipart-uploader.test.js +119 -0
- package/dist/sync/multipart-uploader.test.js.map +1 -0
- package/dist/sync/mutation-client.d.ts +85 -0
- package/dist/sync/mutation-client.d.ts.map +1 -0
- package/dist/sync/mutation-client.js +245 -0
- package/dist/sync/mutation-client.js.map +1 -0
- package/dist/sync/mutation-client.test.d.ts +2 -0
- package/dist/sync/mutation-client.test.d.ts.map +1 -0
- package/dist/sync/mutation-client.test.js +51 -0
- package/dist/sync/mutation-client.test.js.map +1 -0
- package/dist/sync/push-receiver.d.ts +45 -0
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +101 -0
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/sync/push-receiver.test.js +54 -2
- package/dist/sync/push-receiver.test.js.map +1 -1
- package/dist/sync/scope-inventory-client.d.ts +69 -0
- package/dist/sync/scope-inventory-client.d.ts.map +1 -0
- package/dist/sync/scope-inventory-client.js +210 -0
- package/dist/sync/scope-inventory-client.js.map +1 -0
- package/dist/sync/scope-inventory-client.test.d.ts +2 -0
- package/dist/sync/scope-inventory-client.test.d.ts.map +1 -0
- package/dist/sync/scope-inventory-client.test.js +94 -0
- package/dist/sync/scope-inventory-client.test.js.map +1 -0
- package/dist/sync/snapshot-client.d.ts +98 -0
- package/dist/sync/snapshot-client.d.ts.map +1 -0
- package/dist/sync/snapshot-client.js +402 -0
- package/dist/sync/snapshot-client.js.map +1 -0
- package/dist/sync/snapshot-client.test.d.ts +2 -0
- package/dist/sync/snapshot-client.test.d.ts.map +1 -0
- package/dist/sync/snapshot-client.test.js +169 -0
- package/dist/sync/snapshot-client.test.js.map +1 -0
- package/dist/sync/uploader-finalization.d.ts +97 -0
- package/dist/sync/uploader-finalization.d.ts.map +1 -0
- package/dist/sync/uploader-finalization.js +273 -0
- package/dist/sync/uploader-finalization.js.map +1 -0
- package/dist/sync/uploader-finalization.test.d.ts +2 -0
- package/dist/sync/uploader-finalization.test.d.ts.map +1 -0
- package/dist/sync/uploader-finalization.test.js +92 -0
- package/dist/sync/uploader-finalization.test.js.map +1 -0
- package/dist/telemetry.d.ts +11 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +21 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +80 -0
- package/dist/telemetry.test.js.map +1 -1
- package/package.json +6 -1
- package/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +0 -30
- package/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +0 -22
- package/.github/workflows/ci.yml +0 -84
- package/.github/workflows/publish.yml +0 -56
- package/.github/workflows/unreleased-commits-nag.yml +0 -256
- package/eslint.config.js +0 -67
- package/pnpm-workspace.yaml +0 -2
- package/scripts/presign-transport-e2e.mjs +0 -250
- package/scripts/vault-rebaseline.sh +0 -323
- package/scripts/vault-rescue.sh +0 -332
- package/src/active-company.test.ts +0 -188
- package/src/active-company.ts +0 -168
- package/src/agent-codex-instructions.test.ts +0 -332
- package/src/agent-codex-instructions.ts +0 -309
- package/src/auth.ts +0 -146
- package/src/backup-prune.test.ts +0 -98
- package/src/backup-prune.ts +0 -182
- package/src/bin/backup-prune-runner.ts +0 -33
- package/src/bin/rescue-runner.ts +0 -25
- package/src/bin/sync-runner-company.ts +0 -695
- package/src/bin/sync-runner-events.test.ts +0 -143
- package/src/bin/sync-runner-events.ts +0 -55
- package/src/bin/sync-runner-planning.test.ts +0 -311
- package/src/bin/sync-runner-planning.ts +0 -258
- package/src/bin/sync-runner-rollup.test.ts +0 -37
- package/src/bin/sync-runner-rollup.ts +0 -97
- package/src/bin/sync-runner-telemetry.ts +0 -15
- package/src/bin/sync-runner-watch-loop.ts +0 -1235
- package/src/bin/sync-runner-watch-routes.test.ts +0 -71
- package/src/bin/sync-runner-watch-routes.ts +0 -184
- package/src/bin/sync-runner.test.ts +0 -8767
- package/src/bin/sync-runner.ts +0 -2190
- package/src/cli/accept.ts +0 -124
- package/src/cli/conflict.ts +0 -119
- package/src/cli/doctor.test.ts +0 -581
- package/src/cli/doctor.ts +0 -642
- package/src/cli/index.ts +0 -49
- package/src/cli/invite.test.ts +0 -250
- package/src/cli/invite.ts +0 -214
- package/src/cli/promote.ts +0 -157
- package/src/cli/reindex-knowledge.test.ts +0 -307
- package/src/cli/reindex-knowledge.ts +0 -450
- package/src/cli/reindex.test.ts +0 -957
- package/src/cli/reindex.ts +0 -979
- package/src/cli/rescue-classify-ordering.test.ts +0 -548
- package/src/cli/rescue-clone-diagnostics.test.ts +0 -120
- package/src/cli/rescue-core.ts +0 -3011
- package/src/cli/rescue-drift-reconcile.test.ts +0 -179
- package/src/cli/rescue-drop-dir-symlink.test.ts +0 -224
- package/src/cli/rescue-exec-bit-preserve.test.ts +0 -187
- package/src/cli/rescue-hq-root-guard.test.ts +0 -232
- package/src/cli/rescue-journal-reconcile.test.ts +0 -215
- package/src/cli/rescue-mtime-preserve.test.ts +0 -203
- package/src/cli/rescue-settings-reconcile.test.ts +0 -637
- package/src/cli/rescue-snapshot.test.ts +0 -57
- package/src/cli/rescue-snapshot.ts +0 -51
- package/src/cli/rescue.reindex.test.ts +0 -63
- package/src/cli/rescue.test.ts +0 -131
- package/src/cli/rescue.ts +0 -182
- package/src/cli/share.test.ts +0 -7843
- package/src/cli/share.ts +0 -3663
- package/src/cli/sync-scope.test.ts +0 -652
- package/src/cli/sync.test.ts +0 -5207
- package/src/cli/sync.ts +0 -3470
- package/src/cli/tombstones.ts +0 -106
- package/src/cli/watch-event-push-conflict.test.ts +0 -234
- package/src/client-info.test.ts +0 -214
- package/src/client-info.ts +0 -121
- package/src/cognito-auth.test.ts +0 -712
- package/src/cognito-auth.ts +0 -1422
- package/src/company-resolver.test.ts +0 -618
- package/src/company-resolver.ts +0 -521
- package/src/context.test.ts +0 -583
- package/src/context.ts +0 -378
- package/src/daemon-worker.ts +0 -26
- package/src/daemon.ts +0 -99
- package/src/entity-resolver.test.ts +0 -315
- package/src/entity-resolver.ts +0 -180
- package/src/ignore.test.ts +0 -466
- package/src/ignore.ts +0 -469
- package/src/index.ts +0 -439
- package/src/journal.test.ts +0 -968
- package/src/journal.ts +0 -765
- package/src/lib/cloud-authoritative.test.ts +0 -45
- package/src/lib/cloud-authoritative.ts +0 -59
- package/src/lib/conflict-file.ts +0 -86
- package/src/lib/conflict-index.ts +0 -289
- package/src/lib/conflict.test.ts +0 -348
- package/src/lib/describe-error.test.ts +0 -100
- package/src/lib/describe-error.ts +0 -58
- package/src/lib/exit-codes.ts +0 -24
- package/src/lib/machine-id.test.ts +0 -231
- package/src/lib/machine-id.ts +0 -175
- package/src/lib/net-errors.test.ts +0 -65
- package/src/lib/net-errors.ts +0 -86
- package/src/lib/readlink-safe.test.ts +0 -43
- package/src/lib/readlink-safe.ts +0 -29
- package/src/local-path-codec.test.ts +0 -138
- package/src/local-path-codec.ts +0 -161
- package/src/machine-auth.test.ts +0 -1323
- package/src/manifest-reconcile.test.ts +0 -1123
- package/src/manifest-reconcile.ts +0 -518
- package/src/object-io.test.ts +0 -1221
- package/src/object-io.ts +0 -1306
- package/src/operation-lock.test.ts +0 -484
- package/src/operation-lock.ts +0 -680
- package/src/outcome-telemetry.test.ts +0 -498
- package/src/outcome-telemetry.ts +0 -639
- package/src/personal-vault-exclusions.test.ts +0 -308
- package/src/personal-vault-exclusions.ts +0 -354
- package/src/personal-vault.test.ts +0 -756
- package/src/personal-vault.ts +0 -496
- package/src/prefix-coalesce.test.ts +0 -240
- package/src/prefix-coalesce.ts +0 -273
- package/src/public-surface.test.ts +0 -117
- package/src/qmd-reindex.test.ts +0 -877
- package/src/qmd-reindex.ts +0 -842
- package/src/read-only-state-dir.test.ts +0 -188
- package/src/remote-pull.test.ts +0 -1130
- package/src/remote-pull.ts +0 -618
- package/src/s3.symlink-materialize.test.ts +0 -492
- package/src/s3.test.ts +0 -1789
- package/src/s3.ts +0 -1532
- package/src/schemas/signal-types.test.ts +0 -82
- package/src/schemas/signal-types.ts +0 -38
- package/src/schemas/source-channels.test.ts +0 -82
- package/src/schemas/source-channels.ts +0 -53
- package/src/scope-shrink.test.ts +0 -633
- package/src/scope-shrink.ts +0 -481
- package/src/signals/get.test.ts +0 -310
- package/src/signals/get.ts +0 -75
- package/src/signals/internals.ts +0 -195
- package/src/signals/list.test.ts +0 -420
- package/src/signals/list.ts +0 -79
- package/src/signals/parse.ts +0 -8
- package/src/signals/types.ts +0 -91
- package/src/skill-telemetry.test.ts +0 -1825
- package/src/skill-telemetry.ts +0 -1439
- package/src/sources/get.test.ts +0 -293
- package/src/sources/get.ts +0 -66
- package/src/sources/internals.ts +0 -198
- package/src/sources/list.test.ts +0 -402
- package/src/sources/list.ts +0 -84
- package/src/sources/parse.ts +0 -43
- package/src/sources/types.ts +0 -84
- package/src/sync/event-sync.test.ts +0 -594
- package/src/sync/event-sync.ts +0 -545
- package/src/sync/feature-flags.test.ts +0 -378
- package/src/sync/feature-flags.ts +0 -62
- package/src/sync/index.ts +0 -76
- package/src/sync/lease-client.test.ts +0 -128
- package/src/sync/lease-client.ts +0 -207
- package/src/sync/logger.test.ts +0 -242
- package/src/sync/logger.ts +0 -79
- package/src/sync/metrics.test.ts +0 -462
- package/src/sync/metrics.ts +0 -213
- package/src/sync/pull-scope.ts +0 -265
- package/src/sync/push-event.test.ts +0 -266
- package/src/sync/push-event.ts +0 -224
- package/src/sync/push-receiver.test.ts +0 -566
- package/src/sync/push-receiver.ts +0 -1048
- package/src/sync/push-transport.ts +0 -231
- package/src/sync/realtime-rollout.test.ts +0 -86
- package/src/sync/realtime-rollout.ts +0 -262
- package/src/sync/state-store.test.ts +0 -194
- package/src/sync/state-store.ts +0 -727
- package/src/sync-core.ts +0 -58
- package/src/sync-progress.test.ts +0 -94
- package/src/sync-progress.ts +0 -140
- package/src/telemetry-events.test.ts +0 -88
- package/src/telemetry-events.ts +0 -205
- package/src/telemetry.test.ts +0 -1280
- package/src/telemetry.ts +0 -1109
- package/src/types.ts +0 -314
- package/src/vault-client.test.ts +0 -1380
- package/src/vault-client.ts +0 -1694
- package/src/version.ts +0 -24
- package/src/watch-roots.test.ts +0 -278
- package/src/watch-roots.ts +0 -162
- package/src/watcher-event-gate.test.ts +0 -212
- package/src/watcher.test.ts +0 -1079
- package/src/watcher.ts +0 -1741
- package/test/e2e/sync/cross-tenant-isolation.test.ts +0 -630
- package/test/e2e/sync/skill-telemetry-oversized-transcript.test.ts +0 -124
- package/test/e2e/sync/transient-company-leg.test.ts +0 -384
- package/test/e2e/sync/windows-unreadable-link-leg.test.ts +0 -191
- package/test/e2e/watcher-real-chokidar.test.ts +0 -165
- package/test/e2e/watcher-recursive-backend.test.ts +0 -181
- package/test/e2e/watcher-scoped-coverage.test.ts +0 -381
- package/test/invite-flow.integration.test.ts +0 -244
- package/test/joiner-manifest-reconcile.integration.test.ts +0 -322
- package/test/share-sync.integration.test.ts +0 -213
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -22
package/src/scope-shrink.ts
DELETED
|
@@ -1,481 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scope-shrink detection + classification + clean removal (US-005).
|
|
3
|
-
*
|
|
4
|
-
* Implements the "hybrid scope-change contract" decided in US-000 Task 3
|
|
5
|
-
* (see companies/indigo/projects/hq-sync-browse-vs-sync/references.md):
|
|
6
|
-
*
|
|
7
|
-
* - Compare the current pull's `prefixSet` against the last `PullRecord`'s
|
|
8
|
-
* `prefixSet` for the same company. Files in the journal covered by the
|
|
9
|
-
* previous scope but NOT covered by the new scope are **orphans**.
|
|
10
|
-
* - Classify each orphan **clean** (safe to silently delete) or **dirty**
|
|
11
|
-
* (locally modified — sacred, never silently delete).
|
|
12
|
-
* - The remote-pull caller drives:
|
|
13
|
-
* * default mode: abort the leg if any dirty orphan exists;
|
|
14
|
-
* * `--force-scope-shrink`: continue, leave dirty files on disk,
|
|
15
|
-
* tombstone their journal entries.
|
|
16
|
-
*
|
|
17
|
-
* The pure-detection layer here intentionally does NOT touch disk for the
|
|
18
|
-
* tombstone write — that lives in `journal.ts`. It DOES touch disk for the
|
|
19
|
-
* orphan classification (hash + stat) because cleanliness is a function of
|
|
20
|
-
* the file's current on-disk state vs the journal.
|
|
21
|
-
*
|
|
22
|
-
* PUSH-ONLY PREFIXES ARE NEVER PRUNED HERE (US-006). Session transcripts under
|
|
23
|
-
* `sessions/{personUid}/...` are push-only: pushed into the vault, then
|
|
24
|
-
* subtracted from every pull scope (incl. `all`/owner) via
|
|
25
|
-
* `resolvePullScope().excludePrefixes`. That exclude set is applied ONLY to the
|
|
26
|
-
* download filter (`computePullPlan`) and is DELIBERATELY NOT passed into this
|
|
27
|
-
* module's `currentPrefixSet`. So a session file that was authored locally rides
|
|
28
|
-
* the `direction:"up"` skip below, and one materialized on demand (`hq files
|
|
29
|
-
* get`) rides the pin union already folded into the caller's inclusion
|
|
30
|
-
* `prefixSet` — either way it is never pulled AND never orphaned. Feeding the
|
|
31
|
-
* exclude set into `currentPrefixSet` would prune exactly those files; callers
|
|
32
|
-
* must not.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import * as fs from "fs";
|
|
36
|
-
import * as path from "path";
|
|
37
|
-
import type {
|
|
38
|
-
JournalEntry,
|
|
39
|
-
PullRecord,
|
|
40
|
-
SyncJournal,
|
|
41
|
-
} from "./types.js";
|
|
42
|
-
import { hashFile, tombstoneEntry } from "./journal.js";
|
|
43
|
-
import { localPathForVaultKey } from "./local-path-codec.js";
|
|
44
|
-
import {
|
|
45
|
-
isCoveredByAny,
|
|
46
|
-
type ScopePrefixInput,
|
|
47
|
-
} from "./prefix-coalesce.js";
|
|
48
|
-
|
|
49
|
-
export interface OrphanClassification {
|
|
50
|
-
/** Relative path (journal key). */
|
|
51
|
-
path: string;
|
|
52
|
-
/** Journal entry as of last sync. */
|
|
53
|
-
entry: JournalEntry;
|
|
54
|
-
/** True iff the local file is provably unchanged since last sync. */
|
|
55
|
-
clean: boolean;
|
|
56
|
-
/** Why we called it dirty — surfaced in the abort error for operators. */
|
|
57
|
-
dirtyReason?:
|
|
58
|
-
| "modified-after-sync"
|
|
59
|
-
| "hash-mismatch"
|
|
60
|
-
| "stat-error";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface ScopeShrinkPlan {
|
|
64
|
-
/** Set of files covered by `lastPrefixSet` but not by `currentPrefixSet`. */
|
|
65
|
-
orphans: OrphanClassification[];
|
|
66
|
-
/** Subset of `orphans` with `clean === true`. */
|
|
67
|
-
clean: OrphanClassification[];
|
|
68
|
-
/** Subset of `orphans` with `clean === false`. */
|
|
69
|
-
dirty: OrphanClassification[];
|
|
70
|
-
/** True iff at least one orphan was found. */
|
|
71
|
-
scopeChangeDetected: boolean;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface BuildScopeShrinkPlanInput {
|
|
75
|
-
journal: SyncJournal;
|
|
76
|
-
hqRoot: string;
|
|
77
|
-
/** Coalesced prefixes used by the LAST pull for this company. */
|
|
78
|
-
lastPrefixSet: readonly ScopePrefixInput[];
|
|
79
|
-
/** Coalesced prefixes the CURRENT pull will use. */
|
|
80
|
-
currentPrefixSet: readonly ScopePrefixInput[];
|
|
81
|
-
/**
|
|
82
|
-
* The caller's own Cognito `sub`. When set, a file the caller authored
|
|
83
|
-
* (`entry.createdBySub === callerSub`) is NEVER orphaned by a scope shrink —
|
|
84
|
-
* regardless of mode. This is the core of the authorship contract: sync mode
|
|
85
|
-
* governs whether you mirror *other people's* files; it must never disown
|
|
86
|
-
* your own work. Owners hold their whole vault by role-bypass, so without
|
|
87
|
-
* this guard a `shared`/`custom` scope would treat their own un-granted
|
|
88
|
-
* content as "someone else's file I happen to see" and prune it.
|
|
89
|
-
*/
|
|
90
|
-
callerSub?: string;
|
|
91
|
-
/**
|
|
92
|
-
* When `true`, an orphan whose authorship is unknown (`createdBySub`
|
|
93
|
-
* undefined — a legacy entry predating author stamping, or an object
|
|
94
|
-
* uploaded without author metadata) is also retained rather than pruned.
|
|
95
|
-
* The automatic background pull sets this so a routine sync never makes a
|
|
96
|
-
* destructive guess about pre-stamp content; the explicit `hq sync narrow`
|
|
97
|
-
* ritual (which carries its own confirmation + dirty gate) leaves it off so
|
|
98
|
-
* a deliberately-confirmed narrow can still reclaim legacy files.
|
|
99
|
-
*/
|
|
100
|
-
protectUnknownAuthors?: boolean;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Build a scope-shrink plan: find orphans, classify each clean/dirty.
|
|
105
|
-
* Pure given the journal + filesystem state — no network, no journal
|
|
106
|
-
* mutation.
|
|
107
|
-
*
|
|
108
|
-
* **Tombstone-aware:** journal entries that already carry a `removedAt`
|
|
109
|
-
* marker are skipped — they represent a prior scope-shrink prune and must
|
|
110
|
-
* not be re-flagged as orphans on each subsequent pull (that's the whole
|
|
111
|
-
* point of the tombstone retention window).
|
|
112
|
-
*
|
|
113
|
-
* **Direction-aware:** only `direction: "down"` entries (and pre-ETag
|
|
114
|
-
* legacy entries without an explicit direction marker) participate in
|
|
115
|
-
* shrink detection. Push-only files (`direction: "up"`) represent local
|
|
116
|
-
* authorship — they aren't in scope-as-pulled, so a scope change doesn't
|
|
117
|
-
* orphan them.
|
|
118
|
-
*/
|
|
119
|
-
export function buildScopeShrinkPlan(
|
|
120
|
-
input: BuildScopeShrinkPlanInput,
|
|
121
|
-
): ScopeShrinkPlan {
|
|
122
|
-
const { journal, hqRoot, lastPrefixSet, currentPrefixSet } = input;
|
|
123
|
-
const { callerSub, protectUnknownAuthors } = input;
|
|
124
|
-
const orphans: OrphanClassification[] = [];
|
|
125
|
-
|
|
126
|
-
for (const [relPath, entry] of Object.entries(journal.files)) {
|
|
127
|
-
if (entry.removedAt) continue; // tombstone — already pruned
|
|
128
|
-
if (entry.direction !== "down") continue;
|
|
129
|
-
if (!isCoveredByAny(relPath, lastPrefixSet)) continue;
|
|
130
|
-
if (isCoveredByAny(relPath, currentPrefixSet)) continue;
|
|
131
|
-
// Authorship guard: sync mode decides whether you mirror OTHER people's
|
|
132
|
-
// files — it must never disown your own. A file the caller authored is
|
|
133
|
-
// sacred and never orphaned, even out of the current prefix scope. When
|
|
134
|
-
// `protectUnknownAuthors` is set (the automatic pull path), a legacy
|
|
135
|
-
// entry with no recorded author is also retained — a routine background
|
|
136
|
-
// sync should never make a destructive guess about pre-stamp content.
|
|
137
|
-
if (callerSub && entry.createdBySub === callerSub) continue;
|
|
138
|
-
if (protectUnknownAuthors && entry.createdBySub === undefined) continue;
|
|
139
|
-
orphans.push(classifyOrphan(relPath, entry, hqRoot));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const clean = orphans.filter((o) => o.clean);
|
|
143
|
-
const dirty = orphans.filter((o) => !o.clean);
|
|
144
|
-
return {
|
|
145
|
-
orphans,
|
|
146
|
-
clean,
|
|
147
|
-
dirty,
|
|
148
|
-
scopeChangeDetected: orphans.length > 0,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Classify a single orphan:
|
|
154
|
-
*
|
|
155
|
-
* - **Clean** when:
|
|
156
|
-
* * the local file is missing (already removed by the user — harmless), OR
|
|
157
|
-
* * `sha256(localFile) === entry.hash` AND `stat.mtime ≤ entry.syncedAt`.
|
|
158
|
-
* - **Dirty** otherwise.
|
|
159
|
-
*
|
|
160
|
-
* Symlinks: we don't re-hash with the symlink-target convention here —
|
|
161
|
-
* the safer default is to treat any symlink whose lstat exists but whose
|
|
162
|
-
* target hash doesn't match `entry.hash` (via `hashFile` reading the
|
|
163
|
-
* target) as dirty. In practice symlinks materialize through the same
|
|
164
|
-
* code path as files; a stale-target symlink is correctly flagged dirty
|
|
165
|
-
* and the operator-facing message points at the path either way.
|
|
166
|
-
*/
|
|
167
|
-
function classifyOrphan(
|
|
168
|
-
relPath: string,
|
|
169
|
-
entry: JournalEntry,
|
|
170
|
-
hqRoot: string,
|
|
171
|
-
): OrphanClassification {
|
|
172
|
-
const absPath = localPathForVaultKey(hqRoot, relPath);
|
|
173
|
-
let stat: fs.Stats;
|
|
174
|
-
try {
|
|
175
|
-
stat = fs.lstatSync(absPath);
|
|
176
|
-
} catch (err) {
|
|
177
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
178
|
-
if (code === "ENOENT") {
|
|
179
|
-
return { path: relPath, entry, clean: true };
|
|
180
|
-
}
|
|
181
|
-
return {
|
|
182
|
-
path: relPath,
|
|
183
|
-
entry,
|
|
184
|
-
clean: false,
|
|
185
|
-
dirtyReason: "stat-error",
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// mtime guard: a local edit moves mtime past syncedAt. We use ≤ because
|
|
190
|
-
// a download stamps syncedAt at the close of the write; mtime is set by
|
|
191
|
-
// the OS before that, so an unmodified pulled file has mtime ≤ syncedAt.
|
|
192
|
-
const mtimeMs = stat.mtimeMs;
|
|
193
|
-
const syncedAtMs = Date.parse(entry.syncedAt);
|
|
194
|
-
if (!Number.isNaN(syncedAtMs) && mtimeMs > syncedAtMs + 1000) {
|
|
195
|
-
// 1s grace for filesystem clock jitter.
|
|
196
|
-
return {
|
|
197
|
-
path: relPath,
|
|
198
|
-
entry,
|
|
199
|
-
clean: false,
|
|
200
|
-
dirtyReason: "modified-after-sync",
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Hash check — final word. If the content matches the journaled hash,
|
|
205
|
-
// the file is provably what the last pull left there.
|
|
206
|
-
let actualHash: string;
|
|
207
|
-
try {
|
|
208
|
-
actualHash = hashFile(absPath);
|
|
209
|
-
} catch {
|
|
210
|
-
return {
|
|
211
|
-
path: relPath,
|
|
212
|
-
entry,
|
|
213
|
-
clean: false,
|
|
214
|
-
dirtyReason: "stat-error",
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
if (actualHash !== entry.hash) {
|
|
218
|
-
return {
|
|
219
|
-
path: relPath,
|
|
220
|
-
entry,
|
|
221
|
-
clean: false,
|
|
222
|
-
dirtyReason: "hash-mismatch",
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
return { path: relPath, entry, clean: true };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Where a scope-shrink error is going to be rendered, so the structured error
|
|
230
|
-
* can carry advice that is ACTUALLY FOLLOWABLE from that entry point.
|
|
231
|
-
*
|
|
232
|
-
* - `"cli"` — a human at a terminal running `hq sync pull|now`. They can
|
|
233
|
-
* re-run with `--force-scope-shrink` or run the guided
|
|
234
|
-
* `hq sync narrow --apply` ritual.
|
|
235
|
-
* - `"runner"` — the menubar's `hq-sync-runner`. It accepts NO such flag
|
|
236
|
-
* (DEV-1768 fix #2: the old "pass --force-scope-shrink" advice
|
|
237
|
-
* was impossible to follow from here), so the only followable
|
|
238
|
-
* action is to open a terminal and run `hq sync narrow --apply`.
|
|
239
|
-
* In practice the runner pulls with `scopeShrinkPolicy:
|
|
240
|
-
* "auto-recover"` and never throws this — but the context keeps
|
|
241
|
-
* the message honest if it ever surfaces.
|
|
242
|
-
* - `"engine"` — unknown/library caller; generic advice.
|
|
243
|
-
*/
|
|
244
|
-
export type ScopeShrinkAdviceContext = "cli" | "runner" | "engine";
|
|
245
|
-
|
|
246
|
-
/** Followable next-step advice for a blocked scope shrink, per entry point. */
|
|
247
|
-
function scopeShrinkAdvice(ctx: ScopeShrinkAdviceContext): string {
|
|
248
|
-
switch (ctx) {
|
|
249
|
-
case "cli":
|
|
250
|
-
return (
|
|
251
|
-
"Re-run with `--force-scope-shrink` to disown them now (dirty files " +
|
|
252
|
-
"are KEPT on disk, only un-tracked from sync), or run " +
|
|
253
|
-
"`hq sync narrow --apply` to migrate with a confirmation prompt."
|
|
254
|
-
);
|
|
255
|
-
case "runner":
|
|
256
|
-
return (
|
|
257
|
-
"The menubar sync cannot take this flag — open a terminal and run " +
|
|
258
|
-
"`hq sync narrow --apply` to migrate this membership (you confirm the " +
|
|
259
|
-
"file list), or `hq sync now --force-scope-shrink` once to proceed " +
|
|
260
|
-
"(dirty files are KEPT on disk, only un-tracked)."
|
|
261
|
-
);
|
|
262
|
-
default:
|
|
263
|
-
return (
|
|
264
|
-
"Run `hq sync narrow --apply` to migrate with confirmation, or pass " +
|
|
265
|
-
"`forceScopeShrink` (dirty files are kept on disk, only un-tracked)."
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Structured error thrown when the engine refuses to proceed because a scope
|
|
272
|
-
* shrink would orphan dirty files. The CLI catches this and renders the
|
|
273
|
-
* operator-facing message; the engine never prints directly.
|
|
274
|
-
*
|
|
275
|
-
* `adviceContext` makes the message FOLLOWABLE from each entry point — the old
|
|
276
|
-
* fixed "pass --force-scope-shrink" line was impossible to act on from the
|
|
277
|
-
* menubar runner, which rejects that flag (DEV-1768 fix #2).
|
|
278
|
-
*/
|
|
279
|
-
export class ScopeShrinkBlockedError extends Error {
|
|
280
|
-
readonly code = "SCOPE_SHRINK_BLOCKED";
|
|
281
|
-
constructor(
|
|
282
|
-
public readonly companyUid: string,
|
|
283
|
-
public readonly fromMode: PullRecord["syncMode"] | "unknown",
|
|
284
|
-
public readonly toMode: PullRecord["syncMode"],
|
|
285
|
-
public readonly dirty: OrphanClassification[],
|
|
286
|
-
public readonly clean: OrphanClassification[],
|
|
287
|
-
public readonly adviceContext: ScopeShrinkAdviceContext = "engine",
|
|
288
|
-
) {
|
|
289
|
-
super(
|
|
290
|
-
`Sync scope shrank for ${companyUid} (${fromMode} → ${toMode}); ` +
|
|
291
|
-
`${dirty.length} locally-modified file(s) outside the new scope ` +
|
|
292
|
-
`would be un-tracked from sync. ${scopeShrinkAdvice(adviceContext)}`,
|
|
293
|
-
);
|
|
294
|
-
this.name = "ScopeShrinkBlockedError";
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Structured error thrown when an AUTOMATIC scope shrink would prune more
|
|
300
|
-
* CLEAN local files than the configured safety cap in a single pull. This is
|
|
301
|
-
* the bulk-delete guard: a routine background sync should never silently
|
|
302
|
-
* delete a large local tree — whether from a deliberate-but-abrupt first
|
|
303
|
-
* narrow, a server bug returning an unexpectedly small grant set, or a
|
|
304
|
-
* mis-resolved scope. The operator runs the explicit `hq sync narrow` ritual
|
|
305
|
-
* (which has its own confirmation + dirty gate) or passes `--force-scope-shrink`
|
|
306
|
-
* to proceed. The engine never deletes anything when it throws this.
|
|
307
|
-
*/
|
|
308
|
-
export class ScopeShrinkLargePruneError extends Error {
|
|
309
|
-
readonly code = "SCOPE_SHRINK_LARGE_PRUNE";
|
|
310
|
-
constructor(
|
|
311
|
-
public readonly companyUid: string,
|
|
312
|
-
public readonly toMode: PullRecord["syncMode"],
|
|
313
|
-
public readonly cleanCount: number,
|
|
314
|
-
public readonly cap: number,
|
|
315
|
-
public readonly adviceContext: ScopeShrinkAdviceContext = "engine",
|
|
316
|
-
) {
|
|
317
|
-
super(
|
|
318
|
-
`Refusing to auto-move ${cleanCount} local file(s) for ${companyUid} ` +
|
|
319
|
-
`(${toMode} scope) in one sync — exceeds the safety cap of ${cap}. ` +
|
|
320
|
-
`Raise HQ_SYNC_MAX_AUTO_PRUNE, or ${scopeShrinkAdvice(adviceContext)}`,
|
|
321
|
-
);
|
|
322
|
-
this.name = "ScopeShrinkLargePruneError";
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Disposition for CLEAN orphans (files provably unchanged since the last sync)
|
|
328
|
-
* that fall outside the new scope:
|
|
329
|
-
*
|
|
330
|
-
* - `"delete"` — `unlink` the local file. The legacy behavior; reserved
|
|
331
|
-
* for the explicit `hq sync narrow --apply` ritual, which
|
|
332
|
-
* already confirms the file list with the operator.
|
|
333
|
-
* - `"quarantine"` — MOVE the file into `quarantineRoot` instead of deleting
|
|
334
|
-
* it, so it stays recoverable. The conservative default
|
|
335
|
-
* for the automatic pull path: a background sync must
|
|
336
|
-
* never silently PURGE local files (DEV-1768 fix #3).
|
|
337
|
-
*/
|
|
338
|
-
export type CleanOrphanDisposition = "delete" | "quarantine";
|
|
339
|
-
|
|
340
|
-
export interface ApplyScopeShrinkInput {
|
|
341
|
-
journal: SyncJournal;
|
|
342
|
-
plan: ScopeShrinkPlan;
|
|
343
|
-
hqRoot: string;
|
|
344
|
-
/**
|
|
345
|
-
* When `true`, dirty files are LEFT ON DISK and their journal entries are
|
|
346
|
-
* tombstoned anyway. When `false` (default), the caller should have
|
|
347
|
-
* already aborted on dirty orphans — this function still tombstones any
|
|
348
|
-
* dirty entries handed to it, on the assumption the caller knows what
|
|
349
|
-
* it's doing.
|
|
350
|
-
*/
|
|
351
|
-
forceScopeShrink: boolean;
|
|
352
|
-
reason?: "scope_shrink" | "narrow_apply" | "manual";
|
|
353
|
-
/**
|
|
354
|
-
* How to dispose of CLEAN orphans. Defaults to `"delete"` so existing
|
|
355
|
-
* callers (and the confirmed `narrow --apply` ritual) keep their behavior;
|
|
356
|
-
* the automatic pull path passes `"quarantine"`.
|
|
357
|
-
*/
|
|
358
|
-
cleanDisposition?: CleanOrphanDisposition;
|
|
359
|
-
/**
|
|
360
|
-
* Absolute directory clean orphans are relocated into when
|
|
361
|
-
* `cleanDisposition === "quarantine"`. Each orphan moves to
|
|
362
|
-
* `<quarantineRoot>/<orphan.path>` (parent dirs created). REQUIRED when
|
|
363
|
-
* quarantining; if absent, the function falls back to `"delete"` so it can
|
|
364
|
-
* never get stuck unable to make progress.
|
|
365
|
-
*/
|
|
366
|
-
quarantineRoot?: string;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export interface ApplyScopeShrinkResult {
|
|
370
|
-
/** Clean orphans `unlink`ed from disk (only when disposition is `delete`). */
|
|
371
|
-
cleanRemoved: number;
|
|
372
|
-
/** Clean orphans MOVED to quarantine (only when disposition is `quarantine`). */
|
|
373
|
-
cleanQuarantined: number;
|
|
374
|
-
/** Dirty orphans tombstoned in the journal (file LEFT on disk). */
|
|
375
|
-
dirtyTombstoned: number;
|
|
376
|
-
/** Named paths deleted — for explicit, non-silent operator reporting. */
|
|
377
|
-
removedPaths: string[];
|
|
378
|
-
/** Named paths moved to quarantine — for explicit reporting. */
|
|
379
|
-
quarantinedPaths: string[];
|
|
380
|
-
/** Named dirty paths un-tracked but KEPT on disk — for explicit reporting. */
|
|
381
|
-
dirtyKeptPaths: string[];
|
|
382
|
-
/** Absolute quarantine directory used (when anything was quarantined). */
|
|
383
|
-
quarantineRoot?: string;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Move a clean orphan from the working tree into the quarantine tree,
|
|
388
|
-
* preserving its relative path. Same-device `rename` first (cheap, atomic);
|
|
389
|
-
* cross-device falls back to copy+unlink. A missing source is a no-op (the
|
|
390
|
-
* user already removed it — harmless). Returns true iff the file was relocated
|
|
391
|
-
* (or was already absent), false only on an unexpected error the caller should
|
|
392
|
-
* surface.
|
|
393
|
-
*/
|
|
394
|
-
function quarantineOrphan(
|
|
395
|
-
srcAbs: string,
|
|
396
|
-
destAbs: string,
|
|
397
|
-
): void {
|
|
398
|
-
fs.mkdirSync(path.dirname(destAbs), { recursive: true });
|
|
399
|
-
try {
|
|
400
|
-
fs.renameSync(srcAbs, destAbs);
|
|
401
|
-
} catch (err) {
|
|
402
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
403
|
-
if (code === "ENOENT") return; // source already gone — nothing to move
|
|
404
|
-
if (code === "EXDEV") {
|
|
405
|
-
// Cross-device move: copy then unlink. cpSync handles files + symlinks.
|
|
406
|
-
fs.cpSync(srcAbs, destAbs, { recursive: true, verbatimSymlinks: true });
|
|
407
|
-
fs.rmSync(srcAbs, { recursive: true, force: true });
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
throw err;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Apply a scope-shrink plan: dispose of clean orphans (delete OR quarantine)
|
|
416
|
-
* + tombstone their journal entries. With `forceScopeShrink: true`, dirty
|
|
417
|
-
* orphans are PRESERVED on disk and only their journal entries are tombstoned
|
|
418
|
-
* (so they stop being re-flagged on every pull — the idempotent recovery seam).
|
|
419
|
-
*
|
|
420
|
-
* Returns counts AND named paths so the caller can report exactly what moved /
|
|
421
|
-
* was un-tracked — never a silent purge (DEV-1768 fix #3).
|
|
422
|
-
*/
|
|
423
|
-
export function applyScopeShrink(
|
|
424
|
-
input: ApplyScopeShrinkInput,
|
|
425
|
-
): ApplyScopeShrinkResult {
|
|
426
|
-
const { journal, plan, hqRoot, forceScopeShrink } = input;
|
|
427
|
-
const reason = input.reason ?? "scope_shrink";
|
|
428
|
-
// Quarantine only when explicitly asked AND a destination is provided;
|
|
429
|
-
// otherwise fall back to delete so we always make progress.
|
|
430
|
-
const quarantining =
|
|
431
|
-
input.cleanDisposition === "quarantine" && !!input.quarantineRoot;
|
|
432
|
-
let cleanRemoved = 0;
|
|
433
|
-
let cleanQuarantined = 0;
|
|
434
|
-
let dirtyTombstoned = 0;
|
|
435
|
-
const removedPaths: string[] = [];
|
|
436
|
-
const quarantinedPaths: string[] = [];
|
|
437
|
-
const dirtyKeptPaths: string[] = [];
|
|
438
|
-
|
|
439
|
-
for (const orphan of plan.clean) {
|
|
440
|
-
const absPath = localPathForVaultKey(hqRoot, orphan.path);
|
|
441
|
-
if (quarantining) {
|
|
442
|
-
const destAbs = localPathForVaultKey(input.quarantineRoot!, orphan.path);
|
|
443
|
-
quarantineOrphan(absPath, destAbs);
|
|
444
|
-
tombstoneEntry(journal, orphan.path, reason);
|
|
445
|
-
cleanQuarantined++;
|
|
446
|
-
quarantinedPaths.push(orphan.path);
|
|
447
|
-
} else {
|
|
448
|
-
try {
|
|
449
|
-
fs.unlinkSync(absPath);
|
|
450
|
-
} catch (err) {
|
|
451
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
452
|
-
if (code !== "ENOENT") throw err; // missing-on-disk is fine; anything else escalates
|
|
453
|
-
}
|
|
454
|
-
tombstoneEntry(journal, orphan.path, reason);
|
|
455
|
-
cleanRemoved++;
|
|
456
|
-
removedPaths.push(orphan.path);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (forceScopeShrink) {
|
|
461
|
-
for (const orphan of plan.dirty) {
|
|
462
|
-
// Do NOT delete the file — keep dirty content on disk, prune only the
|
|
463
|
-
// journal entry so it stops being re-flagged as an orphan on each pull.
|
|
464
|
-
tombstoneEntry(journal, orphan.path, reason);
|
|
465
|
-
dirtyTombstoned++;
|
|
466
|
-
dirtyKeptPaths.push(orphan.path);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
return {
|
|
471
|
-
cleanRemoved,
|
|
472
|
-
cleanQuarantined,
|
|
473
|
-
dirtyTombstoned,
|
|
474
|
-
removedPaths,
|
|
475
|
-
quarantinedPaths,
|
|
476
|
-
dirtyKeptPaths,
|
|
477
|
-
...(quarantining && quarantinedPaths.length > 0
|
|
478
|
-
? { quarantineRoot: input.quarantineRoot }
|
|
479
|
-
: {}),
|
|
480
|
-
};
|
|
481
|
-
}
|