@indigoai-us/hq-cloud 6.14.22 → 6.14.23
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-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +9 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-events.test.js +22 -0
- package/dist/bin/sync-runner-events.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +27 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +14 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +51 -1
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.test.js +1 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +25 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +83 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +171 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/lib/conflict-index.d.ts +7 -0
- package/dist/lib/conflict-index.d.ts.map +1 -1
- package/dist/lib/conflict-index.js +3 -0
- package/dist/lib/conflict-index.js.map +1 -1
- package/dist/lib/conflict.test.js +31 -0
- package/dist/lib/conflict.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-company.ts +8 -0
- package/src/bin/sync-runner-events.test.ts +31 -0
- package/src/bin/sync-runner.test.ts +62 -1
- package/src/bin/sync-runner.ts +40 -1
- package/src/cli/share.test.ts +1 -0
- package/src/cli/sync.test.ts +192 -0
- package/src/cli/sync.ts +126 -1
- package/src/lib/conflict-index.ts +11 -0
- package/src/lib/conflict.test.ts +33 -0
package/src/cli/sync.ts
CHANGED
|
@@ -307,6 +307,32 @@ export type SyncProgressEvent =
|
|
|
307
307
|
count: number;
|
|
308
308
|
samplePaths: string[];
|
|
309
309
|
}
|
|
310
|
+
| {
|
|
311
|
+
/**
|
|
312
|
+
* Emitted at most ONCE per PULL leg when the leg ran under a
|
|
313
|
+
* membership-scoped `syncMode` (`"shared"` or `"custom"` — never `"all"`)
|
|
314
|
+
* AND one or more remote keys were skipped as out-of-scope
|
|
315
|
+
* (`filesOutOfScope > 0`). This turns the silent shared-vs-all gap into a
|
|
316
|
+
* VISIBLE, actionable surface. The reporter of feedback_d2082110 lost
|
|
317
|
+
* files across devices precisely because four memberships defaulted to
|
|
318
|
+
* `shared` rather than `all`, so not all vault content materialized on the
|
|
319
|
+
* second device — and nothing told them; they had to notice the gap and
|
|
320
|
+
* flip each membership to `all` by hand. This event names the gap and the
|
|
321
|
+
* lever (raise the membership's access level to `all`) so FULL
|
|
322
|
+
* materialization is a deliberate, visible choice rather than a silent
|
|
323
|
+
* omission.
|
|
324
|
+
*
|
|
325
|
+
* `count` is the number of remote keys skipped as out-of-scope on this
|
|
326
|
+
* leg; `samplePaths` carries up to 10 company-relative keys for display;
|
|
327
|
+
* `syncMode` is the active scoped mode. Distinct from the push-side
|
|
328
|
+
* `scope-excluded` (which reports what a grantee could not PUSH). Not
|
|
329
|
+
* emitted in `all` mode or when `count === 0` — no gap, no noise.
|
|
330
|
+
*/
|
|
331
|
+
type: "scope-materialization-gap";
|
|
332
|
+
count: number;
|
|
333
|
+
samplePaths: string[];
|
|
334
|
+
syncMode: SyncMode;
|
|
335
|
+
}
|
|
310
336
|
| {
|
|
311
337
|
/**
|
|
312
338
|
* Emitted at most ONCE per `share()` push leg when the base ignore
|
|
@@ -856,6 +882,8 @@ async function syncWithOperationLockHeld(
|
|
|
856
882
|
await verifyPlannedJournalTombstones(run, plan);
|
|
857
883
|
executeJournalTombstoneDeletes(run, plan, counters);
|
|
858
884
|
|
|
885
|
+
emitScopeMaterializationGap(run, plan, counters);
|
|
886
|
+
|
|
859
887
|
return finalizePullRun(run, plan, scopeRun, counters);
|
|
860
888
|
}
|
|
861
889
|
|
|
@@ -1170,7 +1198,8 @@ async function executeConflictExecutor(
|
|
|
1170
1198
|
item.action === "skip-personal-mode" ||
|
|
1171
1199
|
item.action === "skip-unchanged" ||
|
|
1172
1200
|
item.action === "skip-local-only" ||
|
|
1173
|
-
item.action === "skip-stale-overlay-marker"
|
|
1201
|
+
item.action === "skip-stale-overlay-marker" ||
|
|
1202
|
+
item.action === "skip-overlay-marker-with-children"
|
|
1174
1203
|
) {
|
|
1175
1204
|
counters.filesSkipped++;
|
|
1176
1205
|
continue;
|
|
@@ -1844,6 +1873,37 @@ function finalizePullRun(
|
|
|
1844
1873
|
};
|
|
1845
1874
|
}
|
|
1846
1875
|
|
|
1876
|
+
/**
|
|
1877
|
+
* Surface the shared-vs-all materialization gap (feedback_d2082110). When a
|
|
1878
|
+
* pull leg ran under a membership-scoped `syncMode` and skipped one or more
|
|
1879
|
+
* remote keys as out-of-scope, emit a single summary event so the operator
|
|
1880
|
+
* SEES that not all vault content materialized on this device and knows the
|
|
1881
|
+
* lever — raise the membership's access level to `all`. Silent in `all` mode
|
|
1882
|
+
* (nothing is scoped away) and when nothing fell out of scope: no gap, no
|
|
1883
|
+
* noise. Deliberately AFTER the transfer executors, so `filesOutOfScope` is
|
|
1884
|
+
* final; sample keys are read off the plan's `skip-out-of-scope` items (pure,
|
|
1885
|
+
* no I/O). This is the visible complement to the previously-silent
|
|
1886
|
+
* `SyncResult.filesOutOfScope` count.
|
|
1887
|
+
*/
|
|
1888
|
+
function emitScopeMaterializationGap(
|
|
1889
|
+
run: PullRunContext,
|
|
1890
|
+
plan: PullPlan,
|
|
1891
|
+
counters: PullCounters,
|
|
1892
|
+
): void {
|
|
1893
|
+
if (run.syncMode === "all") return;
|
|
1894
|
+
if (counters.filesOutOfScope <= 0) return;
|
|
1895
|
+
const samplePaths = plan.items
|
|
1896
|
+
.filter((item) => item.action === "skip-out-of-scope")
|
|
1897
|
+
.slice(0, 10)
|
|
1898
|
+
.map((item) => item.remoteFile.key);
|
|
1899
|
+
run.emit({
|
|
1900
|
+
type: "scope-materialization-gap",
|
|
1901
|
+
count: counters.filesOutOfScope,
|
|
1902
|
+
samplePaths,
|
|
1903
|
+
syncMode: run.syncMode,
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1847
1907
|
/**
|
|
1848
1908
|
* Decide whether a remote object present in the LIST is a GENUINE RE-CREATE
|
|
1849
1909
|
* written AFTER a FILE_TOMBSTONE — in which case the tombstone is stale and the
|
|
@@ -2034,6 +2094,15 @@ type PullPlanItem =
|
|
|
2034
2094
|
// no-action-needed case is never surfaced with the alarming "rm -rf the
|
|
2035
2095
|
// local directory" reconciliation advice.
|
|
2036
2096
|
| { action: "skip-stale-overlay-marker"; remoteFile: RemoteFile; localPath: string }
|
|
2097
|
+
// A directory-overlay marker (a single vault object at a key that ALSO has
|
|
2098
|
+
// child objects under it in the same LIST — a symlink record for a directory
|
|
2099
|
+
// mirrored from another tree) colliding with a local REAL directory whose
|
|
2100
|
+
// contents are already materialized. The marker can never be renamed over the
|
|
2101
|
+
// directory; its children carry the real content and materialize via their own
|
|
2102
|
+
// plan items. A benign, no-action skip — distinct from skip-local-only so the
|
|
2103
|
+
// case is never surfaced with the alarming (and, for a materialized directory,
|
|
2104
|
+
// data-destroying) "rm -rf the local directory to pull" advice. feedback_d2082110.
|
|
2105
|
+
| { action: "skip-overlay-marker-with-children"; remoteFile: RemoteFile; localPath: string }
|
|
2037
2106
|
// Remote keys refused by ephemeral-mirror policy. The push walker has
|
|
2038
2107
|
// refused to upload these since 5.33.0; the pull walker now refuses to
|
|
2039
2108
|
// download them so legacy litter in cloud staging drains naturally.
|
|
@@ -2176,6 +2245,28 @@ function computePullPlan(
|
|
|
2176
2245
|
localPath: string;
|
|
2177
2246
|
}> = [];
|
|
2178
2247
|
|
|
2248
|
+
// Remote keys that ALSO appear as an ancestor of another remote key — i.e.
|
|
2249
|
+
// keys that carry child objects under `${key}/…` in this same LIST. The
|
|
2250
|
+
// vault stores a directory OVERLAY (a symlink into another tree, e.g. a
|
|
2251
|
+
// company skill mirrored from a shared repo) as a symlink RECORD at the
|
|
2252
|
+
// directory's own key AND stores the directory's contents as separate child
|
|
2253
|
+
// objects beneath it. A REAL directory, by contrast, only ever syncs as
|
|
2254
|
+
// those child objects — never as a single object AT its own key. So a single
|
|
2255
|
+
// remote object sitting at a key that also has children is unambiguously a
|
|
2256
|
+
// directory-overlay marker, not a regular file. Precomputed once (pure path
|
|
2257
|
+
// derivation, no I/O — the planner is synchronous) so the dir-vs-object
|
|
2258
|
+
// collision branch can tell an inert overlay marker apart from a genuine
|
|
2259
|
+
// file-vs-directory structural collision. feedback_d2082110.
|
|
2260
|
+
const remoteKeysWithChildren = new Set<string>();
|
|
2261
|
+
for (const rf of remoteFiles) {
|
|
2262
|
+
const key = toPosixKey(rf.key);
|
|
2263
|
+
let slash = key.indexOf("/");
|
|
2264
|
+
while (slash !== -1) {
|
|
2265
|
+
remoteKeysWithChildren.add(key.slice(0, slash));
|
|
2266
|
+
slash = key.indexOf("/", slash + 1);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2179
2270
|
for (const remoteFile of remoteFiles) {
|
|
2180
2271
|
const invalidKey = classifyVaultKey(
|
|
2181
2272
|
remoteFile.key,
|
|
@@ -2437,6 +2528,30 @@ function computePullPlan(
|
|
|
2437
2528
|
});
|
|
2438
2529
|
continue;
|
|
2439
2530
|
}
|
|
2531
|
+
// Directory-overlay marker vs. a locally-materialized directory:
|
|
2532
|
+
// the remote object at this key carries CHILD objects under it in the
|
|
2533
|
+
// same LIST (`remoteKeysWithChildren`), so it is a symlink/overlay
|
|
2534
|
+
// record for a directory — e.g. the DPP vault's `skills/cody-copywriter`
|
|
2535
|
+
// overlay whose 13 child objects are already materialized locally. The
|
|
2536
|
+
// marker can never be renamed over the real directory, and its children
|
|
2537
|
+
// carry the actual content and materialize via their own plan items, so
|
|
2538
|
+
// the marker is a benign, no-action skip. The generic "rm -rf the local
|
|
2539
|
+
// directory to pull" advice below would tell the operator to DELETE the
|
|
2540
|
+
// materialized (and possibly locally-authored) contents to install an
|
|
2541
|
+
// inert link, and it recurred on EVERY sync cycle. feedback_d2082110.
|
|
2542
|
+
if (remoteKeysWithChildren.has(toPosixKey(remoteFile.key))) {
|
|
2543
|
+
console.error(
|
|
2544
|
+
` Note: ${remoteFile.key} is a directory-overlay marker whose ` +
|
|
2545
|
+
`contents already exist locally as a directory; keeping the ` +
|
|
2546
|
+
`local directory and ignoring the marker (no action needed).`,
|
|
2547
|
+
);
|
|
2548
|
+
items.push({
|
|
2549
|
+
action: "skip-overlay-marker-with-children",
|
|
2550
|
+
remoteFile,
|
|
2551
|
+
localPath,
|
|
2552
|
+
});
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2440
2555
|
console.error(
|
|
2441
2556
|
` Warning: ${remoteFile.key} exists locally as a directory; ` +
|
|
2442
2557
|
`cloud has a single object at this key. Skipping; manual ` +
|
|
@@ -2896,6 +3011,16 @@ function defaultConsoleLogger(event: SyncProgressEvent): void {
|
|
|
2896
3011
|
if (event.count > event.samplePaths.length) {
|
|
2897
3012
|
console.log(` ... and ${event.count - event.samplePaths.length} more`);
|
|
2898
3013
|
}
|
|
3014
|
+
} else if (event.type === "scope-materialization-gap") {
|
|
3015
|
+
console.warn(
|
|
3016
|
+
` ! ${event.count} item${event.count === 1 ? "" : "s"} in this vault did NOT sync to this device — your access level is "${event.syncMode}", so only shared paths materialize. To pull everything you have access to, set this membership's access level to "all":`,
|
|
3017
|
+
);
|
|
3018
|
+
for (const p of event.samplePaths) {
|
|
3019
|
+
console.warn(` · ${p}`);
|
|
3020
|
+
}
|
|
3021
|
+
if (event.count > event.samplePaths.length) {
|
|
3022
|
+
console.warn(` ... and ${event.count - event.samplePaths.length} more`);
|
|
3023
|
+
}
|
|
2899
3024
|
} else if (event.type === "ignore-excluded") {
|
|
2900
3025
|
console.warn(
|
|
2901
3026
|
` ! ${event.count} path${event.count === 1 ? "" : "s"} were EXCLUDED from sync by an ignore rule and did NOT reach the vault (review in case this is unintended):`,
|
|
@@ -136,6 +136,13 @@ export interface PruneConflictIndexResult {
|
|
|
136
136
|
removedMirrors: number;
|
|
137
137
|
/** Rows kept (genuine divergence, or unprovable — fail-safe retained). */
|
|
138
138
|
kept: number;
|
|
139
|
+
/**
|
|
140
|
+
* Up to 10 ORIGINAL (non-mirror) paths of the kept rows, for a
|
|
141
|
+
* post-sync reconcile surface (`conflicts-remaining`). Empty when
|
|
142
|
+
* `kept === 0`. Lets a caller name the preserved conflict variants a human
|
|
143
|
+
* still has to resolve without re-reading the index. feedback_d2082110.
|
|
144
|
+
*/
|
|
145
|
+
keptSamplePaths: string[];
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
/**
|
|
@@ -198,6 +205,7 @@ export function pruneConflictIndex(hqRoot: string): PruneConflictIndexResult {
|
|
|
198
205
|
prunedIdentical: 0,
|
|
199
206
|
removedMirrors: 0,
|
|
200
207
|
kept: 0,
|
|
208
|
+
keptSamplePaths: [],
|
|
201
209
|
};
|
|
202
210
|
|
|
203
211
|
const index = readConflictIndex(hqRoot);
|
|
@@ -254,6 +262,9 @@ export function pruneConflictIndex(hqRoot: string): PruneConflictIndexResult {
|
|
|
254
262
|
}
|
|
255
263
|
}
|
|
256
264
|
|
|
265
|
+
// Name the preserved divergences for the post-sync reconcile surface.
|
|
266
|
+
result.keptSamplePaths = kept.slice(0, 10).map((e) => e.originalPath);
|
|
267
|
+
|
|
257
268
|
// No row dropped → leave the file (and its mtime) untouched.
|
|
258
269
|
if (kept.length === index.conflicts.length) return result;
|
|
259
270
|
|
package/src/lib/conflict.test.ts
CHANGED
|
@@ -194,6 +194,7 @@ describe("pruneConflictIndex", () => {
|
|
|
194
194
|
prunedIdentical: 0,
|
|
195
195
|
removedMirrors: 0,
|
|
196
196
|
kept: 0,
|
|
197
|
+
keptSamplePaths: [],
|
|
197
198
|
});
|
|
198
199
|
expect(fs.existsSync(getConflictIndexPath(tmpHq))).toBe(false);
|
|
199
200
|
});
|
|
@@ -292,6 +293,7 @@ describe("pruneConflictIndex", () => {
|
|
|
292
293
|
prunedIdentical: 1,
|
|
293
294
|
removedMirrors: 1,
|
|
294
295
|
kept: 1,
|
|
296
|
+
keptSamplePaths: ["dir/real.md"],
|
|
295
297
|
});
|
|
296
298
|
expect(readConflictIndex(tmpHq).conflicts.map((c) => c.id)).toEqual(["real"]);
|
|
297
299
|
});
|
|
@@ -312,4 +314,35 @@ describe("pruneConflictIndex", () => {
|
|
|
312
314
|
pruneConflictIndex(tmpHq);
|
|
313
315
|
expect(fs.statSync(indexPath).mtimeMs).toBe(before);
|
|
314
316
|
});
|
|
317
|
+
|
|
318
|
+
it("names the preserved conflict variants (keptSamplePaths, capped at 10) for the reconcile surface (feedback_d2082110)", () => {
|
|
319
|
+
// Reporter feedback_d2082110 finished a full sync with "20 older preserved
|
|
320
|
+
// conflict entries" still on disk and NO signal they were there. The prune
|
|
321
|
+
// self-heals the ledger but conservatively KEEPS genuine divergences; those
|
|
322
|
+
// were silent. keptSamplePaths surfaces the original (non-mirror) paths so
|
|
323
|
+
// the runner can emit a `conflicts-remaining` reconcile line pointing the
|
|
324
|
+
// operator at `/resolve-conflicts`.
|
|
325
|
+
const rows: ConflictIndexEntry[] = [];
|
|
326
|
+
for (let i = 0; i < 12; i++) {
|
|
327
|
+
const id = `real-${String(i).padStart(2, "0")}`;
|
|
328
|
+
const row = rowFor(id);
|
|
329
|
+
put(row.originalPath, `local-${i}`);
|
|
330
|
+
put(row.conflictPath, `remote-${i}`);
|
|
331
|
+
rows.push(row);
|
|
332
|
+
}
|
|
333
|
+
writeConflictIndex(tmpHq, { version: 1, conflicts: rows });
|
|
334
|
+
|
|
335
|
+
const res = pruneConflictIndex(tmpHq);
|
|
336
|
+
expect(res.kept).toBe(12);
|
|
337
|
+
// Capped at 10 samples, drawn from the kept (detectedAt-sorted) rows.
|
|
338
|
+
expect(res.keptSamplePaths).toHaveLength(10);
|
|
339
|
+
expect(res.keptSamplePaths.every((p) => p.startsWith("dir/real-"))).toBe(true);
|
|
340
|
+
// Orphaned/identical rows never contribute to the surfaced sample.
|
|
341
|
+
const orphan = rowFor("orphan");
|
|
342
|
+
put(orphan.originalPath, "x"); // mirror missing → dropped, not sampled
|
|
343
|
+
writeConflictIndex(tmpHq, { version: 1, conflicts: [orphan] });
|
|
344
|
+
const res2 = pruneConflictIndex(tmpHq);
|
|
345
|
+
expect(res2.kept).toBe(0);
|
|
346
|
+
expect(res2.keptSamplePaths).toEqual([]);
|
|
347
|
+
});
|
|
315
348
|
});
|