@plinth-music/cli 0.23.0 → 0.23.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/cli.js CHANGED
@@ -7584,7 +7584,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
7584
7584
  // package.json
7585
7585
  var package_default = {
7586
7586
  name: "@plinth-music/cli",
7587
- version: "0.23.0",
7587
+ version: "0.23.2",
7588
7588
  description: "Plinth capstone sync daemon and CLI",
7589
7589
  type: "module",
7590
7590
  private: false,
@@ -7843,8 +7843,48 @@ function emptyCorpusCounts() {
7843
7843
  return { nonMarkdown: 0, unreadable: 0 };
7844
7844
  }
7845
7845
 
7846
+ // src/lib/matter.ts
7847
+ var import_gray_matter = __toESM(require_gray_matter(), 1);
7848
+ var FRONTMATTER_IS_CODE = "it is marked as code, which Plinth never runs";
7849
+ var PATCHED = Symbol.for("plinth.matter.engines-before-patch");
7850
+ var marked = (table) => table[PATCHED];
7851
+ var refuse = {
7852
+ parse() {
7853
+ throw new Error(FRONTMATTER_IS_CODE);
7854
+ },
7855
+ stringify() {
7856
+ throw new Error(FRONTMATTER_IS_CODE);
7857
+ }
7858
+ };
7859
+ var engines = import_gray_matter.default.engines;
7860
+ if (marked(engines) === undefined) {
7861
+ const before = Object.keys(engines).sort();
7862
+ const safeLoad = engines.yaml.parse;
7863
+ Object.assign(engines, {
7864
+ javascript: refuse,
7865
+ coffee: refuse,
7866
+ yaml: { parse: (input) => safeLoad(input), stringify: engines.yaml.stringify }
7867
+ });
7868
+ Object.defineProperty(engines, PATCHED, { value: before });
7869
+ }
7870
+ var ENGINES_BEFORE_PATCH = marked(engines);
7871
+ function matter(input, read) {
7872
+ return read === undefined ? import_gray_matter.default(input) : import_gray_matter.default(input, { cache: false });
7873
+ }
7874
+ function stringifyFrontmatter(body, data) {
7875
+ const out = import_gray_matter.default.stringify({ content: body }, data);
7876
+ const bodyOnly = body.endsWith(`
7877
+ `) ? body : `${body}
7878
+ `;
7879
+ if (out === bodyOnly && import_gray_matter.default.test(body.replace(/^/, ""))) {
7880
+ return `---
7881
+ ---
7882
+ ${bodyOnly}`;
7883
+ }
7884
+ return out;
7885
+ }
7886
+
7846
7887
  // src/lib/backlinks/scan.ts
7847
- var import_gray_matter4 = __toESM(require_gray_matter(), 1);
7848
7888
  import { readFileSync as readFileSync2 } from "node:fs";
7849
7889
  import { relative, sep as sep2 } from "node:path";
7850
7890
 
@@ -7860,9 +7900,6 @@ import {
7860
7900
  } from "node:fs";
7861
7901
  import { dirname, join as join2, resolve, sep } from "node:path";
7862
7902
 
7863
- // src/lib/entities/_helpers.ts
7864
- var import_gray_matter = __toESM(require_gray_matter(), 1);
7865
-
7866
7903
  // node_modules/zod/v4/classic/external.js
7867
7904
  var exports_external = {};
7868
7905
  __export(exports_external, {
@@ -22168,7 +22205,8 @@ var TombstoneSchema = exports_external.object({
22168
22205
  function makePageResponseSchema(responseKey, liveSchema) {
22169
22206
  return exports_external.object({
22170
22207
  [responseKey]: exports_external.array(exports_external.union([liveSchema, TombstoneSchema])),
22171
- next_cursor: exports_external.string().nullable()
22208
+ next_cursor: exports_external.string().nullable(),
22209
+ high_water: exports_external.string().nullable().optional()
22172
22210
  });
22173
22211
  }
22174
22212
  function makeParsePageResponse(schema, responseKey) {
@@ -22176,7 +22214,8 @@ function makeParsePageResponse(schema, responseKey) {
22176
22214
  const parsed = schema.parse(json2);
22177
22215
  return {
22178
22216
  rows: parsed[responseKey],
22179
- next_cursor: parsed.next_cursor
22217
+ next_cursor: parsed.next_cursor,
22218
+ high_water: parsed.high_water ?? null
22180
22219
  };
22181
22220
  };
22182
22221
  }
@@ -22187,7 +22226,7 @@ function buildFrontmatter(doc2, projectionKeys) {
22187
22226
  return { ...doc2.frontmatter, ...projection };
22188
22227
  }
22189
22228
  function serialiseRow(doc2, projectionKeys, bodyAccessor) {
22190
- const out = import_gray_matter.default.stringify(bodyAccessor(doc2), buildFrontmatter(doc2, projectionKeys));
22229
+ const out = stringifyFrontmatter(bodyAccessor(doc2), buildFrontmatter(doc2, projectionKeys));
22191
22230
  return out.endsWith(`
22192
22231
  `) ? out : out + `
22193
22232
  `;
@@ -22827,7 +22866,6 @@ var documentsStrategy = {
22827
22866
  };
22828
22867
 
22829
22868
  // src/lib/entities/meetings.ts
22830
- var import_gray_matter2 = __toESM(require_gray_matter(), 1);
22831
22869
  var MeetingLiveSchema = exports_external.object({
22832
22870
  ...BASE_ROW,
22833
22871
  deleted_at: exports_external.null(),
@@ -22982,7 +23020,7 @@ function serialise(doc2) {
22982
23020
  decisions: doc2.decisions,
22983
23021
  follow_up_summary: doc2.follow_up_summary
22984
23022
  });
22985
- const out = import_gray_matter2.default.stringify(body, fm);
23023
+ const out = stringifyFrontmatter(body, fm);
22986
23024
  return out.endsWith(`
22987
23025
  `) ? out : out + `
22988
23026
  `;
@@ -24473,7 +24511,6 @@ function readContextFile(workspaceSlug, relativePath) {
24473
24511
  }
24474
24512
 
24475
24513
  // src/lib/backlinks/parse.ts
24476
- var import_gray_matter3 = __toESM(require_gray_matter(), 1);
24477
24514
  var RENDERED_BLOCKS = [
24478
24515
  { name: "tasks", begin: TASKS_BLOCK_BEGIN, end: TASKS_BLOCK_END },
24479
24516
  { name: "comments", begin: COMMENTS_BLOCK_BEGIN, end: COMMENTS_BLOCK_END },
@@ -24555,7 +24592,7 @@ function frontmatterLinks(body) {
24555
24592
  return null;
24556
24593
  let data;
24557
24594
  try {
24558
- data = import_gray_matter3.default(body).data;
24595
+ data = matter(body).data;
24559
24596
  } catch {
24560
24597
  return null;
24561
24598
  }
@@ -24823,7 +24860,7 @@ function buildArtistIndex(relPaths, root, readFile) {
24823
24860
  const slug = m[1];
24824
24861
  let name = null;
24825
24862
  try {
24826
- const raw = import_gray_matter4.default(readFile(`${root}${sep2}${rel}`)).data;
24863
+ const raw = matter(readFile(`${root}${sep2}${rel}`)).data;
24827
24864
  if (typeof raw.name === "string" && raw.name.trim().length > 0) {
24828
24865
  name = raw.name.trim();
24829
24866
  }
@@ -25345,7 +25382,8 @@ var WorkspaceEntrySchema = exports_external.object({
25345
25382
  last_synced_at: exports_external.string().datetime({ offset: true }).nullable(),
25346
25383
  workspace_name: exports_external.string().min(1).optional(),
25347
25384
  member: MemberPrincipalSchema.optional(),
25348
- sync_cursors: SyncCursorsSchema.optional()
25385
+ sync_cursors: SyncCursorsSchema.optional(),
25386
+ sync_high_water: SyncCursorsSchema.optional()
25349
25387
  });
25350
25388
  var ConfigFileSchema = exports_external.object({
25351
25389
  version: exports_external.literal(1),
@@ -25407,7 +25445,8 @@ function carryForwardSyncState(prior, entry) {
25407
25445
  return {
25408
25446
  ...entry,
25409
25447
  last_synced_at: prior.last_synced_at,
25410
- ...prior.sync_cursors !== undefined ? { sync_cursors: prior.sync_cursors } : {}
25448
+ ...prior.sync_cursors !== undefined ? { sync_cursors: prior.sync_cursors } : {},
25449
+ ...prior.sync_high_water !== undefined ? { sync_high_water: prior.sync_high_water } : {}
25411
25450
  };
25412
25451
  }
25413
25452
  function upsertWorkspace(slug, entry, options2 = {}) {
@@ -25448,6 +25487,23 @@ function updateSyncCursor(slug, entityType, cursor) {
25448
25487
  writeAtomic(next);
25449
25488
  return next;
25450
25489
  }
25490
+ function updateSyncHighWater(slug, entityType, mark) {
25491
+ const existing = loadConfig();
25492
+ if (!existing) {
25493
+ throw new Error(`No config. Run \`plinth login --workspace ${slug}\` first.`);
25494
+ }
25495
+ const entry = requireWorkspace(existing, slug);
25496
+ if ((entry.sync_high_water?.[entityType] ?? null) === mark)
25497
+ return;
25498
+ const nextEntry = {
25499
+ ...entry,
25500
+ sync_high_water: { ...entry.sync_high_water, [entityType]: mark }
25501
+ };
25502
+ writeAtomic({
25503
+ ...existing,
25504
+ workspaces: { ...existing.workspaces, [slug]: nextEntry }
25505
+ });
25506
+ }
25451
25507
  function setMemberPrincipal(slug, member) {
25452
25508
  const existing = loadConfig();
25453
25509
  if (!existing)
@@ -25613,7 +25669,8 @@ var TripReasonSchema = exports_external.object({
25613
25669
  entityType: exports_external.string().optional(),
25614
25670
  typeCount: exports_external.number().int().nonnegative().optional(),
25615
25671
  typePopulation: exports_external.number().int().nonnegative().optional(),
25616
- pct: exports_external.number().optional()
25672
+ pct: exports_external.number().optional(),
25673
+ loneRemoved: exports_external.number().int().nonnegative().optional()
25617
25674
  });
25618
25675
  var PendingBatchFileSchema = exports_external.object({
25619
25676
  version: exports_external.literal(1),
@@ -25722,6 +25779,9 @@ function describeTrip(reason) {
25722
25779
  const pct = reason.pct !== undefined ? ` (${Math.round(reason.pct * 100)}%)` : "";
25723
25780
  return `${reason.typeCount}/${reason.typePopulation} ${reason.entityType}${pct} — proportion threshold`;
25724
25781
  }
25782
+ if (reason.loneRemoved !== undefined) {
25783
+ return reason.count === 1 ? "1 large overwrite — size threshold" : `${reason.count} destructive ops, including a large overwrite — size threshold`;
25784
+ }
25725
25785
  return `${reason.count} destructive ops — count threshold`;
25726
25786
  }
25727
25787
  function formatBytes(n) {
@@ -26735,7 +26795,7 @@ function allFileEntries(state) {
26735
26795
  return Object.values(state.files ?? {});
26736
26796
  }
26737
26797
  function fileEntriesEqual(a, b) {
26738
- return a.id === b.id && a.path === b.path && a.content_hash === b.content_hash && a.size_bytes === b.size_bytes && a.mime_type === b.mime_type && a.updated_at === b.updated_at && a.inode === b.inode;
26798
+ return a.id === b.id && a.path === b.path && a.content_hash === b.content_hash && a.size_bytes === b.size_bytes && a.mime_type === b.mime_type && a.updated_at === b.updated_at && a.inode === b.inode && a.text_hash === b.text_hash;
26739
26799
  }
26740
26800
  function clearFrontmatterIdBackfillMarker(state) {
26741
26801
  if (state.frontmatter_id_backfill_version === undefined) {
@@ -27609,7 +27669,6 @@ function decideRegen(input) {
27609
27669
  }
27610
27670
 
27611
27671
  // src/lib/context/roster.ts
27612
- var import_gray_matter5 = __toESM(require_gray_matter(), 1);
27613
27672
  import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "node:fs";
27614
27673
  import { join as join7 } from "node:path";
27615
27674
  function readRoster(workspaceSlug) {
@@ -27634,7 +27693,7 @@ function readRoster(workspaceSlug) {
27634
27693
  continue;
27635
27694
  }
27636
27695
  try {
27637
- const fm = import_gray_matter5.default(raw).data;
27696
+ const fm = matter(raw).data;
27638
27697
  const name = typeof fm.name === "string" && fm.name.length > 0 ? fm.name : slug;
27639
27698
  const status = typeof fm.status === "string" ? fm.status : null;
27640
27699
  roster.push({ slug, name, status });
@@ -29024,7 +29083,6 @@ var SKILL_LOCATIONS = [
29024
29083
  var SECURITY_NOTE = "Scoped to a single Plinth workspace: do not read or write across " + "workspace folders; each has its own token and isolation boundary.";
29025
29084
 
29026
29085
  // src/lib/context/artist-data.ts
29027
- var import_gray_matter6 = __toESM(require_gray_matter(), 1);
29028
29086
  import { readFileSync as readFileSync10 } from "node:fs";
29029
29087
  function artistUuidBySlug(state) {
29030
29088
  const map2 = new Map;
@@ -29151,7 +29209,7 @@ function readEntityFrontmatter(workspaceSlug, entityType, slug) {
29151
29209
  return null;
29152
29210
  }
29153
29211
  try {
29154
- return import_gray_matter6.default(raw).data;
29212
+ return matter(raw).data;
29155
29213
  } catch {
29156
29214
  return null;
29157
29215
  }
@@ -30195,7 +30253,8 @@ var FileTombstoneSchema = exports_external.object({
30195
30253
  var FileRowSchema = exports_external.union([FileLiveSchema, FileTombstoneSchema]);
30196
30254
  var FilesPageSchema = exports_external.object({
30197
30255
  files: exports_external.array(FileRowSchema),
30198
- next_cursor: exports_external.string().nullable()
30256
+ next_cursor: exports_external.string().nullable(),
30257
+ high_water: exports_external.string().nullable().optional()
30199
30258
  });
30200
30259
  var FileWithDownloadSchema = FileLiveSchema.extend({
30201
30260
  download_url: exports_external.string().url().nullable()
@@ -30248,7 +30307,11 @@ async function syncFilesPage(args) {
30248
30307
  if (!parsed.success) {
30249
30308
  throw new ApiError(res.status, `sync files response did not match the expected shape: ${parsed.error.message}`);
30250
30309
  }
30251
- return { rows: parsed.data.files, next_cursor: parsed.data.next_cursor };
30310
+ return {
30311
+ rows: parsed.data.files,
30312
+ next_cursor: parsed.data.next_cursor,
30313
+ high_water: parsed.data.high_water ?? null
30314
+ };
30252
30315
  }
30253
30316
  async function getFile(args) {
30254
30317
  const { baseUrl, pat, id, fetchImpl = fetch } = args;
@@ -30519,6 +30582,15 @@ function fileDeleteSuppressedLine(f) {
30519
30582
  function entityDeleteSuppressedLine(f) {
30520
30583
  return `[ENTITY_DELETE_SUPPRESSED entityType=${f.entityType} slug=${f.slug} reason=${f.reason}]`;
30521
30584
  }
30585
+ function entityPushHeldLine(f) {
30586
+ return `[PUSH_HELD entityType=${f.entityType} slug=${f.slug} reason=${f.reason}]`;
30587
+ }
30588
+ function entityRestoreDeferredLine(f) {
30589
+ return `[ENTITY_RESTORE_DEFERRED entityType=${f.entityType} slug=${f.slug} wait_s=${f.waitSeconds}]`;
30590
+ }
30591
+ function fileRestoreDeferredLine(f) {
30592
+ return `[FILE_RESTORE_DEFERRED path=${f.path} wait_s=${f.waitSeconds}]`;
30593
+ }
30522
30594
  function fileOverCapLine(f) {
30523
30595
  return `[FILE_OVER_CAP path=${f.path} size=${f.size} max=${f.max} max_from=${f.source}]`;
30524
30596
  }
@@ -30707,6 +30779,34 @@ function recordEntityRejected(args) {
30707
30779
  rejected_at: args.now().toISOString()
30708
30780
  }));
30709
30781
  }
30782
+ var FRONTMATTER_UNREADABLE_REASON = "frontmatter_unreadable";
30783
+ function frontmatterUnreadableSentence(why) {
30784
+ const reason = why.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
30785
+ return `not synced, because its frontmatter could not be read (${reason}). ` + `Fix the frontmatter and it syncs.`;
30786
+ }
30787
+ function recordEntityFrontmatterUnreadable(args) {
30788
+ let path;
30789
+ try {
30790
+ path = relative4(workspaceMirrorPath(args.workspaceSlug), args.absolutePath);
30791
+ } catch {
30792
+ path = args.absolutePath;
30793
+ }
30794
+ const message = frontmatterUnreadableSentence(args.why);
30795
+ const withoutStale = removeUnsynced(args.state, "entity", path);
30796
+ const existing = listRejected(args.state).find((o) => o.target === "entity" && o.path === path);
30797
+ if (!withoutStale.changed && existing?.reason === FRONTMATTER_UNREADABLE_REASON && existing.message === message) {
30798
+ return args.state;
30799
+ }
30800
+ return commitState(args.state, upsertRejected(withoutStale.state, {
30801
+ target: "entity",
30802
+ op: "upsert",
30803
+ path,
30804
+ status: 0,
30805
+ reason: FRONTMATTER_UNREADABLE_REASON,
30806
+ message,
30807
+ rejected_at: args.now().toISOString()
30808
+ }).state);
30809
+ }
30710
30810
  function recordFileRejected(args) {
30711
30811
  const cat = categorizeError(args.err);
30712
30812
  const withoutStale = removeUnsynced(args.state, "file", args.relPath).state;
@@ -34287,8 +34387,7 @@ var sessionCommand = defineCommand({
34287
34387
  });
34288
34388
 
34289
34389
  // src/commands/start.ts
34290
- var import_gray_matter14 = __toESM(require_gray_matter(), 1);
34291
- import { existsSync as existsSync10, readFileSync as readFileSync30, statSync as statSync18 } from "node:fs";
34390
+ import { existsSync as existsSync10, readFileSync as readFileSync31, statSync as statSync18 } from "node:fs";
34292
34391
 
34293
34392
  // src/lib/daemon-log.ts
34294
34393
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
@@ -34354,9 +34453,311 @@ class DaemonLog {
34354
34453
  }
34355
34454
 
34356
34455
  // src/lib/destructive-gate.ts
34357
- var import_gray_matter7 = __toESM(require_gray_matter(), 1);
34358
- import { readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
34456
+ import { readFileSync as readFileSync16, statSync as statSync6 } from "node:fs";
34359
34457
  import { basename as basename4 } from "node:path";
34458
+
34459
+ // src/lib/pull-write.ts
34460
+ import { readFileSync as readFileSync15 } from "node:fs";
34461
+
34462
+ // src/lib/conflicts.ts
34463
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync14, readdirSync as readdirSync6, statSync as statSync5, writeFileSync as writeFileSync7 } from "node:fs";
34464
+ import { dirname as dirname9, join as join12 } from "node:path";
34465
+ function conflictsRoot(workspaceSlug) {
34466
+ return join12(plinthHome(), "_conflicts", workspaceSlug);
34467
+ }
34468
+ function conflictFilePath(args) {
34469
+ const { workspaceSlug, entityType, entitySlug, timestamp, side } = args;
34470
+ const iso = timestamp.toISOString().replace(/[:.]/g, "-");
34471
+ return join12(conflictsRoot(workspaceSlug), `${entityType}.${entitySlug}.${iso}.${side}.md`);
34472
+ }
34473
+ function stashConflict(args) {
34474
+ const target = conflictFilePath(args);
34475
+ mkdirSync10(dirname9(target), { recursive: true, mode: 448 });
34476
+ writeFileSync7(target, args.contents, { mode: 384 });
34477
+ return target;
34478
+ }
34479
+ function stashEntityConflictOnce(args) {
34480
+ const dir = conflictsRoot(args.workspaceSlug);
34481
+ const head = `${args.entityType}.${args.entitySlug}.`;
34482
+ const tail = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${args.side}\\.md$`);
34483
+ let names = [];
34484
+ try {
34485
+ names = readdirSync6(dir);
34486
+ } catch {
34487
+ names = [];
34488
+ }
34489
+ const newest = names.filter((name) => name.startsWith(head) && tail.test(name.slice(head.length))).sort().pop();
34490
+ if (newest !== undefined) {
34491
+ const path5 = join12(dir, newest);
34492
+ try {
34493
+ if (readFileSync14(path5, "utf8") === args.contents)
34494
+ return { path: path5, reused: true };
34495
+ } catch {}
34496
+ }
34497
+ return { path: stashConflict(args), reused: false };
34498
+ }
34499
+ function fileConflictPath(args) {
34500
+ const { workspaceSlug, relPath, timestamp, side } = args;
34501
+ const iso = timestamp.toISOString().replace(/[:.]/g, "-");
34502
+ return join12(conflictsRoot(workspaceSlug), `${fileConflictStem(relPath)}.${iso}.${side}`);
34503
+ }
34504
+ function fileConflictStem(relPath) {
34505
+ return `files.${relPath.replace(/[/\\]/g, "__")}`;
34506
+ }
34507
+ function stashFileConflict(args) {
34508
+ const target = fileConflictPath(args);
34509
+ mkdirSync10(dirname9(target), { recursive: true, mode: 448 });
34510
+ writeFileSync7(target, args.bytes, { mode: 384 });
34511
+ return target;
34512
+ }
34513
+ function stashFileConflictOnce(args) {
34514
+ const dir = conflictsRoot(args.workspaceSlug);
34515
+ const head = `${fileConflictStem(args.relPath)}.`;
34516
+ const tail = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${args.side}$`);
34517
+ let names = [];
34518
+ try {
34519
+ names = readdirSync6(dir);
34520
+ } catch {
34521
+ names = [];
34522
+ }
34523
+ const newest = names.filter((n) => n.startsWith(head) && tail.test(n.slice(head.length))).sort().pop();
34524
+ if (newest !== undefined) {
34525
+ const path5 = join12(dir, newest);
34526
+ try {
34527
+ if (Buffer.compare(args.bytes, readFileSync14(path5)) === 0)
34528
+ return { path: path5, reused: true };
34529
+ } catch {}
34530
+ }
34531
+ return { path: stashFileConflict(args), reused: false };
34532
+ }
34533
+ var STAMP_RE2 = /\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)(?:\.(local|cloud))?(?:\.md)?$/;
34534
+ function parseStamp(stamp) {
34535
+ const iso = stamp.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
34536
+ const d = new Date(iso);
34537
+ return Number.isNaN(d.getTime()) ? null : d;
34538
+ }
34539
+ function labelFor(file2) {
34540
+ const m = STAMP_RE2.exec(file2);
34541
+ if (!m)
34542
+ return file2;
34543
+ const head = file2.slice(0, m.index);
34544
+ if (head.startsWith("files.")) {
34545
+ return `files/${head.slice("files.".length).replace(/__/g, "/")}`;
34546
+ }
34547
+ const dot = head.indexOf(".");
34548
+ if (dot === -1)
34549
+ return head;
34550
+ return `${head.slice(0, dot)}/${head.slice(dot + 1)}`;
34551
+ }
34552
+ function listConflictStashes(workspaceSlug) {
34553
+ const dir = conflictsRoot(workspaceSlug);
34554
+ let names;
34555
+ try {
34556
+ names = readdirSync6(dir);
34557
+ } catch {
34558
+ return [];
34559
+ }
34560
+ const out = [];
34561
+ for (const file2 of names) {
34562
+ const absPath = join12(dir, file2);
34563
+ let st;
34564
+ try {
34565
+ st = statSync5(absPath);
34566
+ } catch {
34567
+ continue;
34568
+ }
34569
+ if (!st.isFile())
34570
+ continue;
34571
+ if (file2.startsWith("."))
34572
+ continue;
34573
+ const m = STAMP_RE2.exec(file2);
34574
+ const side = m?.[2];
34575
+ out.push({
34576
+ file: file2,
34577
+ absPath,
34578
+ label: labelFor(file2),
34579
+ stashedAt: m ? parseStamp(m[1]) : null,
34580
+ side: side === "local" || side === "cloud" ? side : null,
34581
+ mtimeMs: st.mtimeMs
34582
+ });
34583
+ }
34584
+ return out.sort((a, b) => (b.stashedAt?.getTime() ?? b.mtimeMs) - (a.stashedAt?.getTime() ?? a.mtimeMs));
34585
+ }
34586
+
34587
+ // src/lib/pull-write.ts
34588
+ var UNCACHED = { cache: false };
34589
+ function parseUncached(raw) {
34590
+ return matter(raw, UNCACHED);
34591
+ }
34592
+ var NOT_A_MAPPING = "it is not a list of `key: value` lines";
34593
+ var SELF_REFERENCE = "an alias in it refers to itself";
34594
+ function readFrontmatter(raw) {
34595
+ let parsed;
34596
+ try {
34597
+ parsed = parseUncached(raw);
34598
+ } catch (err) {
34599
+ return { ok: false, why: yamlReason(err) };
34600
+ }
34601
+ if (Object.prototype.toString.call(parsed.data) !== "[object Object]") {
34602
+ return { ok: false, why: NOT_A_MAPPING };
34603
+ }
34604
+ try {
34605
+ JSON.stringify(parsed.data);
34606
+ } catch {
34607
+ return { ok: false, why: SELF_REFERENCE };
34608
+ }
34609
+ return { ok: true, parsed };
34610
+ }
34611
+ function yamlReason(err) {
34612
+ const message = err instanceof Error ? err.message : String(err);
34613
+ return (message.split(`
34614
+ `)[0] ?? "").replace(/:\s*$/, "").trim();
34615
+ }
34616
+ function frontmatterParseError(raw) {
34617
+ const read = readFrontmatter(raw);
34618
+ return read.ok ? null : read.why;
34619
+ }
34620
+ function computeMirrorHashes(strategy, content) {
34621
+ const parsed = matter(content);
34622
+ const fm = parsed.data ?? {};
34623
+ const lifted = splitUserFrontmatter(strategy, fm);
34624
+ const userFm = userFrontmatter(strategy, lifted.frontmatter);
34625
+ const hashBody = hashableBody(strategy, parsed.content);
34626
+ return {
34627
+ bodyHash: stableHash(hashBody),
34628
+ frontmatterHash: stableHash({ ...userFm, ...lifted.topLevel }),
34629
+ bodyLen: hashBody.length,
34630
+ frontmatterId: fm["id"]
34631
+ };
34632
+ }
34633
+ function hashableBody(strategy, body) {
34634
+ return strategy.stripLocalBlocks?.(body) ?? body;
34635
+ }
34636
+ function relocateForSlugChange(args) {
34637
+ const { workspaceSlug, strategy, fromSlug, toSlug, acked, echo, now } = args;
34638
+ if (fromSlug === toSlug)
34639
+ return { kind: "not_needed", writeSlug: toSlug };
34640
+ if (!echo) {
34641
+ return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "unguarded" };
34642
+ }
34643
+ if (strategy.mirrorRelativePath(fromSlug).split("/").length !== 2) {
34644
+ return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "foldered" };
34645
+ }
34646
+ let fromPath;
34647
+ try {
34648
+ fromPath = entityFilePath(workspaceSlug, strategy.type, fromSlug);
34649
+ entityFilePath(workspaceSlug, strategy.type, toSlug);
34650
+ } catch (err) {
34651
+ if (err instanceof MirrorPathError) {
34652
+ return {
34653
+ kind: "skipped",
34654
+ writeSlug: toSlug,
34655
+ fromSlug,
34656
+ reason: "unresolvable"
34657
+ };
34658
+ }
34659
+ throw err;
34660
+ }
34661
+ let raw = null;
34662
+ try {
34663
+ raw = readFileSync15(fromPath, "utf8");
34664
+ } catch (err) {
34665
+ const code = err?.code;
34666
+ if (code === "ENOENT") {
34667
+ return { kind: "absent", writeSlug: toSlug, fromSlug };
34668
+ }
34669
+ return {
34670
+ kind: "refused",
34671
+ writeSlug: fromSlug,
34672
+ fromSlug,
34673
+ reason: "source_unreadable"
34674
+ };
34675
+ }
34676
+ let diverged = true;
34677
+ try {
34678
+ const hashes = computeMirrorHashes(strategy, raw);
34679
+ diverged = hashes.bodyHash !== acked.bodyHash || hashes.frontmatterHash !== acked.frontmatterHash;
34680
+ } catch {
34681
+ diverged = true;
34682
+ }
34683
+ const stashPath = diverged ? stashConflict({
34684
+ workspaceSlug,
34685
+ entityType: strategy.type,
34686
+ entitySlug: fromSlug,
34687
+ timestamp: now(),
34688
+ contents: raw,
34689
+ side: "local"
34690
+ }) : undefined;
34691
+ try {
34692
+ let registered = false;
34693
+ const result = moveEntityFile(workspaceSlug, strategy.type, fromSlug, toSlug, () => {
34694
+ if (echo.watcher === "live") {
34695
+ echo.suppressor.registerDelete(fromPath);
34696
+ registered = true;
34697
+ }
34698
+ });
34699
+ if (result === "moved") {
34700
+ return { kind: "moved", writeSlug: toSlug, fromSlug, stashPath };
34701
+ }
34702
+ if (registered && echo.watcher === "live") {
34703
+ echo.suppressor.cancelDelete(fromPath);
34704
+ }
34705
+ return { kind: "absent", writeSlug: toSlug, fromSlug };
34706
+ } catch (err) {
34707
+ if (err instanceof MirrorDestinationOccupiedError) {
34708
+ return {
34709
+ kind: "refused",
34710
+ writeSlug: fromSlug,
34711
+ fromSlug,
34712
+ reason: "destination_occupied",
34713
+ stashPath
34714
+ };
34715
+ }
34716
+ if (err instanceof MirrorPathError) {
34717
+ return {
34718
+ kind: "skipped",
34719
+ writeSlug: toSlug,
34720
+ fromSlug,
34721
+ reason: "unresolvable"
34722
+ };
34723
+ }
34724
+ throw err;
34725
+ }
34726
+ }
34727
+ function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor, pathSlug) {
34728
+ const targetSlug = pathSlug ?? doc2.slug;
34729
+ const content = renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug);
34730
+ if (suppressor) {
34731
+ const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
34732
+ suppressor.registerWrite(target, computeMirrorHashes(strategy, content));
34733
+ }
34734
+ writeMirrorFile(workspaceSlug, strategy.type, targetSlug, content);
34735
+ return content;
34736
+ }
34737
+ function renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug) {
34738
+ let content = strategy.serialise(doc2);
34739
+ if (strategy.preserveLocalBodyOnPull) {
34740
+ const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
34741
+ let existing = null;
34742
+ try {
34743
+ existing = readFileSync15(target, "utf8");
34744
+ } catch (err) {
34745
+ if (err?.code !== "ENOENT")
34746
+ throw err;
34747
+ }
34748
+ if (existing !== null) {
34749
+ const existingParsed = matter(existing);
34750
+ const cloudParsed = matter(content);
34751
+ const stitched = stringifyFrontmatter(existingParsed.content, cloudParsed.data);
34752
+ content = stitched.endsWith(`
34753
+ `) ? stitched : stitched + `
34754
+ `;
34755
+ }
34756
+ }
34757
+ return content;
34758
+ }
34759
+
34760
+ // src/lib/destructive-gate.ts
34360
34761
  var GUTTING_RATIO_DEFAULT = 0.5;
34361
34762
  var GUTTING_MIN_PRIOR_BYTES = 200;
34362
34763
  function envFloat(name, def) {
@@ -34399,10 +34800,10 @@ function classifyDestructive(event, entityType, state, deps = {}) {
34399
34800
  absolutePath: event.absolutePath
34400
34801
  };
34401
34802
  }
34402
- const readFile = deps.readFile ?? ((p) => readFileSync14(p, "utf8"));
34803
+ const readFile = deps.readFile ?? ((p) => readFileSync16(p, "utf8"));
34403
34804
  const statInode = deps.statInode ?? ((p) => {
34404
34805
  try {
34405
- return statSync5(p).ino;
34806
+ return statSync6(p).ino;
34406
34807
  } catch {
34407
34808
  return;
34408
34809
  }
@@ -34413,13 +34814,11 @@ function classifyDestructive(event, entityType, state, deps = {}) {
34413
34814
  } catch {
34414
34815
  return null;
34415
34816
  }
34416
- let newLen;
34417
- try {
34418
- const body = import_gray_matter7.default(raw).content;
34419
- newLen = (getStrategy(entityType)?.stripLocalBlocks?.(body) ?? body).length;
34420
- } catch {
34817
+ const read = readFrontmatter(raw);
34818
+ if (!read.ok)
34421
34819
  return null;
34422
- }
34820
+ const body = read.parsed.content;
34821
+ const newLen = (getStrategy(entityType)?.stripLocalBlocks?.(body) ?? body).length;
34423
34822
  const liveInode = statInode(event.absolutePath);
34424
34823
  const slug = basename4(event.absolutePath, ".md");
34425
34824
  const entry2 = (liveInode !== undefined ? getEntryByInode(state, entityType, liveInode) : undefined) ?? getEntryBySlug(state, entityType, slug);
@@ -34438,13 +34837,26 @@ function classifyDestructive(event, entityType, state, deps = {}) {
34438
34837
  newLen
34439
34838
  };
34440
34839
  }
34840
+ var LONE_MIN_REMOVED_DEFAULT = 5000;
34841
+ var LONE_MIN_REMOVED_BYTES_DEFAULT = 100 * 1024;
34441
34842
  function defaultThresholds() {
34442
34843
  return {
34443
34844
  absCount: envInt("PLINTH_GATE_ABS_COUNT", 3),
34444
34845
  propMinOps: envInt("PLINTH_GATE_PROP_MIN_OPS", 2),
34445
- propPct: envFloat("PLINTH_GATE_PROP_PCT", 0.5)
34846
+ propPct: envFloat("PLINTH_GATE_PROP_PCT", 0.5),
34847
+ loneMinRemoved: envInt("PLINTH_GATE_LONE_MIN_REMOVED", LONE_MIN_REMOVED_DEFAULT),
34848
+ loneMinRemovedBytes: envInt("PLINTH_GATE_LONE_MIN_REMOVED_BYTES", LONE_MIN_REMOVED_BYTES_DEFAULT)
34446
34849
  };
34447
34850
  }
34851
+ function tripReasonToken(reason) {
34852
+ return reason.loneRemoved !== undefined ? "size" : reason.kind;
34853
+ }
34854
+ function evaluateLoneOverwrite(count, removed, floor) {
34855
+ const loneRemoved = Math.max(0, ...removed);
34856
+ if (loneRemoved < floor)
34857
+ return { tripped: false };
34858
+ return { tripped: true, reason: { kind: "count", count, loneRemoved } };
34859
+ }
34448
34860
  function evaluateBatch(ops, typePopulations, thresholds = defaultThresholds()) {
34449
34861
  const count = ops.length;
34450
34862
  if (count === 0)
@@ -34472,7 +34884,7 @@ function evaluateBatch(ops, typePopulations, thresholds = defaultThresholds()) {
34472
34884
  };
34473
34885
  }
34474
34886
  }
34475
- return { tripped: false };
34887
+ return evaluateLoneOverwrite(count, ops.map((op) => op.kind === "overwrite" ? op.priorLen - op.newLen : 0), thresholds.loneMinRemoved);
34476
34888
  }
34477
34889
  function dedupeOps(ops) {
34478
34890
  const byKey = new Map;
@@ -34532,135 +34944,10 @@ class DestructiveGate {
34532
34944
  }
34533
34945
 
34534
34946
  // src/lib/resolve-pending.ts
34535
- import { readFileSync as readFileSync20 } from "node:fs";
34536
-
34537
- // src/lib/conflicts.ts
34538
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, readdirSync as readdirSync6, statSync as statSync6, writeFileSync as writeFileSync7 } from "node:fs";
34539
- import { dirname as dirname9, join as join12 } from "node:path";
34540
- function conflictsRoot(workspaceSlug) {
34541
- return join12(plinthHome(), "_conflicts", workspaceSlug);
34542
- }
34543
- function conflictFilePath(args) {
34544
- const { workspaceSlug, entityType, entitySlug, timestamp, side } = args;
34545
- const iso = timestamp.toISOString().replace(/[:.]/g, "-");
34546
- return join12(conflictsRoot(workspaceSlug), `${entityType}.${entitySlug}.${iso}.${side}.md`);
34547
- }
34548
- function stashConflict(args) {
34549
- const target = conflictFilePath(args);
34550
- mkdirSync10(dirname9(target), { recursive: true, mode: 448 });
34551
- writeFileSync7(target, args.contents, { mode: 384 });
34552
- return target;
34553
- }
34554
- function stashEntityConflictOnce(args) {
34555
- const dir = conflictsRoot(args.workspaceSlug);
34556
- const head = `${args.entityType}.${args.entitySlug}.`;
34557
- const tail = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${args.side}\\.md$`);
34558
- let names = [];
34559
- try {
34560
- names = readdirSync6(dir);
34561
- } catch {
34562
- names = [];
34563
- }
34564
- const newest = names.filter((name) => name.startsWith(head) && tail.test(name.slice(head.length))).sort().pop();
34565
- if (newest !== undefined) {
34566
- const path5 = join12(dir, newest);
34567
- try {
34568
- if (readFileSync15(path5, "utf8") === args.contents)
34569
- return { path: path5, reused: true };
34570
- } catch {}
34571
- }
34572
- return { path: stashConflict(args), reused: false };
34573
- }
34574
- function fileConflictPath(args) {
34575
- const { workspaceSlug, relPath, timestamp, side } = args;
34576
- const iso = timestamp.toISOString().replace(/[:.]/g, "-");
34577
- return join12(conflictsRoot(workspaceSlug), `${fileConflictStem(relPath)}.${iso}.${side}`);
34578
- }
34579
- function fileConflictStem(relPath) {
34580
- return `files.${relPath.replace(/[/\\]/g, "__")}`;
34581
- }
34582
- function stashFileConflict(args) {
34583
- const target = fileConflictPath(args);
34584
- mkdirSync10(dirname9(target), { recursive: true, mode: 448 });
34585
- writeFileSync7(target, args.bytes, { mode: 384 });
34586
- return target;
34587
- }
34588
- function stashFileConflictOnce(args) {
34589
- const dir = conflictsRoot(args.workspaceSlug);
34590
- const head = `${fileConflictStem(args.relPath)}.`;
34591
- const tail = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${args.side}$`);
34592
- let names = [];
34593
- try {
34594
- names = readdirSync6(dir);
34595
- } catch {
34596
- names = [];
34597
- }
34598
- const newest = names.filter((n) => n.startsWith(head) && tail.test(n.slice(head.length))).sort().pop();
34599
- if (newest !== undefined) {
34600
- const path5 = join12(dir, newest);
34601
- try {
34602
- if (Buffer.compare(args.bytes, readFileSync15(path5)) === 0)
34603
- return { path: path5, reused: true };
34604
- } catch {}
34605
- }
34606
- return { path: stashFileConflict(args), reused: false };
34607
- }
34608
- var STAMP_RE2 = /\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)(?:\.(local|cloud))?(?:\.md)?$/;
34609
- function parseStamp(stamp) {
34610
- const iso = stamp.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
34611
- const d = new Date(iso);
34612
- return Number.isNaN(d.getTime()) ? null : d;
34613
- }
34614
- function labelFor(file2) {
34615
- const m = STAMP_RE2.exec(file2);
34616
- if (!m)
34617
- return file2;
34618
- const head = file2.slice(0, m.index);
34619
- if (head.startsWith("files.")) {
34620
- return `files/${head.slice("files.".length).replace(/__/g, "/")}`;
34621
- }
34622
- const dot = head.indexOf(".");
34623
- if (dot === -1)
34624
- return head;
34625
- return `${head.slice(0, dot)}/${head.slice(dot + 1)}`;
34626
- }
34627
- function listConflictStashes(workspaceSlug) {
34628
- const dir = conflictsRoot(workspaceSlug);
34629
- let names;
34630
- try {
34631
- names = readdirSync6(dir);
34632
- } catch {
34633
- return [];
34634
- }
34635
- const out = [];
34636
- for (const file2 of names) {
34637
- const absPath = join12(dir, file2);
34638
- let st;
34639
- try {
34640
- st = statSync6(absPath);
34641
- } catch {
34642
- continue;
34643
- }
34644
- if (!st.isFile())
34645
- continue;
34646
- if (file2.startsWith("."))
34647
- continue;
34648
- const m = STAMP_RE2.exec(file2);
34649
- const side = m?.[2];
34650
- out.push({
34651
- file: file2,
34652
- absPath,
34653
- label: labelFor(file2),
34654
- stashedAt: m ? parseStamp(m[1]) : null,
34655
- side: side === "local" || side === "cloud" ? side : null,
34656
- mtimeMs: st.mtimeMs
34657
- });
34658
- }
34659
- return out.sort((a, b) => (b.stashedAt?.getTime() ?? b.mtimeMs) - (a.stashedAt?.getTime() ?? a.mtimeMs));
34660
- }
34947
+ import { readFileSync as readFileSync21 } from "node:fs";
34661
34948
 
34662
34949
  // src/lib/files/push.ts
34663
- import { readFileSync as readFileSync17, statSync as statSync7 } from "node:fs";
34950
+ import { readFileSync as readFileSync18, statSync as statSync7 } from "node:fs";
34664
34951
 
34665
34952
  // src/lib/permanent-deletions.ts
34666
34953
  import { createHash as createHash3, randomUUID } from "node:crypto";
@@ -34674,7 +34961,7 @@ import {
34674
34961
  lstatSync as lstatSync4,
34675
34962
  mkdirSync as mkdirSync11,
34676
34963
  openSync as openSync4,
34677
- readFileSync as readFileSync16,
34964
+ readFileSync as readFileSync17,
34678
34965
  renameSync as renameSync8,
34679
34966
  unlinkSync as unlinkSync8,
34680
34967
  writeSync as writeSync2
@@ -34865,7 +35152,7 @@ function loadPermanentDeletionJournal(workspaceSlug) {
34865
35152
  return emptyJournal(workspaceSlug);
34866
35153
  assertPrivateDirectory(dirname10(target));
34867
35154
  assertOwnerOnlyFile(target);
34868
- const journal = parseJournal(workspaceSlug, readFileSync16(target, "utf8"));
35155
+ const journal = parseJournal(workspaceSlug, readFileSync17(target, "utf8"));
34869
35156
  try {
34870
35157
  fsyncDirectory(dirname10(target));
34871
35158
  } catch (error52) {
@@ -34932,7 +35219,7 @@ function writePermanentDeletionJournal(prior, next) {
34932
35219
  let installed2 = false;
34933
35220
  try {
34934
35221
  assertOwnerOnlyFile(target);
34935
- const candidate = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
35222
+ const candidate = parseJournal(body.workspace_slug, readFileSync17(target, "utf8"));
34936
35223
  installed2 = candidate.revision === body.revision && checksum(candidate) === checksum(body);
34937
35224
  } catch {
34938
35225
  installed2 = false;
@@ -34946,7 +35233,7 @@ function writePermanentDeletionJournal(prior, next) {
34946
35233
  }
34947
35234
  }
34948
35235
  assertOwnerOnlyFile(target);
34949
- const installed = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
35236
+ const installed = parseJournal(body.workspace_slug, readFileSync17(target, "utf8"));
34950
35237
  if (installed.revision !== body.revision || checksum(installed) !== checksum(body)) {
34951
35238
  throw new PermanentDeletionJournalError("renamed journal revision did not validate", "durability_indeterminate");
34952
35239
  }
@@ -34954,7 +35241,7 @@ function writePermanentDeletionJournal(prior, next) {
34954
35241
  fsyncDirectory(parent);
34955
35242
  } catch (firstError) {
34956
35243
  assertOwnerOnlyFile(target);
34957
- const reopened = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
35244
+ const reopened = parseJournal(body.workspace_slug, readFileSync17(target, "utf8"));
34958
35245
  if (reopened.revision !== body.revision || checksum(reopened) !== checksum(body)) {
34959
35246
  throw new PermanentDeletionJournalError("journal changed after rename", "durability_indeterminate");
34960
35247
  }
@@ -35453,7 +35740,7 @@ async function pushFilesUpsertLocked(args) {
35453
35740
  state: next
35454
35741
  };
35455
35742
  }
35456
- bytes = readFileSync17(absolutePath);
35743
+ bytes = readFileSync18(absolutePath);
35457
35744
  hash2 = sha256Bytes(bytes);
35458
35745
  inode = st.ino;
35459
35746
  }
@@ -35715,12 +36002,64 @@ async function pushFilesDelete(args) {
35715
36002
  }
35716
36003
 
35717
36004
  // src/lib/push.ts
35718
- var import_gray_matter10 = __toESM(require_gray_matter(), 1);
35719
- import { existsSync as existsSync5, readFileSync as readFileSync19, statSync as statSync8 } from "node:fs";
36005
+ import { existsSync as existsSync5, readFileSync as readFileSync20, statSync as statSync8 } from "node:fs";
35720
36006
  import { basename as basename5, relative as relative5 } from "node:path";
35721
36007
 
36008
+ // src/lib/frontmatter-held.ts
36009
+ import { lstatSync as lstatSync5, readFileSync as readFileSync19 } from "node:fs";
36010
+ import { resolve as resolve4, sep as sep5 } from "node:path";
36011
+ function holdUnreadableFrontmatter(args) {
36012
+ const next = recordEntityFrontmatterUnreadable(args);
36013
+ args.output?.log(entityPushHeldLine({
36014
+ entityType: args.entityType,
36015
+ slug: args.slug,
36016
+ reason: FRONTMATTER_UNREADABLE_REASON
36017
+ }));
36018
+ if (next !== args.state) {
36019
+ args.output?.warn(`${args.entityType}/${args.slug}: ${frontmatterUnreadableSentence(args.why)}`);
36020
+ }
36021
+ return next;
36022
+ }
36023
+ function settleFrontmatterHolds(state, workspaceSlug) {
36024
+ const held = listRejected(state).filter(isFrontmatterHold);
36025
+ if (held.length === 0)
36026
+ return state;
36027
+ let root;
36028
+ try {
36029
+ root = workspaceMirrorPath(workspaceSlug);
36030
+ } catch {
36031
+ return state;
36032
+ }
36033
+ const settled = new Set(held.map((record2) => record2.path).filter((path5) => !stillHeld(workspaceSlug, root, path5)));
36034
+ if (settled.size === 0)
36035
+ return state;
36036
+ return commitState(state, {
36037
+ ...state,
36038
+ rejected: listRejected(state).filter((o) => !(isFrontmatterHold(o) && settled.has(o.path)))
36039
+ });
36040
+ }
36041
+ function isFrontmatterHold(record2) {
36042
+ return record2.target === "entity" && record2.reason === FRONTMATTER_UNREADABLE_REASON;
36043
+ }
36044
+ function stillHeld(workspaceSlug, root, recordPath) {
36045
+ const absolutePath = resolve4(root, recordPath);
36046
+ if (!absolutePath.startsWith(root + sep5))
36047
+ return true;
36048
+ if (hasSymlinkedComponent(workspaceSlug, absolutePath))
36049
+ return true;
36050
+ let raw;
36051
+ try {
36052
+ if (!lstatSync5(absolutePath).isFile())
36053
+ return true;
36054
+ raw = readFileSync19(absolutePath, "utf8");
36055
+ } catch (err) {
36056
+ const code = err.code;
36057
+ return code !== "ENOENT" && code !== "ENOTDIR";
36058
+ }
36059
+ return frontmatterParseError(raw) !== null;
36060
+ }
36061
+
35722
36062
  // src/lib/frontmatter-id.ts
35723
- var import_gray_matter8 = __toESM(require_gray_matter(), 1);
35724
36063
  function eolOf(content) {
35725
36064
  return /\r\n/.test(content) ? `\r
35726
36065
  ` : `
@@ -35757,158 +36096,16 @@ function withFrontmatterId(raw, parsed, id) {
35757
36096
  }
35758
36097
  function frontmatterIdWriteIsSound(next, id, expectedBody) {
35759
36098
  try {
35760
- const check2 = import_gray_matter8.default(next);
36099
+ const check2 = matter(next);
35761
36100
  return check2.data?.["id"] === id && check2.content === expectedBody;
35762
36101
  } catch {
35763
36102
  return false;
35764
36103
  }
35765
36104
  }
35766
36105
 
35767
- // src/lib/pull-write.ts
35768
- var import_gray_matter9 = __toESM(require_gray_matter(), 1);
35769
- import { readFileSync as readFileSync18 } from "node:fs";
35770
- function computeMirrorHashes(strategy, content) {
35771
- const parsed = import_gray_matter9.default(content);
35772
- const fm = parsed.data ?? {};
35773
- const lifted = splitUserFrontmatter(strategy, fm);
35774
- const userFm = userFrontmatter(strategy, lifted.frontmatter);
35775
- const hashBody = hashableBody(strategy, parsed.content);
35776
- return {
35777
- bodyHash: stableHash(hashBody),
35778
- frontmatterHash: stableHash({ ...userFm, ...lifted.topLevel }),
35779
- bodyLen: hashBody.length,
35780
- frontmatterId: fm["id"]
35781
- };
35782
- }
35783
- function hashableBody(strategy, body) {
35784
- return strategy.stripLocalBlocks?.(body) ?? body;
35785
- }
35786
- function relocateForSlugChange(args) {
35787
- const { workspaceSlug, strategy, fromSlug, toSlug, acked, echo, now } = args;
35788
- if (fromSlug === toSlug)
35789
- return { kind: "not_needed", writeSlug: toSlug };
35790
- if (!echo) {
35791
- return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "unguarded" };
35792
- }
35793
- if (strategy.mirrorRelativePath(fromSlug).split("/").length !== 2) {
35794
- return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "foldered" };
35795
- }
35796
- let fromPath;
35797
- try {
35798
- fromPath = entityFilePath(workspaceSlug, strategy.type, fromSlug);
35799
- entityFilePath(workspaceSlug, strategy.type, toSlug);
35800
- } catch (err) {
35801
- if (err instanceof MirrorPathError) {
35802
- return {
35803
- kind: "skipped",
35804
- writeSlug: toSlug,
35805
- fromSlug,
35806
- reason: "unresolvable"
35807
- };
35808
- }
35809
- throw err;
35810
- }
35811
- let raw = null;
35812
- try {
35813
- raw = readFileSync18(fromPath, "utf8");
35814
- } catch (err) {
35815
- const code = err?.code;
35816
- if (code === "ENOENT") {
35817
- return { kind: "absent", writeSlug: toSlug, fromSlug };
35818
- }
35819
- return {
35820
- kind: "refused",
35821
- writeSlug: fromSlug,
35822
- fromSlug,
35823
- reason: "source_unreadable"
35824
- };
35825
- }
35826
- let diverged = true;
35827
- try {
35828
- const hashes = computeMirrorHashes(strategy, raw);
35829
- diverged = hashes.bodyHash !== acked.bodyHash || hashes.frontmatterHash !== acked.frontmatterHash;
35830
- } catch {
35831
- diverged = true;
35832
- }
35833
- const stashPath = diverged ? stashConflict({
35834
- workspaceSlug,
35835
- entityType: strategy.type,
35836
- entitySlug: fromSlug,
35837
- timestamp: now(),
35838
- contents: raw,
35839
- side: "local"
35840
- }) : undefined;
35841
- try {
35842
- let registered = false;
35843
- const result = moveEntityFile(workspaceSlug, strategy.type, fromSlug, toSlug, () => {
35844
- if (echo.watcher === "live") {
35845
- echo.suppressor.registerDelete(fromPath);
35846
- registered = true;
35847
- }
35848
- });
35849
- if (result === "moved") {
35850
- return { kind: "moved", writeSlug: toSlug, fromSlug, stashPath };
35851
- }
35852
- if (registered && echo.watcher === "live") {
35853
- echo.suppressor.cancelDelete(fromPath);
35854
- }
35855
- return { kind: "absent", writeSlug: toSlug, fromSlug };
35856
- } catch (err) {
35857
- if (err instanceof MirrorDestinationOccupiedError) {
35858
- return {
35859
- kind: "refused",
35860
- writeSlug: fromSlug,
35861
- fromSlug,
35862
- reason: "destination_occupied",
35863
- stashPath
35864
- };
35865
- }
35866
- if (err instanceof MirrorPathError) {
35867
- return {
35868
- kind: "skipped",
35869
- writeSlug: toSlug,
35870
- fromSlug,
35871
- reason: "unresolvable"
35872
- };
35873
- }
35874
- throw err;
35875
- }
35876
- }
35877
- function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor, pathSlug) {
35878
- const targetSlug = pathSlug ?? doc2.slug;
35879
- const content = renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug);
35880
- if (suppressor) {
35881
- const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
35882
- suppressor.registerWrite(target, computeMirrorHashes(strategy, content));
35883
- }
35884
- writeMirrorFile(workspaceSlug, strategy.type, targetSlug, content);
35885
- return content;
35886
- }
35887
- function renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug) {
35888
- let content = strategy.serialise(doc2);
35889
- if (strategy.preserveLocalBodyOnPull) {
35890
- const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
35891
- let existing = null;
35892
- try {
35893
- existing = readFileSync18(target, "utf8");
35894
- } catch (err) {
35895
- if (err?.code !== "ENOENT")
35896
- throw err;
35897
- }
35898
- if (existing !== null) {
35899
- const existingParsed = import_gray_matter9.default(existing);
35900
- const cloudParsed = import_gray_matter9.default(content);
35901
- const stitched = import_gray_matter9.default.stringify(existingParsed.content, cloudParsed.data);
35902
- content = stitched.endsWith(`
35903
- `) ? stitched : stitched + `
35904
- `;
35905
- }
35906
- }
35907
- return content;
35908
- }
35909
-
35910
36106
  // src/lib/push.ts
35911
36107
  var DELETE_DECLINED_PATH_EXISTS = "the file is on disk again — a newer write owns this row";
36108
+ var FRONTMATTER_PARSE_ERROR = "frontmatter parse error";
35912
36109
  function deleteDeclinedSentence(relPath) {
35913
36110
  return `Not deleted: ${relPath} is on disk again, so the row was left alone. If you meant to delete it, remove the file and it will be retried.`;
35914
36111
  }
@@ -36017,8 +36214,8 @@ function stampStateFromMirror(args) {
36017
36214
  const { strategy, workspaceSlug, doc: doc2, state, priorMemoryHash } = args;
36018
36215
  const targetSlug = args.pathSlug ?? doc2.slug;
36019
36216
  const mirrorPath = entityFilePath(workspaceSlug, strategy.type, targetSlug);
36020
- const mirrorRaw = readFileSync19(mirrorPath, "utf8");
36021
- const mirrorParsed = import_gray_matter10.default(mirrorRaw);
36217
+ const mirrorRaw = readFileSync20(mirrorPath, "utf8");
36218
+ const mirrorParsed = matter(mirrorRaw);
36022
36219
  const mirrorFm = mirrorParsed.data ?? {};
36023
36220
  const mirrorLifted = splitUserFrontmatter(strategy, mirrorFm);
36024
36221
  const mirrorUserFm = userFrontmatter(strategy, mirrorLifted.frontmatter);
@@ -36170,7 +36367,7 @@ async function pushFile(args) {
36170
36367
  }
36171
36368
  let raw;
36172
36369
  try {
36173
- raw = readFileSync19(absolutePath, "utf8");
36370
+ raw = readFileSync20(absolutePath, "utf8");
36174
36371
  } catch (err) {
36175
36372
  if (err?.code === "ENOENT") {
36176
36373
  return runPushDelete({
@@ -36190,23 +36387,32 @@ async function pushFile(args) {
36190
36387
  }
36191
36388
  throw err;
36192
36389
  }
36193
- let parsed;
36194
- try {
36195
- parsed = import_gray_matter10.default(raw);
36196
- } catch {
36197
- return {
36198
- outcome: { kind: "ignored", absolutePath, reason: "frontmatter parse error" },
36199
- state
36200
- };
36201
- }
36202
- const fm = parsed.data ?? {};
36203
- const slugFromFm = typeof fm.slug === "string" && fm.slug.length > 0 ? fm.slug : null;
36204
36390
  let parsedSlug = null;
36205
36391
  try {
36206
36392
  const rel = relative5(workspaceMirrorPath(workspaceSlug), absolutePath);
36207
36393
  parsedSlug = strategy.parseMirrorPath(rel)?.slug ?? null;
36208
36394
  } catch {}
36209
36395
  const slugFromPath = parsedSlug ?? basename5(absolutePath, ".md");
36396
+ const read = readFrontmatter(raw);
36397
+ if (!read.ok) {
36398
+ return {
36399
+ outcome: { kind: "ignored", absolutePath, reason: FRONTMATTER_PARSE_ERROR },
36400
+ state: holdUnreadableFrontmatter({
36401
+ state,
36402
+ workspaceSlug,
36403
+ absolutePath,
36404
+ entityType,
36405
+ slug: slugFromPath,
36406
+ why: read.why,
36407
+ now,
36408
+ output
36409
+ }),
36410
+ base: state
36411
+ };
36412
+ }
36413
+ let parsed = read.parsed;
36414
+ const fm = parsed.data ?? {};
36415
+ const slugFromFm = typeof fm.slug === "string" && fm.slug.length > 0 ? fm.slug : null;
36210
36416
  const slug = identity === "path" ? slugFromPath : slugFromFm ?? slugFromPath;
36211
36417
  if (!slug) {
36212
36418
  return {
@@ -36220,14 +36426,14 @@ async function pushFile(args) {
36220
36426
  if (strategy.classifyFieldErrors) {
36221
36427
  const desiredBody = spliceSyncErrorBlock(parsed.content, fieldErrors);
36222
36428
  if (desiredBody.replace(/\s+$/, "") !== parsed.content.replace(/\s+$/, "")) {
36223
- let rewritten = import_gray_matter10.default.stringify(desiredBody, parsed.data ?? {});
36429
+ let rewritten = stringifyFrontmatter(desiredBody, parsed.data ?? {});
36224
36430
  if (!rewritten.endsWith(`
36225
36431
  `))
36226
36432
  rewritten += `
36227
36433
  `;
36228
36434
  writeMirrorFile(workspaceSlug, entityType, slug, rewritten);
36229
36435
  raw = rewritten;
36230
- parsed = import_gray_matter10.default(rewritten);
36436
+ parsed = parseUncached(rewritten);
36231
36437
  }
36232
36438
  }
36233
36439
  const liveInode = safeInode2(absolutePath);
@@ -36505,7 +36711,7 @@ async function runPushUpsert(args) {
36505
36711
  }
36506
36712
  const wireProjection = (md) => {
36507
36713
  try {
36508
- const p = import_gray_matter10.default(md);
36714
+ const p = matter(md);
36509
36715
  const f = p.data ?? {};
36510
36716
  const l = splitUserFrontmatter(strategy, f);
36511
36717
  const u = userFrontmatter(strategy, l.frontmatter);
@@ -36520,7 +36726,7 @@ async function runPushUpsert(args) {
36520
36726
  if (liveEntry !== snapshotEntry) {
36521
36727
  let onDisk = null;
36522
36728
  try {
36523
- onDisk = readFileSync19(args.absolutePath, "utf8");
36729
+ onDisk = readFileSync20(args.absolutePath, "utf8");
36524
36730
  } catch {
36525
36731
  onDisk = null;
36526
36732
  }
@@ -36982,7 +37188,7 @@ function writeCreatedIdBack(args) {
36982
37188
  if (entityFilePath(args.workspaceSlug, args.strategy.type, args.slug) !== args.absolutePath) {
36983
37189
  return;
36984
37190
  }
36985
- const readBack = args.readBack ?? ((p) => readFileSync19(p, "utf8"));
37191
+ const readBack = args.readBack ?? ((p) => readFileSync20(p, "utf8"));
36986
37192
  if (readBack(args.absolutePath) !== args.raw)
36987
37193
  return;
36988
37194
  args.suppressor?.registerWrite(args.absolutePath, computeMirrorHashes(args.strategy, write.content));
@@ -37454,8 +37660,8 @@ async function pushRename(args) {
37454
37660
  };
37455
37661
  if (!updated.body_hash || !updated.frontmatter_hash) {
37456
37662
  try {
37457
- const mirrorRaw = readFileSync19(toPath, "utf8");
37458
- const mirrorParsed = import_gray_matter10.default(mirrorRaw);
37663
+ const mirrorRaw = readFileSync20(toPath, "utf8");
37664
+ const mirrorParsed = matter(mirrorRaw);
37459
37665
  const renameFm = mirrorParsed.data ?? {};
37460
37666
  const renameLifted = splitUserFrontmatter(strategy, renameFm);
37461
37667
  const renameUserFm = userFrontmatter(strategy, renameLifted.frontmatter);
@@ -37491,7 +37697,7 @@ async function pushRename(args) {
37491
37697
  if (patchResult.reason === "slug_conflict") {
37492
37698
  let contents2 = "";
37493
37699
  try {
37494
- contents2 = readFileSync19(toPath, "utf8");
37700
+ contents2 = readFileSync20(toPath, "utf8");
37495
37701
  } catch {}
37496
37702
  const stashPath2 = stashConflict({
37497
37703
  workspaceSlug,
@@ -37539,7 +37745,7 @@ async function pushRename(args) {
37539
37745
  }
37540
37746
  let contents = "";
37541
37747
  try {
37542
- contents = readFileSync19(toPath, "utf8");
37748
+ contents = readFileSync20(toPath, "utf8");
37543
37749
  } catch {}
37544
37750
  const stashPath = stashConflict({
37545
37751
  workspaceSlug,
@@ -37672,7 +37878,7 @@ async function discardBatchOps(batch, deps) {
37672
37878
  const path5 = opPath(deps.workspaceSlug, op);
37673
37879
  let local = null;
37674
37880
  try {
37675
- local = readFileSync20(path5, "utf8");
37881
+ local = readFileSync21(path5, "utf8");
37676
37882
  } catch {
37677
37883
  local = null;
37678
37884
  }
@@ -37711,7 +37917,7 @@ async function discardFileOp(op, deps) {
37711
37917
  const abs = filesSubtreePath(deps.workspaceSlug, op.path);
37712
37918
  let local = null;
37713
37919
  try {
37714
- local = readFileSync20(abs);
37920
+ local = readFileSync21(abs);
37715
37921
  } catch {
37716
37922
  local = null;
37717
37923
  }
@@ -37748,7 +37954,7 @@ function opPath(workspaceSlug, op) {
37748
37954
  }
37749
37955
 
37750
37956
  // src/lib/files/classify.ts
37751
- import { relative as relative6, sep as sep5 } from "node:path";
37957
+ import { relative as relative6, sep as sep6 } from "node:path";
37752
37958
  var FILES_SUBTREE = "files";
37753
37959
  function classifyFilesPath(args) {
37754
37960
  let root;
@@ -37758,20 +37964,20 @@ function classifyFilesPath(args) {
37758
37964
  return null;
37759
37965
  }
37760
37966
  const rel = relative6(root, args.absolutePath);
37761
- if (rel.startsWith("..") || rel.startsWith(sep5))
37967
+ if (rel.startsWith("..") || rel.startsWith(sep6))
37762
37968
  return null;
37763
- const prefix = FILES_SUBTREE + sep5;
37969
+ const prefix = FILES_SUBTREE + sep6;
37764
37970
  if (!rel.startsWith(prefix))
37765
37971
  return null;
37766
37972
  const sub = rel.slice(prefix.length);
37767
37973
  if (sub.length === 0)
37768
37974
  return null;
37769
- const posix = sep5 === "/" ? sub : sub.split(sep5).join("/");
37975
+ const posix = sep6 === "/" ? sub : sub.split(sep6).join("/");
37770
37976
  return validateFilesRelPath(posix) ? { relPath: posix } : null;
37771
37977
  }
37772
37978
 
37773
37979
  // src/lib/memory-facts/sync.ts
37774
- import { isAbsolute as isAbsolute2, relative as relative7, sep as sep6 } from "node:path";
37980
+ import { isAbsolute as isAbsolute2, relative as relative7, sep as sep7 } from "node:path";
37775
37981
 
37776
37982
  // src/lib/memory-facts/api.ts
37777
37983
  var FactCommonSchema = exports_external.object({
@@ -38054,7 +38260,7 @@ function unknownBecause(reason) {
38054
38260
  case "no_watermark":
38055
38261
  return "The last download carried no freshness marker, so there is nothing to compare against.";
38056
38262
  case "incomplete_apply":
38057
- return "The last download did not fully land, so this copy is not the one the freshness marker describes.";
38263
+ return "The last download either did not fully land or carried a correction trail this copy could not use, so this copy is not one the freshness marker can vouch for.";
38058
38264
  case "check_failed":
38059
38265
  return "The freshness check did not complete.";
38060
38266
  default:
@@ -38211,6 +38417,9 @@ function resolveTrail(row, slugById) {
38211
38417
  consistent: true
38212
38418
  };
38213
38419
  }
38420
+ if (outcome === "current" && successor === row.id) {
38421
+ return { trail: "current", consistent: true };
38422
+ }
38214
38423
  if (successor !== null)
38215
38424
  return { trail: UNKNOWN, consistent: false };
38216
38425
  return { trail: outcome, consistent: true };
@@ -38512,7 +38721,7 @@ function classifyMemoryFactPath(args) {
38512
38721
  const rel = relative7(root, args.absolutePath);
38513
38722
  if (rel === "" || rel.startsWith("..") || isAbsolute2(rel))
38514
38723
  return null;
38515
- const mirrorRel = `${MEMORY_FACTS_DIR}/${rel.split(sep6).join("/")}`;
38724
+ const mirrorRel = `${MEMORY_FACTS_DIR}/${rel.split(sep7).join("/")}`;
38516
38725
  return { relPath: mirrorRel, fact: parseMemoryFactRelPath(mirrorRel) };
38517
38726
  }
38518
38727
  function classifyMemoryFactEvent(args) {
@@ -38609,7 +38818,7 @@ function evaluateFileBatch(ops, population, thresholds = defaultThresholds()) {
38609
38818
  }
38610
38819
  };
38611
38820
  }
38612
- return { tripped: false };
38821
+ return evaluateLoneOverwrite(count, ops.map((op) => op.kind === "overwrite" ? op.priorSize - op.newSize : 0), thresholds.loneMinRemovedBytes);
38613
38822
  }
38614
38823
  function dedupeFileOps(ops) {
38615
38824
  const byKey = new Map;
@@ -38680,7 +38889,7 @@ class FileDestructiveGate {
38680
38889
  }
38681
38890
 
38682
38891
  // src/lib/files/reconcile.ts
38683
- import { readFileSync as readFileSync21, statSync as statSync10 } from "node:fs";
38892
+ import { readFileSync as readFileSync22, statSync as statSync10 } from "node:fs";
38684
38893
 
38685
38894
  // src/lib/unsynced.ts
38686
38895
  import { join as join14, relative as relative8 } from "node:path";
@@ -39676,7 +39885,7 @@ class NodeFsHandler {
39676
39885
  this._addToNodeFs(path5, initialAdd, wh, depth + 1);
39677
39886
  }
39678
39887
  }).on(EV.ERROR, this._boundHandleError);
39679
- return new Promise((resolve5, reject) => {
39888
+ return new Promise((resolve6, reject) => {
39680
39889
  if (!stream)
39681
39890
  return reject();
39682
39891
  stream.once(STR_END, () => {
@@ -39685,7 +39894,7 @@ class NodeFsHandler {
39685
39894
  return;
39686
39895
  }
39687
39896
  const wasThrottled = throttler ? throttler.clear() : false;
39688
- resolve5(undefined);
39897
+ resolve6(undefined);
39689
39898
  previous.getChildren().filter((item) => {
39690
39899
  return item !== directory && !current.has(item);
39691
39900
  }).forEach((item) => {
@@ -40444,7 +40653,7 @@ var chokidar_default = { watch, FSWatcher };
40444
40653
 
40445
40654
  // src/lib/watcher.ts
40446
40655
  import { statSync as statSync9 } from "node:fs";
40447
- import { sep as sep7 } from "node:path";
40656
+ import { sep as sep8 } from "node:path";
40448
40657
 
40449
40658
  // src/lib/rename-classifier.ts
40450
40659
  class RenameClassifier {
@@ -40575,7 +40784,7 @@ function watcherIgnoreMatcher(workspaceRoot) {
40575
40784
  return (absolutePath) => patterns.some((re) => re.test(absolutePath));
40576
40785
  }
40577
40786
  function escSep() {
40578
- return sep7 === "/" ? "/" : "\\\\";
40787
+ return sep8 === "/" ? "/" : "\\\\";
40579
40788
  }
40580
40789
 
40581
40790
  class DebounceMap {
@@ -40675,7 +40884,12 @@ function startWatcher(opts, handler) {
40675
40884
  });
40676
40885
  watcher.on("add", (p) => classifier.onAdd(p));
40677
40886
  watcher.on("change", (p) => classifier.onChange(p));
40678
- watcher.on("unlink", (p) => classifier.onUnlink(p));
40887
+ watcher.on("unlink", (p) => {
40888
+ try {
40889
+ opts.onUnlink?.(p);
40890
+ } catch {}
40891
+ classifier.onUnlink(p);
40892
+ });
40679
40893
  return {
40680
40894
  async stop() {
40681
40895
  classifier.shutdown();
@@ -40714,7 +40928,7 @@ function readBlobForPush(args) {
40714
40928
  }
40715
40929
  let bytes;
40716
40930
  try {
40717
- bytes = readFileSync21(absolutePath);
40931
+ bytes = readFileSync22(absolutePath);
40718
40932
  } catch (err) {
40719
40933
  if (err?.code === "ENOENT")
40720
40934
  return { kind: "gone" };
@@ -40857,7 +41071,7 @@ async function reconcileFilesSubtree(args) {
40857
41071
  ops: pendingOps
40858
41072
  });
40859
41073
  quarantined = ops.length;
40860
- output?.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${ops.length} reason=${result.reason.kind} source=files-reconcile]`);
41074
+ output?.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${ops.length} reason=${tripReasonToken(result.reason)} source=files-reconcile]`);
40861
41075
  } else {
40862
41076
  for (const c of guttingCandidates) {
40863
41077
  await pushDivergedBlob(c.op.relPath, c.op.absolutePath, c.prepared);
@@ -41024,7 +41238,7 @@ function applyArtistMemoryOnPull(args) {
41024
41238
  const cloudMemory = live.memory;
41025
41239
  const generated = live.memory_generated === true;
41026
41240
  const local = readArtistMemoryFile(workspaceSlug, slug);
41027
- const refuse = (localContent, canonical2) => {
41241
+ const refuse2 = (localContent, canonical2) => {
41028
41242
  const refused = refuseGeneratedMemoryEdit({
41029
41243
  state,
41030
41244
  workspaceSlug,
@@ -41053,7 +41267,7 @@ function applyArtistMemoryOnPull(args) {
41053
41267
  const cloudHash = stableHash(cloudMemory);
41054
41268
  if (cloudHash === ackedMemoryHash && local !== null) {
41055
41269
  if (generated && stableHash(local) !== ackedMemoryHash) {
41056
- refuse(local, cloudMemory);
41270
+ refuse2(local, cloudMemory);
41057
41271
  return { memoryHash: cloudHash, state };
41058
41272
  }
41059
41273
  if (generated) {
@@ -41065,7 +41279,7 @@ function applyArtistMemoryOnPull(args) {
41065
41279
  const localHash = stableHash(local);
41066
41280
  if (localHash !== ackedMemoryHash && localHash !== cloudHash) {
41067
41281
  if (generated) {
41068
- refuse(local, cloudMemory);
41282
+ refuse2(local, cloudMemory);
41069
41283
  return { memoryHash: cloudHash, state };
41070
41284
  }
41071
41285
  const stashPath = stashConflict({
@@ -41775,7 +41989,7 @@ function anyUnknown(state) {
41775
41989
  }
41776
41990
 
41777
41991
  // src/lib/memory/write.ts
41778
- import { readFileSync as readFileSync22, statSync as statSync11 } from "node:fs";
41992
+ import { readFileSync as readFileSync23, statSync as statSync11 } from "node:fs";
41779
41993
 
41780
41994
  // src/lib/files/extracted-text.ts
41781
41995
  import { createHash as createHash5 } from "node:crypto";
@@ -41856,7 +42070,6 @@ function extractedTextHash(rendered) {
41856
42070
  }
41857
42071
 
41858
42072
  // src/lib/memory/splice.ts
41859
- var import_gray_matter11 = __toESM(require_gray_matter(), 1);
41860
42073
  var OPEN_RE = /^---\r?\n/;
41861
42074
  function findFrontmatter(content) {
41862
42075
  const open2 = OPEN_RE.exec(content);
@@ -41942,7 +42155,7 @@ function spliceAnnotation(content, lines) {
41942
42155
  }
41943
42156
  function parsesAsFrontmatter(content) {
41944
42157
  try {
41945
- import_gray_matter11.default(content, { cache: false });
42158
+ matter(content, { cache: false });
41946
42159
  return true;
41947
42160
  } catch {
41948
42161
  return false;
@@ -41970,7 +42183,7 @@ function writeDocumentAnnotation(args) {
41970
42183
  }
41971
42184
  let current;
41972
42185
  try {
41973
- current = readFileSync22(target, "utf8");
42186
+ current = readFileSync23(target, "utf8");
41974
42187
  } catch {
41975
42188
  return { kind: "absent" };
41976
42189
  }
@@ -42008,18 +42221,32 @@ function writeFileTextAnnotation(args) {
42008
42221
  const current = readExtractedText(args.workspaceSlug, args.relPath);
42009
42222
  if (current === null)
42010
42223
  return { kind: "absent" };
42011
- if (args.textHash !== undefined && extractedTextHash(current) !== args.textHash) {
42224
+ const onDisk = extractedTextHash(current);
42225
+ const acked = args.textHash;
42226
+ const offAck = acked !== undefined && onDisk !== acked;
42227
+ if (acked !== undefined && offAck && !isOwnRender(current, acked, args.priorLines)) {
42012
42228
  return { kind: "diverged", relPath: extractedTextRelPath(args.relPath) };
42013
42229
  }
42014
42230
  const spliced = spliceAnnotation(current, args.lines);
42015
- if (!spliced.changed)
42016
- return { kind: "unchanged" };
42231
+ if (!spliced.changed) {
42232
+ return offAck ? { kind: "unchanged", textHash: onDisk } : { kind: "unchanged" };
42233
+ }
42017
42234
  writeExtractedText(args.workspaceSlug, args.relPath, spliced.content);
42018
42235
  return { kind: "written", textHash: extractedTextHash(spliced.content) };
42019
42236
  }
42237
+ function isOwnRender(current, ackedHash, priorLines) {
42238
+ if (extractedTextHash(spliceAnnotation(current, []).content) === ackedHash)
42239
+ return true;
42240
+ if (priorLines === undefined)
42241
+ return false;
42242
+ return extractedTextHash(spliceAnnotation(current, priorLines).content) === ackedHash;
42243
+ }
42020
42244
  function divergedNotice(relPath, state) {
42021
42245
  return `${relPath}: its memory context is now "${state}", but the file has local ` + `edits so the marker was not written into it. Your edits are untouched. ` + `Resolve the edit and the next sync writes the marker.`;
42022
42246
  }
42247
+ function generatedDivergedNotice(relPath) {
42248
+ return `${relPath}: this generated file does not match Plinth's record of it, ` + `so its memory marker was not written. Plinth rewrites the file when its ` + `source next changes; changes made to it here are not synced.`;
42249
+ }
42023
42250
  function unreadableNotice(relPath, why) {
42024
42251
  const reason = why.replace(/\s+/g, " ").trim().slice(0, 120);
42025
42252
  return `${relPath}: its memory context could not be written because the file's ` + `frontmatter could not be read (${reason}). The file is untouched. Fix the ` + `frontmatter and the next sync writes the marker.`;
@@ -42049,6 +42276,10 @@ function heldFileTexts(state) {
42049
42276
  textHash: f.text_hash
42050
42277
  }));
42051
42278
  }
42279
+ function priorFileLines(state, id) {
42280
+ return getMemorySourceEntries(state, "workspace_files")[id]?.rendered.split(`
42281
+ `);
42282
+ }
42052
42283
  async function writeChanged(args) {
42053
42284
  const docs = new Map(heldDocuments(args.state).map((d) => [d.id, d.slug]));
42054
42285
  const texts = new Map(heldFileTexts(args.state).map((f) => [f.id, f]));
@@ -42148,7 +42379,8 @@ async function writeChanged(args) {
42148
42379
  workspaceSlug: args.workspaceSlug,
42149
42380
  relPath: file2.relPath,
42150
42381
  textHash: args.freshTextHashes?.get(answer2.id) ?? file2.textHash,
42151
- lines: answer2.lines
42382
+ lines: answer2.lines,
42383
+ priorLines: priorFileLines(args.state, answer2.id)
42152
42384
  });
42153
42385
  if (res.kind !== "diverged" && res.kind !== "absent") {
42154
42386
  args.onWritten?.("workspace_files", answer2.id, res.textHash);
@@ -42175,7 +42407,7 @@ async function writeChanged(args) {
42175
42407
  }
42176
42408
  const textPath = extractedTextRelPath(file2.relPath);
42177
42409
  if (outcome.kind === "diverged") {
42178
- report.push({ path: textPath, record: divergedNotice(textPath, answer2.entry.state) });
42410
+ report.push({ path: textPath, record: generatedDivergedNotice(textPath) });
42179
42411
  } else {
42180
42412
  clear.push(textPath);
42181
42413
  }
@@ -42184,9 +42416,11 @@ async function writeChanged(args) {
42184
42416
  if (outcome.textHash !== undefined) {
42185
42417
  textHashes.set(file2.relPath, { to: outcome.textHash });
42186
42418
  }
42419
+ } else if (outcome.kind === "unchanged" && outcome.textHash !== undefined) {
42420
+ textHashes.set(file2.relPath, { to: outcome.textHash });
42187
42421
  } else if (outcome.kind === "diverged") {
42188
42422
  diverged++;
42189
- notices.push(divergedNotice(outcome.relPath, answer2.entry.state));
42423
+ notices.push(generatedDivergedNotice(outcome.relPath));
42190
42424
  }
42191
42425
  }
42192
42426
  if (args.abortIfStale?.() === true) {
@@ -42799,6 +43033,109 @@ class PullLoop {
42799
43033
  }
42800
43034
  }
42801
43035
 
43036
+ // src/lib/high-water.ts
43037
+ var HIGH_WATER_BASE_READ_MS = 60 * 60 * 1000;
43038
+ function baseReadIntervalMs(env2 = process.env) {
43039
+ const raw = Number(env2.PLINTH_HIGH_WATER_BASE_READ_MS);
43040
+ return Number.isFinite(raw) && raw > 0 ? raw : HIGH_WATER_BASE_READ_MS;
43041
+ }
43042
+ function provenMark(args) {
43043
+ if (!args.clean)
43044
+ return null;
43045
+ return args.finalHighWater ?? args.advancedTo;
43046
+ }
43047
+ function createBaseReadSchedule(bootMs, intervalMs = HIGH_WATER_BASE_READ_MS) {
43048
+ let lastBaseReadMs = bootMs;
43049
+ let restoresWaiting = false;
43050
+ let lastUsedHighWater = false;
43051
+ return {
43052
+ useHighWater(nowMs) {
43053
+ if (restoresWaiting)
43054
+ return lastUsedHighWater;
43055
+ return nowMs - lastBaseReadMs < intervalMs;
43056
+ },
43057
+ completed(nowMs, usedHighWater, ok, restoresDeferred = 0) {
43058
+ if (!ok)
43059
+ return;
43060
+ if (!usedHighWater)
43061
+ lastBaseReadMs = nowMs;
43062
+ restoresWaiting = restoresDeferred > 0;
43063
+ lastUsedHighWater = usedHighWater;
43064
+ }
43065
+ };
43066
+ }
43067
+
43068
+ // src/lib/restore-deferral.ts
43069
+ var RESTORE_DEFERRAL_MS = 5 * 60000;
43070
+ var UNEXPLAINED_GRACE_MS = 5000;
43071
+ function keyOf(absolutePath) {
43072
+ return absolutePath.normalize("NFC");
43073
+ }
43074
+ function sightingKey(key, version2) {
43075
+ return `${key}\x00${version2}`;
43076
+ }
43077
+ function forgetSightings(recent, key) {
43078
+ const prefix = `${key}\x00`;
43079
+ for (const k of recent.keys())
43080
+ if (k.startsWith(prefix))
43081
+ recent.delete(k);
43082
+ }
43083
+ function noteUnlink(recent, absolutePath, nowMs) {
43084
+ forgetExpired(recent, nowMs);
43085
+ const key = keyOf(absolutePath);
43086
+ forgetSightings(recent, key);
43087
+ recent.set(key, { kind: "unlink", at: nowMs, logged: false });
43088
+ }
43089
+ function deferRestore(recent, absolutePath, version2, nowMs) {
43090
+ if (!recent)
43091
+ return { defer: false, first: false };
43092
+ forgetExpired(recent, nowMs);
43093
+ const key = keyOf(absolutePath);
43094
+ const unlinked = recent.get(key);
43095
+ if (unlinked?.kind === "unlink") {
43096
+ if (unlinked.logged)
43097
+ return { defer: true, first: false };
43098
+ recent.set(key, { ...unlinked, logged: true });
43099
+ return { defer: true, first: true };
43100
+ }
43101
+ const sighting = sightingKey(key, version2);
43102
+ const seen = recent.get(sighting);
43103
+ if (seen === undefined) {
43104
+ recent.set(sighting, { kind: "missing", at: nowMs });
43105
+ return { defer: true, first: false };
43106
+ }
43107
+ if (nowMs - seen.at < UNEXPLAINED_GRACE_MS)
43108
+ return { defer: true, first: false };
43109
+ recent.delete(sighting);
43110
+ return { defer: false, first: false };
43111
+ }
43112
+ function presentAgain(recent, absolutePath) {
43113
+ if (recent)
43114
+ forgetSightings(recent, keyOf(absolutePath));
43115
+ }
43116
+ function forgetExpired(recent, nowMs) {
43117
+ for (const [key, seen] of recent) {
43118
+ if (seen.kind === "unlink" && nowMs - seen.at >= RESTORE_DEFERRAL_MS)
43119
+ recent.delete(key);
43120
+ }
43121
+ }
43122
+
43123
+ // src/lib/ambient-poll.ts
43124
+ function createAmbientPollWiring(opts) {
43125
+ const recentUnlinks = new Map;
43126
+ const baseReads = createBaseReadSchedule(opts.now(), opts.baseReadIntervalMs);
43127
+ return {
43128
+ onUnlink: (absolutePath) => noteUnlink(recentUnlinks, absolutePath, opts.now()),
43129
+ async poll(run) {
43130
+ const startMs = opts.now();
43131
+ const useHighWater = baseReads.useHighWater(startMs);
43132
+ const result = await run({ useHighWater, recentUnlinks });
43133
+ baseReads.completed(startMs, useHighWater, result.exitCode === 0, result.restoresDeferred ?? 0);
43134
+ return result;
43135
+ }
43136
+ };
43137
+ }
43138
+
42802
43139
  // src/lib/calendar-loop.ts
42803
43140
  var DEFAULT_CALENDAR_INTERVAL_MS = 900000;
42804
43141
  var CALENDAR_BACKOFF_CAP_MS = 3600000;
@@ -42959,6 +43296,12 @@ var CALENDAR_STATUSES = [
42959
43296
  "token-expired",
42960
43297
  "google-error"
42961
43298
  ];
43299
+ var KNOWN_TRUNCATED_REASONS = ["page_limit", "time_limit"];
43300
+ var TruncatedReasonSchema = exports_external.string().nullish().transform((v) => {
43301
+ if (v === null || v === undefined)
43302
+ return null;
43303
+ return KNOWN_TRUNCATED_REASONS.includes(v) ? v : "other";
43304
+ });
42962
43305
  var CalendarRowSchema = exports_external.object({
42963
43306
  artist_id: exports_external.string().nullable(),
42964
43307
  artist_slug: exports_external.string().nullable(),
@@ -42967,8 +43310,13 @@ var CalendarRowSchema = exports_external.object({
42967
43310
  calendar_name: exports_external.string(),
42968
43311
  status: exports_external.enum(CALENDAR_STATUSES),
42969
43312
  events: exports_external.array(CalendarEventSchema).nullable(),
42970
- error: exports_external.object({ upstream_status: exports_external.number(), message: exports_external.string() }).nullish().transform((v) => v ?? null),
42971
- truncated: exports_external.boolean().optional().default(false)
43313
+ error: exports_external.object({
43314
+ upstream_status: exports_external.number().nullable(),
43315
+ message: exports_external.string()
43316
+ }).nullish().transform((v) => v ?? null),
43317
+ truncated: exports_external.boolean().optional().default(false),
43318
+ truncated_reason: TruncatedReasonSchema,
43319
+ resume_from: exports_external.string().nullish().transform((v) => v ?? null)
42972
43320
  });
42973
43321
  var CalendarResponseSchema = exports_external.object({
42974
43322
  generated_at: exports_external.string().min(1),
@@ -43156,6 +43504,62 @@ function readAsOf(text, today) {
43156
43504
  return { parts, yearInferred: first.yearInferred };
43157
43505
  }
43158
43506
 
43507
+ // src/lib/calendar/limits.ts
43508
+ var CALENDAR_RANGE_TOOL = "list_calendar_events";
43509
+ function spanPhrase(timeMin, timeMax) {
43510
+ const ms = Date.parse(timeMax) - Date.parse(timeMin);
43511
+ if (!Number.isFinite(ms) || ms <= 0)
43512
+ return null;
43513
+ const weeks = ms / (7 * DAY_MS);
43514
+ if (Number.isInteger(weeks))
43515
+ return `${weeks} ${weeks === 1 ? "week" : "weeks"}`;
43516
+ const days = ms / DAY_MS;
43517
+ if (Number.isInteger(days))
43518
+ return `${days} ${days === 1 ? "day" : "days"}`;
43519
+ return null;
43520
+ }
43521
+ function coverageLine(timeMin, timeMax) {
43522
+ const span = spanPhrase(timeMin, timeMax);
43523
+ return `**This file holds only the window above${span ? ` (${span})` : ""}.** ` + `When a question needs a date outside it, past or future, call the Plinth ` + `tool \`${CALENDAR_RANGE_TOOL}\` for just that range.`;
43524
+ }
43525
+ var TRUNCATION_CAUSE = {
43526
+ page_limit: "Google had more entries in this window than one refresh reads.",
43527
+ time_limit: "The read from Google ran out of time.",
43528
+ other: "The read stopped before the end of the window."
43529
+ };
43530
+ var TRUNCATION_SHORT = {
43531
+ page_limit: "stopped at the page limit",
43532
+ time_limit: "ran out of time",
43533
+ other: "stopped before the end of the window"
43534
+ };
43535
+ var OFFSET_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})$/i;
43536
+ function resumeRange(resumeFrom, window2) {
43537
+ if (resumeFrom === null || !OFFSET_TIMESTAMP_RE.test(resumeFrom))
43538
+ return null;
43539
+ if (!OFFSET_TIMESTAMP_RE.test(window2.time_min) || !OFFSET_TIMESTAMP_RE.test(window2.time_max)) {
43540
+ return null;
43541
+ }
43542
+ const at = Date.parse(resumeFrom);
43543
+ const min = Date.parse(window2.time_min);
43544
+ const max = Date.parse(window2.time_max);
43545
+ if (!(at > min && at < max))
43546
+ return null;
43547
+ return { from: new Date(at).toISOString(), to: new Date(max).toISOString() };
43548
+ }
43549
+ function incompleteLine(args) {
43550
+ const { stamp: stamp2, eventCount, reason, resumeFrom, window: window2 } = args;
43551
+ const kept = eventCount === 0 ? "No entries were collected before the read stopped, so this is NOT a clear diary." : `${eventCount} ${eventCount === 1 ? "entry was" : "entries were"} collected before the read stopped.`;
43552
+ const range = resumeRange(resumeFrom, window2);
43553
+ const pointer = range ? ` For the rest, call the Plinth tool \`${CALENDAR_RANGE_TOOL}\` from ` + `\`${range.from}\` to \`${range.to}\` (a few entries repeat).` : ` For the rest, call the Plinth tool \`${CALENDAR_RANGE_TOOL}\` for a ` + `narrower range inside the window.`;
43554
+ return `**Read as of ${stamp2}, but this list is INCOMPLETE.** ` + `${TRUNCATION_CAUSE[reason ?? "other"]} ${kept} ` + `The rest of the window is not included: unknown, not absent.${pointer}`;
43555
+ }
43556
+ function sentence(text) {
43557
+ return /[.!?\u2026]$/.test(text) ? text : `${text}.`;
43558
+ }
43559
+ function truncationShort(reason) {
43560
+ return TRUNCATION_SHORT[reason ?? "other"];
43561
+ }
43562
+
43159
43563
  // src/lib/calendar/render.ts
43160
43564
  var CALENDAR_BODY_MARKER = "<!-- PLINTH:CALENDAR-BODY -->";
43161
43565
  var OWNERSHIP_NOTE2 = [
@@ -43259,37 +43663,61 @@ function windowLine(timeMin, timeMax) {
43259
43663
  return `${timeMin} to ${timeMax}`;
43260
43664
  return `${formatDay(from, -1)} to ${formatDay(to, -1)}`;
43261
43665
  }
43666
+ function isRefusal(error52) {
43667
+ return error52.upstream_status !== null && error52.upstream_status >= 400;
43668
+ }
43669
+ function googleErrorLine(error52) {
43670
+ if (error52 !== null && !isRefusal(error52)) {
43671
+ return `**NOT READ - the read from Google did not complete.** ` + `${sentence(oneLine2(error52.message, 300))} The next refresh retries; until then ` + `nothing below is current, so treat this calendar as unknown rather than empty. ` + `For a date on it you need now, call the Plinth tool \`${CALENDAR_RANGE_TOOL}\` ` + `for a narrower range.`;
43672
+ }
43673
+ return `**NOT READ - Google refused the request` + `${error52 ? ` (HTTP ${error52.upstream_status})` : ""}.** ` + `${error52 ? sentence(oneLine2(error52.message, 300)) : "No detail was returned."} ` + `A calendar that was unshared or deleted in Google reads exactly like this. ` + `Nothing below is current; treat this calendar as unknown rather than empty.`;
43674
+ }
43262
43675
  function statusLine(args) {
43263
43676
  const { status, eventCount, error: error52, truncated, stamp: stamp2, userLabel } = args;
43264
43677
  switch (status) {
43265
43678
  case "fetched": {
43679
+ if (truncated) {
43680
+ return incompleteLine({
43681
+ stamp: stamp2,
43682
+ eventCount,
43683
+ reason: args.truncatedReason,
43684
+ resumeFrom: args.resumeFrom,
43685
+ window: args.window
43686
+ });
43687
+ }
43266
43688
  if (eventCount === 0) {
43267
43689
  return `**Read fine as of ${stamp2}. Nothing in this window.** The diary really ` + `is clear across the dates above. This is a successful read of an empty ` + `calendar, not a failed read - if it had failed, this line would say so.`;
43268
43690
  }
43269
- const truncatedNote = truncated ? " **This list is INCOMPLETE**: Google had more entries than one page and the rest were not collected." : "";
43270
- return `**Read fine as of ${stamp2}.** ${eventCount} ` + `${eventCount === 1 ? "entry" : "entries"} in the window.${truncatedNote}`;
43691
+ return `**Read fine as of ${stamp2}.** ${eventCount} ` + `${eventCount === 1 ? "entry" : "entries"} in the window.`;
43271
43692
  }
43272
43693
  case "no-token":
43273
43694
  return `**NOT READ - Google is not connected for ${userLabel}.** Nothing is listed ` + `below because Plinth could not look, NOT because the diary is clear. ` + `Connect Google in Plinth to fill this in, and until then treat this ` + `calendar as unknown rather than empty.`;
43274
43695
  case "token-expired":
43275
43696
  return `**NOT READ - the Google connection for ${userLabel} has expired.** Plinth ` + `still holds a connection but could not refresh it, so nothing below is ` + `current. Reconnect Google in Plinth. Treat this calendar as unknown ` + `rather than empty.`;
43276
43697
  case "google-error":
43277
- return `**NOT READ - Google refused the request` + `${error52 ? ` (HTTP ${error52.upstream_status})` : ""}.** ` + `${error52 ? oneLine2(error52.message, 300) : "No detail was returned."} ` + `A calendar that was unshared or deleted in Google reads exactly like this. ` + `Nothing below is current; treat this calendar as unknown rather than empty.`;
43698
+ return googleErrorLine(error52);
43278
43699
  }
43279
43700
  }
43280
43701
  function shortStatus(row) {
43281
43702
  switch (row.status) {
43282
43703
  case "fetched": {
43283
43704
  const n = row.events?.length ?? 0;
43705
+ const count = `${n} ${n === 1 ? "entry" : "entries"}`;
43706
+ if (row.truncated) {
43707
+ return `read, ${count}, INCOMPLETE (${truncationShort(row.truncated_reason)}); ` + `the rest of the window is unknown, not empty`;
43708
+ }
43284
43709
  if (n === 0)
43285
43710
  return "read fine, nothing in this window";
43286
- return `read fine, ${n} ${n === 1 ? "entry" : "entries"}${row.truncated ? " (INCOMPLETE - more than one page)" : ""}`;
43711
+ return `read fine, ${count}`;
43287
43712
  }
43288
43713
  case "no-token":
43289
43714
  return "NOT READ - Google is not connected; unknown, not empty";
43290
43715
  case "token-expired":
43291
43716
  return "NOT READ - the Google connection has expired; unknown, not empty";
43292
43717
  case "google-error":
43718
+ if (row.error !== null && !isRefusal(row.error)) {
43719
+ return `NOT READ - the read from Google did not complete: ` + `${oneLine2(row.error.message, 120)}; the next refresh retries; unknown, not empty`;
43720
+ }
43293
43721
  return `NOT READ - Google refused` + `${row.error ? ` (HTTP ${row.error.upstream_status}): ${oneLine2(row.error.message, 120)}` : ""}` + `; unknown, not empty`;
43294
43722
  }
43295
43723
  }
@@ -43347,21 +43775,25 @@ function renderArtistCalendar(args) {
43347
43775
  eventCount: events.length,
43348
43776
  error: row.error,
43349
43777
  truncated: row.truncated,
43778
+ truncatedReason: row.truncated_reason,
43779
+ resumeFrom: row.resume_from,
43780
+ window: window2,
43350
43781
  stamp: stamp2,
43351
43782
  userLabel
43352
43783
  }),
43353
43784
  "",
43354
43785
  `**Window:** ${windowLine(window2.time_min, window2.time_max)}.`,
43355
43786
  "",
43787
+ ...row.status === "fetched" ? [coverageLine(window2.time_min, window2.time_max), ""] : [],
43356
43788
  "**Dates are read here and changed in Plinth's calendar view, by a person.** If something below needs moving, say what the change is and hand it over.",
43357
43789
  "",
43358
- renderBands(events, generatedAt, referenceYear, "")
43790
+ renderBands(events, generatedAt, referenceYear, "", row.truncated)
43359
43791
  ].join(`
43360
43792
  `);
43361
43793
  return `${header}${CALENDAR_BODY_MARKER}
43362
43794
  ${body}`;
43363
43795
  }
43364
- function renderBands(events, generatedAt, referenceYear, artistPrefix) {
43796
+ function renderBands(events, generatedAt, referenceYear, artistPrefix, truncated) {
43365
43797
  if (events.length === 0)
43366
43798
  return "";
43367
43799
  const { full, rest } = splitBands(events, generatedAt);
@@ -43375,7 +43807,7 @@ function renderBands(events, generatedAt, referenceYear, artistPrefix) {
43375
43807
  }
43376
43808
  out.push("", "## Rest of the window", "");
43377
43809
  if (rest.length === 0) {
43378
- out.push("_Nothing further in the window._");
43810
+ out.push(truncated ? "_Nothing further was collected: the read stopped before the end of the window._" : "_Nothing further in the window._");
43379
43811
  } else {
43380
43812
  for (const e of rest)
43381
43813
  out.push(compactLine(e, referenceYear, artistPrefix));
@@ -43384,6 +43816,9 @@ function renderBands(events, generatedAt, referenceYear, artistPrefix) {
43384
43816
  return out.join(`
43385
43817
  `);
43386
43818
  }
43819
+ function lacksConnection(row) {
43820
+ return row.status === "no-token" || row.status === "token-expired";
43821
+ }
43387
43822
  function renderUpcoming(args) {
43388
43823
  const {
43389
43824
  rows,
@@ -43399,6 +43834,7 @@ function renderUpcoming(args) {
43399
43834
  const fetched = rows.filter((r) => r.status === "fetched");
43400
43835
  const failed = rows.filter((r) => r.status !== "fetched");
43401
43836
  const truncatedAnywhere = rows.some((r) => r.truncated);
43837
+ const cutShort = fetched.filter((r) => r.truncated).length;
43402
43838
  const merged = fetched.flatMap((row) => (row.events ?? []).map((event) => ({ event, row }))).sort((a, b) => sortInstant(a.event) - sortInstant(b.event));
43403
43839
  const { full, rest } = splitBands(merged.map((m) => m.event), generatedAt);
43404
43840
  const fullSet = new Set(full);
@@ -43429,7 +43865,12 @@ function renderUpcoming(args) {
43429
43865
  "",
43430
43866
  `**As of ${stamp2}.** Window ${windowLine(window2.time_min, window2.time_max)}.`,
43431
43867
  "",
43868
+ ...rows.length > 0 && !rows.every(lacksConnection) ? [coverageLine(window2.time_min, window2.time_max), ""] : [],
43432
43869
  failed.length === 0 ? `**All ${fetched.length} mapped ${fetched.length === 1 ? "calendar was" : "calendars were"} read.**` : `**${fetched.length} of ${rows.length} mapped ${rows.length === 1 ? "calendar" : "calendars"} read; ${failed.length} could NOT be read.**` + ` Anything on an unread calendar is missing from the lists below - unknown, not absent.`,
43870
+ ...cutShort === 0 ? [] : [
43871
+ "",
43872
+ `**${cutShort} of the calendars read ${cutShort === 1 ? "was" : "were"} cut short** before the end ` + `of the window, so entries past the point where ${cutShort === 1 ? "it" : "each"} stopped are ` + `missing from the lists below - unknown, not absent. The status list says which.`
43873
+ ],
43433
43874
  "",
43434
43875
  `${artistCount} ${artistCount === 1 ? "artist" : "artists"} in the roster; ` + `${mappedArtistCount} ${mappedArtistCount === 1 ? "has" : "have"} a calendar mapped in Plinth.`,
43435
43876
  "",
@@ -43446,7 +43887,7 @@ function renderUpcoming(args) {
43446
43887
  body.push(`## Next ${FULL_BAND_DAYS} days`, "", "_Links are in the per-artist file named beside each calendar above._", "");
43447
43888
  const fullMerged = merged.filter((m) => fullSet.has(m.event));
43448
43889
  if (fullMerged.length === 0) {
43449
- body.push(fetched.length === 0 ? `_No calendar could be read, so nothing can be listed for the next ${FULL_BAND_DAYS} days._` : `_Nothing in the next ${FULL_BAND_DAYS} days on any calendar that was read._`);
43890
+ body.push(fetched.length === 0 ? `_No calendar could be read, so nothing can be listed for the next ${FULL_BAND_DAYS} days._` : cutShort > 0 ? `_Nothing collected for the next ${FULL_BAND_DAYS} days on any calendar that was read; the ones cut short may be missing entries._` : `_Nothing in the next ${FULL_BAND_DAYS} days on any calendar that was read._`);
43450
43891
  } else {
43451
43892
  for (const { event, row } of fullMerged) {
43452
43893
  body.push(fullLine(event, referenceYear, `**${rowDisplayName(row)}**`, false));
@@ -43455,7 +43896,7 @@ function renderUpcoming(args) {
43455
43896
  body.push("", "## Rest of the window", "");
43456
43897
  const restMerged = merged.filter((m) => restSet.has(m.event));
43457
43898
  if (restMerged.length === 0) {
43458
- body.push(fetched.length === 0 ? "_No calendar could be read, so nothing can be listed._" : "_Nothing further in the window on any calendar that was read._");
43899
+ body.push(fetched.length === 0 ? "_No calendar could be read, so nothing can be listed._" : cutShort > 0 ? "_Nothing further was collected on any calendar that was read; the ones cut short may be missing entries past where they stopped._" : "_Nothing further in the window on any calendar that was read._");
43459
43900
  } else {
43460
43901
  for (const { event, row } of restMerged) {
43461
43902
  body.push(compactLine(event, referenceYear, `**${rowDisplayName(row)}**`));
@@ -43701,8 +44142,7 @@ class PullSuppressor {
43701
44142
  }
43702
44143
 
43703
44144
  // src/lib/discover-entities.ts
43704
- var import_gray_matter12 = __toESM(require_gray_matter(), 1);
43705
- import { readFileSync as readFileSync23, statSync as statSync12 } from "node:fs";
44145
+ import { readFileSync as readFileSync24, statSync as statSync12 } from "node:fs";
43706
44146
  var DISCOVERY_WRITE_CAP_DEFAULT = 10;
43707
44147
  function discoveryWriteCap() {
43708
44148
  const raw = process.env.PLINTH_DISCOVERY_WRITE_CAP;
@@ -43729,6 +44169,11 @@ function classifyUntrackedEntity(args) {
43729
44169
  }
43730
44170
  return { kind: "create" };
43731
44171
  }
44172
+ function holdsUnreadableUntracked(args) {
44173
+ if (args.readable)
44174
+ return false;
44175
+ return classifyUntrackedEntity({ ...args, frontmatter: {} }).kind === "create";
44176
+ }
43732
44177
  function classifyRenameDestination(args) {
43733
44178
  const occupant = getEntryBySlug(args.state, args.entityType, args.entitySlug);
43734
44179
  if (occupant && occupant.id !== args.movedEntryId) {
@@ -43799,13 +44244,20 @@ function planDiscoveryWrites(args) {
43799
44244
  if (getEntryBySlug(state, entityType, entitySlug))
43800
44245
  continue;
43801
44246
  counts.discovered++;
43802
- let frontmatter;
44247
+ let raw;
43803
44248
  try {
43804
- frontmatter = import_gray_matter12.default(readFileSync23(absolutePath, "utf8")).data ?? {};
44249
+ raw = readFileSync24(absolutePath, "utf8");
43805
44250
  } catch {
43806
44251
  counts.discovered--;
43807
44252
  continue;
43808
44253
  }
44254
+ const read = readFrontmatter(raw);
44255
+ if (!read.ok && holdsUnreadableUntracked({ entityType, entitySlug, readable: false })) {
44256
+ counts.discovered--;
44257
+ planned.push({ kind: "unreadable", entityType, entitySlug, absolutePath, why: read.why });
44258
+ continue;
44259
+ }
44260
+ const frontmatter = read.ok ? read.parsed.data : {};
43809
44261
  const verdict = classifyUntrackedEntity({ entityType, entitySlug, frontmatter });
43810
44262
  if (verdict.kind === "hold") {
43811
44263
  if (verdict.held === "artist")
@@ -43846,7 +44298,7 @@ async function discoverUntrackedEntities(args) {
43846
44298
  }
43847
44299
  const cap = discoveryWriteCap();
43848
44300
  const creates = planned.filter((w) => w.kind === "create");
43849
- const toRun = creates.length > cap ? planned.filter((w) => w.kind === "rename") : planned;
44301
+ const toRun = creates.length > cap ? planned.filter((w) => w.kind !== "create") : planned;
43850
44302
  if (creates.length > cap) {
43851
44303
  counts.heldCap = creates.length;
43852
44304
  output?.log(`[DISCOVERY_HELD reason=cap creates=${creates.length} cap=${cap} source=entity-discovery]`);
@@ -43869,6 +44321,19 @@ async function runPlannedWrites(args) {
43869
44321
  let state = args.state;
43870
44322
  for (const write of planned) {
43871
44323
  try {
44324
+ if (write.kind === "unreadable") {
44325
+ state = holdUnreadableFrontmatter({
44326
+ state,
44327
+ workspaceSlug,
44328
+ absolutePath: write.absolutePath,
44329
+ entityType: write.entityType,
44330
+ slug: write.entitySlug,
44331
+ why: write.why,
44332
+ now,
44333
+ output
44334
+ });
44335
+ continue;
44336
+ }
43872
44337
  if (write.kind === "rename") {
43873
44338
  state = await runDiscoveredRename({
43874
44339
  write,
@@ -44029,8 +44494,7 @@ function recordDiscoveryFailure(args) {
44029
44494
  }
44030
44495
 
44031
44496
  // src/lib/frontmatter-id-backfill.ts
44032
- var import_gray_matter13 = __toESM(require_gray_matter(), 1);
44033
- import { readFileSync as readFileSync24, statSync as statSync13 } from "node:fs";
44497
+ import { readFileSync as readFileSync25, statSync as statSync13 } from "node:fs";
44034
44498
  var FRONTMATTER_ID_BACKFILL_VERSION = 1;
44035
44499
  function backfillWorthReporting(r) {
44036
44500
  return r.written > 0 || r.failed > 0 || r.skipped.foreignId > 0 || r.skipped.unwritable > 0 || r.skipped.slugCollision > 0;
@@ -44073,7 +44537,7 @@ function safeInode3(path5) {
44073
44537
  function backfillFrontmatterIds(args) {
44074
44538
  const { workspaceSlug, state } = args;
44075
44539
  const compose = args.compose ?? withFrontmatterId;
44076
- const readRaw = args.readFile ?? ((p) => readFileSync24(p, "utf8"));
44540
+ const readRaw = args.readFile ?? ((p) => readFileSync25(p, "utf8"));
44077
44541
  const result = emptyResult();
44078
44542
  if (state.frontmatter_id_backfill_version === FRONTMATTER_ID_BACKFILL_VERSION) {
44079
44543
  return result;
@@ -44122,7 +44586,7 @@ function backfillFrontmatterIds(args) {
44122
44586
  let rawBody;
44123
44587
  let parsedData;
44124
44588
  try {
44125
- const parsed = import_gray_matter13.default(raw);
44589
+ const parsed = parseUncached(raw);
44126
44590
  parsedData = parsed.data ?? {};
44127
44591
  existing = parsedData["id"];
44128
44592
  rawBody = parsed.content;
@@ -44201,11 +44665,11 @@ function backfillFrontmatterIds(args) {
44201
44665
  }
44202
44666
 
44203
44667
  // src/lib/reconcile.ts
44204
- import { readFileSync as readFileSync25, statSync as statSync14 } from "node:fs";
44668
+ import { readFileSync as readFileSync26, statSync as statSync14 } from "node:fs";
44205
44669
  async function reconcileDivergedRows(args) {
44206
44670
  const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
44207
44671
  const now = args.now ?? (() => new Date);
44208
- let state = args.state;
44672
+ let state = settleFrontmatterHolds(args.state, workspaceSlug);
44209
44673
  let scanned = 0;
44210
44674
  let diverged = 0;
44211
44675
  let pushed = 0;
@@ -44313,7 +44777,7 @@ async function reconcileDivergedRows(args) {
44313
44777
  }
44314
44778
  let content;
44315
44779
  try {
44316
- content = readFileSync25(absolutePath, "utf8");
44780
+ content = readFileSync26(absolutePath, "utf8");
44317
44781
  } catch (err) {
44318
44782
  if (err?.code === "ENOENT") {
44319
44783
  state = clearEntityCloudRowDeleted(state, strategy.mirrorRelativePath(entry2.slug));
@@ -44321,6 +44785,20 @@ async function reconcileDivergedRows(args) {
44321
44785
  }
44322
44786
  throw err;
44323
44787
  }
44788
+ const unreadable = frontmatterParseError(content);
44789
+ if (unreadable !== null) {
44790
+ state = holdUnreadableFrontmatter({
44791
+ state,
44792
+ workspaceSlug,
44793
+ absolutePath,
44794
+ entityType,
44795
+ slug: entry2.slug,
44796
+ why: unreadable,
44797
+ now,
44798
+ output
44799
+ });
44800
+ continue;
44801
+ }
44324
44802
  const hashes = computeMirrorHashes(strategy, content);
44325
44803
  if (hashes.frontmatterId === undefined)
44326
44804
  sawTrackedFileWithoutId = true;
@@ -44371,7 +44849,7 @@ async function reconcileDivergedRows(args) {
44371
44849
  ops: pendingOps
44372
44850
  });
44373
44851
  quarantined = ops.length;
44374
- output?.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${ops.length} reason=${result.reason.kind} source=reconcile]`);
44852
+ output?.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${ops.length} reason=${tripReasonToken(result.reason)} source=reconcile]`);
44375
44853
  } else {
44376
44854
  for (const c of guttingCandidates) {
44377
44855
  const op = c.op;
@@ -45602,7 +46080,7 @@ class Semaphore {
45602
46080
  this.active += 1;
45603
46081
  return;
45604
46082
  }
45605
- await new Promise((resolve6) => this.waiters.push(resolve6));
46083
+ await new Promise((resolve7) => this.waiters.push(resolve7));
45606
46084
  }
45607
46085
  release() {
45608
46086
  const next = this.waiters.shift();
@@ -45664,9 +46142,9 @@ function createThrottledFetch(baseFetch, opts = {}) {
45664
46142
  // src/commands/sync.ts
45665
46143
  import {
45666
46144
  existsSync as existsSync9,
45667
- lstatSync as lstatSync6,
46145
+ lstatSync as lstatSync7,
45668
46146
  readdirSync as readdirSync7,
45669
- readFileSync as readFileSync29,
46147
+ readFileSync as readFileSync30,
45670
46148
  statSync as statSync17,
45671
46149
  unlinkSync as unlinkSync9
45672
46150
  } from "node:fs";
@@ -45850,17 +46328,17 @@ function refreshConflictsView(workspaceSlug) {
45850
46328
  }
45851
46329
 
45852
46330
  // src/lib/files/pull.ts
45853
- import { existsSync as existsSync8, lstatSync as lstatSync5, readFileSync as readFileSync27, statSync as statSync15 } from "node:fs";
46331
+ import { existsSync as existsSync8, lstatSync as lstatSync6, readFileSync as readFileSync28, statSync as statSync15 } from "node:fs";
45854
46332
  import { dirname as dirname13 } from "node:path";
45855
46333
 
45856
46334
  // src/lib/permanent-disposition.ts
45857
46335
  import { createHash as createHash6 } from "node:crypto";
45858
- import { readFileSync as readFileSync26 } from "node:fs";
46336
+ import { readFileSync as readFileSync27 } from "node:fs";
45859
46337
  function sha256(value) {
45860
46338
  return createHash6("sha256").update(value).digest("hex");
45861
46339
  }
45862
46340
  function verifyStash(path5, expectedHash) {
45863
- const actual = sha256(readFileSync26(path5));
46341
+ const actual = sha256(readFileSync27(path5));
45864
46342
  if (actual !== expectedHash) {
45865
46343
  throw new Error(`permanent deletion stash verification failed for ${path5}`);
45866
46344
  }
@@ -45904,7 +46382,7 @@ function disposePermanentDocumentLocked(args) {
45904
46382
  const target = entityFilePath(args.workspaceSlug, args.strategy.type, retiredEntry?.slug ?? t.logicalKey);
45905
46383
  let raw;
45906
46384
  try {
45907
- raw = readFileSync26(target, "utf8");
46385
+ raw = readFileSync27(target, "utf8");
45908
46386
  } catch (error52) {
45909
46387
  if (error52?.code !== "ENOENT")
45910
46388
  throw error52;
@@ -46002,7 +46480,7 @@ function disposePermanentFileLocked(args) {
46002
46480
  const target = filesSubtreePath(args.workspaceSlug, t.logicalKey);
46003
46481
  let bytes;
46004
46482
  try {
46005
- bytes = readFileSync26(target);
46483
+ bytes = readFileSync27(target);
46006
46484
  } catch (error52) {
46007
46485
  if (error52?.code !== "ENOENT")
46008
46486
  throw error52;
@@ -46061,7 +46539,8 @@ async function replayPermanentFiles(args, markers) {
46061
46539
  const now = args.now ?? (() => new Date);
46062
46540
  const rows = [];
46063
46541
  let cursor = null;
46064
- let resultingCursor = args.cursor;
46542
+ const savedCursor = args.baseCursor === undefined ? args.cursor : args.baseCursor;
46543
+ let resultingCursor = savedCursor;
46065
46544
  while (true) {
46066
46545
  const page = await syncFilesPage({
46067
46546
  baseUrl: args.baseUrl,
@@ -46084,6 +46563,7 @@ async function replayPermanentFiles(args, markers) {
46084
46563
  let pointerRows = 0;
46085
46564
  let symlinkRefused = 0;
46086
46565
  let stashFailed = 0;
46566
+ let restoresDeferred = 0;
46087
46567
  let completePass = true;
46088
46568
  let lastCommittedState = args.state;
46089
46569
  const completed = [];
@@ -46172,7 +46652,8 @@ async function replayPermanentFiles(args, markers) {
46172
46652
  isFileFrozen: args.isFileFrozen,
46173
46653
  loggedTombstoneSkips: args.loggedTombstoneSkips,
46174
46654
  forceTextRewalk: args.forceTextRewalk,
46175
- loggedStashNotices: args.loggedStashNotices
46655
+ loggedStashNotices: args.loggedStashNotices,
46656
+ recentUnlinks: args.recentUnlinks
46176
46657
  });
46177
46658
  state = outcome.state;
46178
46659
  if (outcome.action === "written")
@@ -46189,11 +46670,15 @@ async function replayPermanentFiles(args, markers) {
46189
46670
  symlinkRefused++;
46190
46671
  completePass = false;
46191
46672
  }
46192
- if (outcome.stashFailed || outcome.textFailed) {
46673
+ if (outcome.stashFailed || outcome.textFailed || outcome.integrityFailed) {
46193
46674
  if (outcome.stashFailed)
46194
46675
  stashFailed++;
46195
46676
  completePass = false;
46196
46677
  }
46678
+ if (outcome.restoreDeferred) {
46679
+ restoresDeferred++;
46680
+ completePass = false;
46681
+ }
46197
46682
  }
46198
46683
  if (args.commit && state !== lastCommittedState)
46199
46684
  state = args.commit(lastCommittedState, state);
@@ -46208,7 +46693,8 @@ async function replayPermanentFiles(args, markers) {
46208
46693
  pointerRows,
46209
46694
  symlinkRefused,
46210
46695
  stashFailed,
46211
- lastCursor: completePass ? resultingCursor : args.cursor,
46696
+ lastCursor: completePass ? resultingCursor : savedCursor,
46697
+ restoresDeferred,
46212
46698
  permanentRefreshes: completed,
46213
46699
  permanentReplayComplete: completePass
46214
46700
  };
@@ -46232,12 +46718,27 @@ async function pullFiles(args) {
46232
46718
  let pointerRows = 0;
46233
46719
  let symlinkRefused = 0;
46234
46720
  let stashFailed = 0;
46721
+ let restoresDeferred = 0;
46235
46722
  let permanentEncountered = false;
46723
+ const baseCursor = args.baseCursor === undefined ? args.cursor : args.baseCursor;
46236
46724
  let cursor = args.cursor;
46237
46725
  let lastCursor = args.cursor;
46726
+ let finalHighWater = null;
46727
+ let markRefused = false;
46238
46728
  let cursorBlocked = permanentJournalBlocked;
46239
46729
  while (true) {
46240
- const page = await syncFilesPage({ baseUrl, pat, cursor, fetchImpl });
46730
+ let page;
46731
+ try {
46732
+ page = await syncFilesPage({ baseUrl, pat, cursor, fetchImpl });
46733
+ } catch (err) {
46734
+ if (!markRefused && cursor === args.cursor && args.cursor !== baseCursor && err instanceof ApiError && err.status === 400) {
46735
+ markRefused = true;
46736
+ cursor = baseCursor;
46737
+ lastCursor = baseCursor;
46738
+ continue;
46739
+ }
46740
+ throw err;
46741
+ }
46241
46742
  const pageBase = state;
46242
46743
  let pageTextFailed = false;
46243
46744
  for (const row of page.rows) {
@@ -46271,7 +46772,8 @@ async function pullFiles(args) {
46271
46772
  isFileFrozen: args.isFileFrozen,
46272
46773
  loggedTombstoneSkips: args.loggedTombstoneSkips,
46273
46774
  forceTextRewalk: args.forceTextRewalk,
46274
- loggedStashNotices: args.loggedStashNotices
46775
+ loggedStashNotices: args.loggedStashNotices,
46776
+ recentUnlinks: args.recentUnlinks
46275
46777
  });
46276
46778
  state = outcome.state;
46277
46779
  if (!outcome.symlinkRefused) {
@@ -46297,13 +46799,21 @@ async function pullFiles(args) {
46297
46799
  stashFailed++;
46298
46800
  pageTextFailed = true;
46299
46801
  }
46802
+ if (outcome.integrityFailed)
46803
+ pageTextFailed = true;
46804
+ if (outcome.restoreDeferred) {
46805
+ restoresDeferred++;
46806
+ pageTextFailed = true;
46807
+ }
46300
46808
  }
46301
46809
  if (args.commit && state !== pageBase)
46302
46810
  state = args.commit(pageBase, state);
46303
46811
  if (pageTextFailed)
46304
46812
  cursorBlocked = true;
46305
- if (page.next_cursor === null)
46813
+ if (page.next_cursor === null) {
46814
+ finalHighWater = page.high_water ?? null;
46306
46815
  break;
46816
+ }
46307
46817
  if (!cursorBlocked)
46308
46818
  lastCursor = page.next_cursor;
46309
46819
  cursor = page.next_cursor;
@@ -46340,7 +46850,14 @@ async function pullFiles(args) {
46340
46850
  pointerRows,
46341
46851
  symlinkRefused,
46342
46852
  stashFailed,
46343
- lastCursor
46853
+ lastCursor,
46854
+ highWater: provenMark({
46855
+ clean: !cursorBlocked && textUnknown === 0,
46856
+ finalHighWater,
46857
+ advancedTo: lastCursor !== (markRefused ? baseCursor : args.cursor) ? lastCursor : null
46858
+ }),
46859
+ markRefused,
46860
+ restoresDeferred
46344
46861
  };
46345
46862
  }
46346
46863
  async function applyExtractedText(args) {
@@ -46455,7 +46972,7 @@ async function applyFileRow(args) {
46455
46972
  }
46456
46973
  let localBytes;
46457
46974
  try {
46458
- localBytes = readFileSync27(abs);
46975
+ localBytes = readFileSync28(abs);
46459
46976
  } catch {
46460
46977
  localBytes = undefined;
46461
46978
  }
@@ -46501,6 +47018,17 @@ async function applyFileRow(args) {
46501
47018
  output
46502
47019
  });
46503
47020
  }
47021
+ if (existing && existing.content_hash === row.content_hash && !existsSync8(abs)) {
47022
+ const verdict = deferRestore(args.recentUnlinks, abs, row.updated_at, now().getTime());
47023
+ if (verdict.defer) {
47024
+ if (verdict.first) {
47025
+ output?.log(fileRestoreDeferredLine({ path: relPath, waitSeconds: RESTORE_DEFERRAL_MS / 1000 }));
47026
+ }
47027
+ return { state, action: "noop", textUnknown, restoreDeferred: true };
47028
+ }
47029
+ } else {
47030
+ presentAgain(args.recentUnlinks, abs);
47031
+ }
46504
47032
  if (existing && existing.content_hash === row.content_hash && existsSync8(abs)) {
46505
47033
  if (!args.forceTextRewalk && textIsCurrent(existing, row, workspaceSlug, relPath)) {
46506
47034
  return { state, action: "noop", textUnknown };
@@ -46553,7 +47081,7 @@ async function applyFileRow(args) {
46553
47081
  const bytes = await downloadBytes(got.row.download_url, fetchImpl);
46554
47082
  if (sha256Bytes(bytes) !== row.content_hash) {
46555
47083
  output?.warn(`Integrity check failed for files/${relPath} (hash mismatch) — skipped this pull.`);
46556
- return { state, action: "skipped" };
47084
+ return { state, action: "skipped", integrityFailed: true };
46557
47085
  }
46558
47086
  const stashFailure = stashUnpushedLocalEdit({
46559
47087
  workspaceSlug,
@@ -46661,7 +47189,7 @@ function stashUnpushedLocalEdit(args) {
46661
47189
  return { reason: "symlink" };
46662
47190
  let st;
46663
47191
  try {
46664
- st = lstatSync5(abs);
47192
+ st = lstatSync6(abs);
46665
47193
  } catch (err) {
46666
47194
  if (err?.code === "ENOENT")
46667
47195
  return null;
@@ -46671,7 +47199,7 @@ function stashUnpushedLocalEdit(args) {
46671
47199
  return { reason: "not_a_file" };
46672
47200
  let localBytes;
46673
47201
  try {
46674
- localBytes = readFileSync27(abs);
47202
+ localBytes = readFileSync28(abs);
46675
47203
  } catch (err) {
46676
47204
  return { reason: "unreadable", code: errCode(err) };
46677
47205
  }
@@ -46712,6 +47240,8 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
46712
47240
  }
46713
47241
  if (existing.text_hash === undefined)
46714
47242
  return false;
47243
+ if (existing.updated_at !== row.updated_at)
47244
+ return false;
46715
47245
  const onDisk = readExtractedText(workspaceSlug, relPath);
46716
47246
  if (onDisk === null)
46717
47247
  return false;
@@ -46719,7 +47249,7 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
46719
47249
  }
46720
47250
 
46721
47251
  // src/lib/files/rejection-sync.ts
46722
- import { readFileSync as readFileSync28, statSync as statSync16 } from "node:fs";
47252
+ import { readFileSync as readFileSync29, statSync as statSync16 } from "node:fs";
46723
47253
 
46724
47254
  // src/lib/files/rejections-api.ts
46725
47255
  var RejectionsFeedSchema = exports_external.object({
@@ -46852,7 +47382,7 @@ function settledDisposition(args) {
46852
47382
  return null;
46853
47383
  let bytes;
46854
47384
  try {
46855
- bytes = readFileSync28(absolute2);
47385
+ bytes = readFileSync29(absolute2);
46856
47386
  } catch {
46857
47387
  return null;
46858
47388
  }
@@ -46968,7 +47498,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
46968
47498
  }
46969
47499
  let raw;
46970
47500
  try {
46971
- raw = readFileSync29(target, "utf8");
47501
+ raw = readFileSync30(target, "utf8");
46972
47502
  } catch (err) {
46973
47503
  if (err?.code === "ENOENT") {
46974
47504
  return { didUnlink: false };
@@ -47023,6 +47553,7 @@ async function replayPermanentDocuments(args) {
47023
47553
  let state = args.state;
47024
47554
  const completed = [];
47025
47555
  let completePass = true;
47556
+ let restoresDeferred = 0;
47026
47557
  for (const marker of args.markers) {
47027
47558
  const candidates = rows.filter((row) => row.deleted_at !== null && row.slug === marker.logical_key && row.permanent_delete === true && typeof row.purge_generation === "string" && typeof row.retirement_sequence === "number");
47028
47559
  candidates.sort((a, b) => b.retirement_sequence - a.retirement_sequence);
@@ -47092,7 +47623,7 @@ async function replayPermanentDocuments(args) {
47092
47623
  for (const row of rows) {
47093
47624
  if (row.deleted_at !== null && row.permanent_delete === true)
47094
47625
  continue;
47095
- const outcome = applyRow(args.workspaceSlug, args.strategy, row, state, args.output, args.suppressor, args.now, args.isFrozen, args.loggedTombstoneSkips);
47626
+ const outcome = applyRow(args.workspaceSlug, args.strategy, row, state, args.output, args.suppressor, args.now, args.isFrozen, args.loggedTombstoneSkips, args.recentUnlinks);
47096
47627
  state = outcome.state;
47097
47628
  if (outcome.action === "written")
47098
47629
  counters.written++;
@@ -47103,9 +47634,12 @@ async function replayPermanentDocuments(args) {
47103
47634
  else if (outcome.action === "slug_refused") {
47104
47635
  counters.refused = (counters.refused ?? 0) + 1;
47105
47636
  completePass = false;
47637
+ } else if (outcome.action === "restore_deferred") {
47638
+ restoresDeferred++;
47639
+ completePass = false;
47106
47640
  }
47107
47641
  }
47108
- return { state, counters, cursor: resultingCursor, completed, completePass };
47642
+ return { state, counters, cursor: resultingCursor, completed, completePass, restoresDeferred };
47109
47643
  }
47110
47644
  async function runSync(workspaceSlug, deps = {}) {
47111
47645
  const out = new Output({
@@ -47226,6 +47760,7 @@ async function runSync(workspaceSlug, deps = {}) {
47226
47760
  committed = state;
47227
47761
  };
47228
47762
  const counters = {};
47763
+ let restoresDeferred = 0;
47229
47764
  try {
47230
47765
  for (const entityType of allEntityTypes()) {
47231
47766
  const strategy = requireStrategy(entityType);
@@ -47247,6 +47782,7 @@ async function runSync(workspaceSlug, deps = {}) {
47247
47782
  now,
47248
47783
  isFrozen: deps.isFrozen,
47249
47784
  loggedTombstoneSkips: deps.loggedTombstoneSkips,
47785
+ recentUnlinks: deps.recentUnlinks,
47250
47786
  commit: (before, after) => {
47251
47787
  const next = commitState(before, after);
47252
47788
  committed = next;
@@ -47256,8 +47792,10 @@ async function runSync(workspaceSlug, deps = {}) {
47256
47792
  state = replay.state;
47257
47793
  commitPending();
47258
47794
  counters[entityType] = replay.counters;
47795
+ restoresDeferred += replay.restoresDeferred;
47259
47796
  if (replay.completePass) {
47260
47797
  updateSyncCursor(slug, entityType, replay.cursor);
47798
+ updateSyncHighWater(slug, entityType, null);
47261
47799
  for (const item of replay.completed) {
47262
47800
  clearFeedRefresh({
47263
47801
  workspaceSlug: slug,
@@ -47270,8 +47808,11 @@ async function runSync(workspaceSlug, deps = {}) {
47270
47808
  continue;
47271
47809
  }
47272
47810
  }
47273
- let cursor = startCursor;
47274
- let lastAdvancedCursor = startCursor;
47811
+ const storedMark = entry2.sync_high_water?.[entityType] ?? null;
47812
+ let walkStart = deps.useHighWater === true && storedMark !== null ? storedMark : startCursor;
47813
+ let cursor = walkStart;
47814
+ let lastAdvancedCursor = walkStart;
47815
+ let finalHighWater = null;
47275
47816
  let cursorBlocked = false;
47276
47817
  let permanentEncountered = false;
47277
47818
  const permanentJournalBlocked = entityType === "documents" && dueDocumentRefreshMarkers(slug, now()) === null;
@@ -47287,13 +47828,26 @@ async function runSync(workspaceSlug, deps = {}) {
47287
47828
  refused: 0
47288
47829
  };
47289
47830
  while (true) {
47290
- const page = await syncEntity({
47291
- strategy,
47292
- baseUrl: entry2.api_base_url,
47293
- pat,
47294
- cursor,
47295
- fetchImpl
47296
- });
47831
+ let page;
47832
+ try {
47833
+ page = await syncEntity({
47834
+ strategy,
47835
+ baseUrl: entry2.api_base_url,
47836
+ pat,
47837
+ cursor,
47838
+ fetchImpl
47839
+ });
47840
+ } catch (err) {
47841
+ if (cursor === walkStart && walkStart !== startCursor && err instanceof ApiError && err.status === 400) {
47842
+ updateSyncHighWater(slug, entityType, null);
47843
+ out.warn(`${entityType}: the stored high-water mark was refused; reading from the cursor instead.`);
47844
+ walkStart = startCursor;
47845
+ cursor = startCursor;
47846
+ lastAdvancedCursor = startCursor;
47847
+ continue;
47848
+ }
47849
+ throw err;
47850
+ }
47297
47851
  for (const row of page.rows) {
47298
47852
  ctr.fetched++;
47299
47853
  if (entityType === "documents" && row.deleted_at !== null && row.permanent_delete === true && row.purge_generation !== undefined) {
@@ -47314,7 +47868,7 @@ async function runSync(workspaceSlug, deps = {}) {
47314
47868
  }
47315
47869
  let outcome;
47316
47870
  try {
47317
- outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen, deps.loggedTombstoneSkips);
47871
+ outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen, deps.loggedTombstoneSkips, deps.recentUnlinks);
47318
47872
  } catch (err) {
47319
47873
  if (!(err instanceof MirrorSymlinkError))
47320
47874
  throw err;
@@ -47337,14 +47891,19 @@ async function runSync(workspaceSlug, deps = {}) {
47337
47891
  ctr.deleted++;
47338
47892
  else if (outcome.action === "moved")
47339
47893
  ctr.moved = (ctr.moved ?? 0) + 1;
47340
- else if (outcome.action === "slug_refused") {
47894
+ else if (outcome.action === "restore_deferred") {
47895
+ cursorBlocked = true;
47896
+ restoresDeferred++;
47897
+ } else if (outcome.action === "slug_refused") {
47341
47898
  ctr.refused = (ctr.refused ?? 0) + 1;
47342
47899
  cursorBlocked = true;
47343
47900
  }
47344
47901
  }
47345
47902
  commitPending();
47346
- if (page.next_cursor === null)
47903
+ if (page.next_cursor === null) {
47904
+ finalHighWater = page.high_water ?? null;
47347
47905
  break;
47906
+ }
47348
47907
  if (!cursorBlocked)
47349
47908
  lastAdvancedCursor = page.next_cursor;
47350
47909
  cursor = page.next_cursor;
@@ -47368,6 +47927,7 @@ async function runSync(workspaceSlug, deps = {}) {
47368
47927
  now,
47369
47928
  isFrozen: deps.isFrozen,
47370
47929
  loggedTombstoneSkips: deps.loggedTombstoneSkips,
47930
+ recentUnlinks: deps.recentUnlinks,
47371
47931
  commit: (before, after) => {
47372
47932
  const next = commitState(before, after);
47373
47933
  committed = next;
@@ -47377,8 +47937,10 @@ async function runSync(workspaceSlug, deps = {}) {
47377
47937
  state = replay.state;
47378
47938
  commitPending();
47379
47939
  counters[entityType] = replay.counters;
47940
+ restoresDeferred += replay.restoresDeferred;
47380
47941
  if (replay.completePass) {
47381
47942
  updateSyncCursor(slug, entityType, replay.cursor);
47943
+ updateSyncHighWater(slug, entityType, null);
47382
47944
  for (const item of replay.completed) {
47383
47945
  clearFeedRefresh({
47384
47946
  workspaceSlug: slug,
@@ -47390,12 +47952,21 @@ async function runSync(workspaceSlug, deps = {}) {
47390
47952
  }
47391
47953
  continue;
47392
47954
  }
47393
- if (lastAdvancedCursor !== startCursor) {
47955
+ if (walkStart === startCursor && lastAdvancedCursor !== walkStart) {
47394
47956
  updateSyncCursor(slug, entityType, lastAdvancedCursor);
47395
47957
  }
47958
+ const mark = provenMark({
47959
+ clean: !cursorBlocked,
47960
+ finalHighWater,
47961
+ advancedTo: lastAdvancedCursor !== walkStart ? lastAdvancedCursor : null
47962
+ });
47963
+ if (mark !== null)
47964
+ updateSyncHighWater(slug, entityType, mark);
47396
47965
  }
47397
47966
  const needsDerivedTextRewalk = state.derived_text_version !== DERIVED_TEXT_VERSION;
47398
- const filesStartCursor = needsDerivedTextRewalk ? null : entry2.sync_cursors?.["files"] ?? null;
47967
+ const filesBaseCursor = needsDerivedTextRewalk ? null : entry2.sync_cursors?.["files"] ?? null;
47968
+ const filesMark = entry2.sync_high_water?.["files"] ?? null;
47969
+ const filesStartCursor = !needsDerivedTextRewalk && deps.useHighWater === true && filesMark !== null ? filesMark : filesBaseCursor;
47399
47970
  if (needsDerivedTextRewalk) {
47400
47971
  out.info("Re-reading the file index once to refresh extracted text (template changed).");
47401
47972
  }
@@ -47405,6 +47976,8 @@ async function runSync(workspaceSlug, deps = {}) {
47405
47976
  pat,
47406
47977
  state,
47407
47978
  cursor: filesStartCursor,
47979
+ baseCursor: filesBaseCursor,
47980
+ recentUnlinks: deps.recentUnlinks,
47408
47981
  fetchImpl,
47409
47982
  now,
47410
47983
  output: out,
@@ -47418,7 +47991,8 @@ async function runSync(workspaceSlug, deps = {}) {
47418
47991
  if (filesResult.state !== state) {
47419
47992
  state = filesResult.state;
47420
47993
  }
47421
- if (needsDerivedTextRewalk && filesResult.textUnknown === 0) {
47994
+ restoresDeferred += filesResult.restoresDeferred ?? 0;
47995
+ if (needsDerivedTextRewalk && filesResult.textUnknown === 0 && (filesResult.restoresDeferred ?? 0) === 0) {
47422
47996
  state = { ...state, derived_text_version: DERIVED_TEXT_VERSION };
47423
47997
  }
47424
47998
  counters["files"] = {
@@ -47427,10 +48001,19 @@ async function runSync(workspaceSlug, deps = {}) {
47427
48001
  deleted: filesResult.deleted + filesResult.textDeleted
47428
48002
  };
47429
48003
  commitPending();
47430
- if (filesResult.lastCursor !== filesStartCursor) {
48004
+ const filesWalkStart = filesResult.markRefused ? filesBaseCursor : filesStartCursor;
48005
+ const filesWalkedFromCursor = filesWalkStart === filesBaseCursor || filesResult.permanentReplayComplete === true;
48006
+ if (filesWalkedFromCursor && filesResult.lastCursor !== filesBaseCursor) {
47431
48007
  updateSyncCursor(slug, "files", filesResult.lastCursor);
47432
48008
  }
48009
+ if (filesResult.markRefused) {
48010
+ updateSyncHighWater(slug, "files", null);
48011
+ out.warn("files: the stored high-water mark was refused; reading from the cursor instead.");
48012
+ }
48013
+ if (filesResult.highWater)
48014
+ updateSyncHighWater(slug, "files", filesResult.highWater);
47433
48015
  if (filesResult.permanentReplayComplete) {
48016
+ updateSyncHighWater(slug, "files", null);
47434
48017
  for (const item of filesResult.permanentRefreshes ?? []) {
47435
48018
  clearFeedRefresh({
47436
48019
  workspaceSlug: slug,
@@ -47618,7 +48201,7 @@ async function runSync(workspaceSlug, deps = {}) {
47618
48201
  const totalRefused = Object.values(counters).reduce((sum, c) => sum + (c.refused ?? 0), 0);
47619
48202
  if (totalWritten === 0 && totalDeleted === 0 && totalMoved === 0 && totalRefused === 0) {
47620
48203
  out.success("Already up to date.");
47621
- return { exitCode: 0, applied: 0 };
48204
+ return { exitCode: 0, applied: 0, restoresDeferred };
47622
48205
  }
47623
48206
  const elapsed = ((now().getTime() - startedAt.getTime()) / 1000).toFixed(1);
47624
48207
  for (const [type, c] of Object.entries(counters)) {
@@ -47636,9 +48219,9 @@ async function runSync(workspaceSlug, deps = {}) {
47636
48219
  out.info(` ${type}: ${parts.join(", ")}`);
47637
48220
  }
47638
48221
  out.success(`Done in ${elapsed}s.`);
47639
- return { exitCode: 0, applied: totalWritten + totalMoved + totalDeleted };
48222
+ return { exitCode: 0, applied: totalWritten + totalMoved + totalDeleted, restoresDeferred };
47640
48223
  }
47641
- function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = () => new Date, isFrozen, loggedTombstoneSkips) {
48224
+ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = () => new Date, isFrozen, loggedTombstoneSkips, recentUnlinks) {
47642
48225
  if (isFrozen?.(strategy.type, row.id)) {
47643
48226
  return { state, action: "skipped" };
47644
48227
  }
@@ -47750,7 +48333,22 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
47750
48333
  const writeSlug = relocation.writeSlug;
47751
48334
  if (existingEntry && existingEntry.updated_at === live.updated_at && relocation.kind === "not_needed") {
47752
48335
  const path5 = entityFilePath(workspaceSlug, strategy.type, live.slug);
47753
- if (existsSync9(path5)) {
48336
+ const present2 = existsSync9(path5);
48337
+ if (!present2) {
48338
+ const verdict = deferRestore(recentUnlinks, path5, live.updated_at, now().getTime());
48339
+ if (verdict.defer) {
48340
+ if (verdict.first) {
48341
+ out.log(entityRestoreDeferredLine({
48342
+ entityType: strategy.type,
48343
+ slug: live.slug,
48344
+ waitSeconds: RESTORE_DEFERRAL_MS / 1000
48345
+ }));
48346
+ }
48347
+ return { state, action: "restore_deferred" };
48348
+ }
48349
+ }
48350
+ if (present2) {
48351
+ presentAgain(recentUnlinks, path5);
47754
48352
  if (strategy.type === "artists") {
47755
48353
  const artistLive = live;
47756
48354
  state = applyArtistMemoryOnPull({
@@ -47777,13 +48375,16 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
47777
48375
  if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at && relocation.kind !== "moved") {
47778
48376
  let localRaw = null;
47779
48377
  try {
47780
- localRaw = readFileSync29(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
48378
+ localRaw = readFileSync30(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
47781
48379
  } catch {
47782
48380
  localRaw = null;
47783
48381
  }
47784
48382
  if (localRaw !== null) {
47785
- const localHashes = computeMirrorHashes(strategy, localRaw);
47786
- const diverged = localHashes.bodyHash !== existingEntry.body_hash || localHashes.frontmatterHash !== existingEntry.frontmatter_hash;
48383
+ let diverged = !strategy.preserveLocalBodyOnPull && frontmatterParseError(localRaw) !== null;
48384
+ if (!diverged) {
48385
+ const localHashes = computeMirrorHashes(strategy, localRaw);
48386
+ diverged = localHashes.bodyHash !== existingEntry.body_hash || localHashes.frontmatterHash !== existingEntry.frontmatter_hash;
48387
+ }
47787
48388
  if (diverged) {
47788
48389
  const stashPath = stashConflict({
47789
48390
  workspaceSlug,
@@ -47801,7 +48402,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
47801
48402
  const target = entityFilePath(workspaceSlug, strategy.type, writeSlug);
47802
48403
  let localRaw = null;
47803
48404
  try {
47804
- localRaw = lstatSync6(target).isFile() ? readFileSync29(target, "utf8") : null;
48405
+ localRaw = lstatSync7(target).isFile() ? readFileSync30(target, "utf8") : null;
47805
48406
  } catch {
47806
48407
  localRaw = null;
47807
48408
  }
@@ -47967,7 +48568,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync18(p)
47967
48568
  return { state, changed: false };
47968
48569
  return { state: { ...state, entries: nextEntries }, changed: true };
47969
48570
  }
47970
- function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync30(p, "utf8")) {
48571
+ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync31(p, "utf8")) {
47971
48572
  let changed = false;
47972
48573
  const nextEntries = {};
47973
48574
  for (const [entityType, bucket] of Object.entries(state.entries)) {
@@ -47979,7 +48580,7 @@ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync3
47979
48580
  }
47980
48581
  try {
47981
48582
  const path5 = entityFilePath(workspaceSlug, entityType, entry2.slug);
47982
- const bodyLen = import_gray_matter14.default(readFn(path5)).content.length;
48583
+ const bodyLen = parseUncached(readFn(path5)).content.length;
47983
48584
  nextBucket[id] = { ...entry2, body_len: bodyLen };
47984
48585
  changed = true;
47985
48586
  } catch {
@@ -47992,6 +48593,18 @@ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync3
47992
48593
  return { state, changed: false };
47993
48594
  return { state: { ...state, entries: nextEntries }, changed: true };
47994
48595
  }
48596
+ function isOwnWriteEcho(suppressor, absolutePath, strategy, raw) {
48597
+ if (frontmatterParseError(raw) !== null)
48598
+ return false;
48599
+ return suppressor.shouldSuppressUpsert(absolutePath, computeMirrorHashes(strategy, raw));
48600
+ }
48601
+ function destinationIsUnreadable(absolutePath) {
48602
+ try {
48603
+ return frontmatterParseError(readFileSync31(absolutePath, "utf8")) !== null;
48604
+ } catch {
48605
+ return false;
48606
+ }
48607
+ }
47995
48608
  function makeWatchHandler(deps) {
47996
48609
  const {
47997
48610
  workspaceSlug,
@@ -48108,7 +48721,7 @@ function makeWatchHandler(deps) {
48108
48721
  spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
48109
48722
  return;
48110
48723
  }
48111
- const bytes = readFileSync30(absolutePath);
48724
+ const bytes = readFileSync31(absolutePath);
48112
48725
  const hash2 = sha256Bytes(bytes);
48113
48726
  if (suppressor?.shouldSuppressUpsert(absolutePath, {
48114
48727
  bodyHash: hash2,
@@ -48172,7 +48785,7 @@ function makeWatchHandler(deps) {
48172
48785
  if (suppressor) {
48173
48786
  let raw = null;
48174
48787
  try {
48175
- raw = readFileSync30(absolutePath, "utf8");
48788
+ raw = readFileSync31(absolutePath, "utf8");
48176
48789
  } catch {
48177
48790
  raw = null;
48178
48791
  }
@@ -48211,7 +48824,7 @@ function makeWatchHandler(deps) {
48211
48824
  if (suppressor) {
48212
48825
  let raw = null;
48213
48826
  try {
48214
- raw = readFileSync30(absolutePath, "utf8");
48827
+ raw = readFileSync31(absolutePath, "utf8");
48215
48828
  } catch {
48216
48829
  raw = null;
48217
48830
  }
@@ -48250,7 +48863,7 @@ function makeWatchHandler(deps) {
48250
48863
  if (suppressor) {
48251
48864
  let raw = null;
48252
48865
  try {
48253
- raw = readFileSync30(absolutePath, "utf8");
48866
+ raw = readFileSync31(absolutePath, "utf8");
48254
48867
  } catch {
48255
48868
  raw = null;
48256
48869
  }
@@ -48289,7 +48902,7 @@ function makeWatchHandler(deps) {
48289
48902
  if (suppressor) {
48290
48903
  let raw = null;
48291
48904
  try {
48292
- raw = readFileSync30(absolutePath, "utf8");
48905
+ raw = readFileSync31(absolutePath, "utf8");
48293
48906
  } catch {
48294
48907
  raw = null;
48295
48908
  }
@@ -48328,7 +48941,7 @@ function makeWatchHandler(deps) {
48328
48941
  if (suppressor) {
48329
48942
  let raw = null;
48330
48943
  try {
48331
- raw = readFileSync30(absolutePath, "utf8");
48944
+ raw = readFileSync31(absolutePath, "utf8");
48332
48945
  } catch {
48333
48946
  raw = null;
48334
48947
  }
@@ -48374,11 +48987,11 @@ function makeWatchHandler(deps) {
48374
48987
  const strategy = getStrategy(classifiedFrom.entityType);
48375
48988
  let toRaw = null;
48376
48989
  try {
48377
- toRaw = readFileSync30(event.toPath, "utf8");
48990
+ toRaw = readFileSync31(event.toPath, "utf8");
48378
48991
  } catch {
48379
48992
  toRaw = null;
48380
48993
  }
48381
- const isEcho = strategy !== undefined && toRaw !== null && suppressor.shouldSuppressUpsert(event.toPath, computeMirrorHashes(strategy, toRaw));
48994
+ const isEcho = strategy !== undefined && toRaw !== null && isOwnWriteEcho(suppressor, event.toPath, strategy, toRaw);
48382
48995
  if (isEcho) {
48383
48996
  output?.log(`[RENAME_SUPPRESSED entityType=${classifiedFrom.entityType} ` + `from=${event.fromSlug} id=${event.id} reason=pull_echo]`);
48384
48997
  return;
@@ -48386,6 +48999,9 @@ function makeWatchHandler(deps) {
48386
48999
  await handle({ kind: "upsert", absolutePath: event.toPath });
48387
49000
  return;
48388
49001
  }
49002
+ if (destinationIsUnreadable(event.toPath)) {
49003
+ await handle({ kind: "upsert", absolutePath: event.toPath });
49004
+ }
48389
49005
  const base3 = getState();
48390
49006
  const result2 = await pushRename({
48391
49007
  workspaceSlug,
@@ -48443,7 +49059,7 @@ function makeWatchHandler(deps) {
48443
49059
  if (event.kind === "upsert") {
48444
49060
  try {
48445
49061
  ownEcho = suppressor?.shouldSuppressUpsert(absolutePath, {
48446
- bodyHash: stableHash(readFileSync30(absolutePath, "utf8")),
49062
+ bodyHash: stableHash(readFileSync31(absolutePath, "utf8")),
48447
49063
  frontmatterHash: ""
48448
49064
  }) === true;
48449
49065
  } catch {
@@ -48498,11 +49114,11 @@ function makeWatchHandler(deps) {
48498
49114
  if (strategy) {
48499
49115
  let raw = null;
48500
49116
  try {
48501
- raw = readFileSync30(absolutePath, "utf8");
49117
+ raw = readFileSync31(absolutePath, "utf8");
48502
49118
  } catch {
48503
49119
  raw = null;
48504
49120
  }
48505
- if (raw !== null && suppressor.shouldSuppressUpsert(absolutePath, computeMirrorHashes(strategy, raw))) {
49121
+ if (raw !== null && isOwnWriteEcho(suppressor, absolutePath, strategy, raw)) {
48506
49122
  return;
48507
49123
  }
48508
49124
  }
@@ -48553,7 +49169,8 @@ function makeWatchHandler(deps) {
48553
49169
  echo: suppressor ? { watcher: "live", suppressor } : undefined
48554
49170
  }
48555
49171
  });
48556
- applyState(result.base ?? base2, clearEntityRejected(result.state, workspaceSlug, absolutePath));
49172
+ const heldUnreadable = result.outcome.kind === "ignored" && result.outcome.reason === FRONTMATTER_PARSE_ERROR;
49173
+ applyState(result.base ?? base2, heldUnreadable ? result.state : clearEntityRejected(result.state, workspaceSlug, absolutePath));
48557
49174
  reportOutcome(spinner, workspaceSlug, getState(), result.outcome);
48558
49175
  } catch (err) {
48559
49176
  const msg = err instanceof Error ? err.message : "unknown push error";
@@ -49101,7 +49718,7 @@ async function runStart(workspaceSlug, deps = {}) {
49101
49718
  return;
49102
49719
  }
49103
49720
  refreshFrozen();
49104
- out.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${batch.length} reason=${result.reason.kind}]`);
49721
+ out.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${batch.length} reason=${tripReasonToken(result.reason)}]`);
49105
49722
  spinner.warn(`Held ${batch.length} destructive op${batch.length === 1 ? "" : "s"} pending confirm → ${stashPath}. Run \`plinth confirm\` to review.`);
49106
49723
  spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
49107
49724
  }
@@ -49157,7 +49774,7 @@ async function runStart(workspaceSlug, deps = {}) {
49157
49774
  return;
49158
49775
  }
49159
49776
  refreshFrozen();
49160
- out.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${batch.length} reason=${result.reason.kind} source=files-watch]`);
49777
+ out.log(`[DESTRUCTIVE_HELD batch=${batchId} ops=${batch.length} reason=${tripReasonToken(result.reason)} source=files-watch]`);
49161
49778
  spinner.warn(`Held ${batch.length} destructive file op${batch.length === 1 ? "" : "s"} pending confirm → ${stashPath}. Run \`plinth confirm\` to review.`);
49162
49779
  spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
49163
49780
  }
@@ -49187,13 +49804,18 @@ async function runStart(workspaceSlug, deps = {}) {
49187
49804
  return { exitCode: 0 };
49188
49805
  }
49189
49806
  writeDaemonPid(slug, process.pid, (deps.now ?? (() => new Date))());
49190
- const watcher = startWatcher({ workspaceSlug: slug, getStateEntry, classifyPath }, handler);
49807
+ const ambient = createAmbientPollWiring({
49808
+ now: () => (deps.now?.() ?? new Date).getTime(),
49809
+ baseReadIntervalMs: baseReadIntervalMs()
49810
+ });
49811
+ const watcher = startWatcher({ workspaceSlug: slug, getStateEntry, classifyPath, onUnlink: ambient.onUnlink }, handler);
49191
49812
  const quietSink = discardSink();
49192
49813
  let draining = false;
49193
49814
  let lastHeartbeatMs = 0;
49194
49815
  const pullLoop = new PullLoop({
49195
49816
  runOnce: async () => {
49196
- const result = await runSync(slug, {
49817
+ const result = await ambient.poll((sc2) => runSync(slug, {
49818
+ ...sc2,
49197
49819
  fetchImpl,
49198
49820
  keychain,
49199
49821
  now: deps.now,
@@ -49205,7 +49827,7 @@ async function runStart(workspaceSlug, deps = {}) {
49205
49827
  isFileFrozen,
49206
49828
  loggedTombstoneSkips,
49207
49829
  loggedStashNotices
49208
- });
49830
+ }));
49209
49831
  if (result.exitCode === 0) {
49210
49832
  const applied = result.applied ?? 0;
49211
49833
  if (applied > 0) {
@@ -49244,6 +49866,9 @@ async function runStart(workspaceSlug, deps = {}) {
49244
49866
  afterPoll: () => {
49245
49867
  (async () => {
49246
49868
  state = loadState(slug);
49869
+ const unsettled = state;
49870
+ const settled = settleFrontmatterHolds(unsettled, slug);
49871
+ state = rebaseState(state, unsettled, settled);
49247
49872
  if (draining)
49248
49873
  return;
49249
49874
  draining = true;
@@ -49465,12 +50090,12 @@ async function runStart(workspaceSlug, deps = {}) {
49465
50090
  try {
49466
50091
  await Promise.race([
49467
50092
  watcher.drainPending().then(() => Promise.all([gate.drain(), fileGate.drain()])),
49468
- new Promise((resolve6) => setTimeout(() => resolve6(-1), SHUTDOWN_DRAIN_TIMEOUT_MS))
50093
+ new Promise((resolve7) => setTimeout(() => resolve7(-1), SHUTDOWN_DRAIN_TIMEOUT_MS))
49469
50094
  ]);
49470
50095
  } finally {
49471
50096
  await Promise.race([
49472
50097
  watcher.stop(),
49473
- new Promise((resolve6) => setTimeout(resolve6, SHUTDOWN_DRAIN_TIMEOUT_MS))
50098
+ new Promise((resolve7) => setTimeout(resolve7, SHUTDOWN_DRAIN_TIMEOUT_MS))
49474
50099
  ]);
49475
50100
  clearDaemonPid(slug);
49476
50101
  releaseDaemonLock(lockHandle);
@@ -50025,7 +50650,7 @@ function assessContextWalls(workspaceSlug, deps = {}) {
50025
50650
 
50026
50651
  // src/lib/build-stamp.ts
50027
50652
  import { execFileSync as execFileSync4 } from "node:child_process";
50028
- import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync31, writeFileSync as writeFileSync8 } from "node:fs";
50653
+ import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync32, writeFileSync as writeFileSync8 } from "node:fs";
50029
50654
  import { dirname as dirname14, join as join19 } from "node:path";
50030
50655
  import { fileURLToPath } from "node:url";
50031
50656
  var BUILD_STAMP_FILENAME = "build-stamp.json";
@@ -50049,7 +50674,7 @@ function readHead(repoDir) {
50049
50674
  }
50050
50675
  function readBuildStamp(distDir) {
50051
50676
  try {
50052
- const parsed = JSON.parse(readFileSync31(stampPath(distDir), "utf8"));
50677
+ const parsed = JSON.parse(readFileSync32(stampPath(distDir), "utf8"));
50053
50678
  if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
50054
50679
  return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
50055
50680
  }
@@ -50110,12 +50735,12 @@ function checkBuildStaleness(deps = {}) {
50110
50735
  }
50111
50736
 
50112
50737
  // src/lib/memory-links.ts
50113
- import { readFileSync as readFileSync32 } from "node:fs";
50114
- import { relative as relative15, sep as sep8 } from "node:path";
50738
+ import { readFileSync as readFileSync33 } from "node:fs";
50739
+ import { relative as relative15, sep as sep9 } from "node:path";
50115
50740
  var MEMORY_FILE_RE = /^(workspace-memory\.md|user-memory\.md|artists\/[^/]+\/memory\.md|memory\/[^/]+\.md|memory\/private\/[^/]+\.md)$/i;
50116
50741
  function scanMemoryLinks(deps) {
50117
50742
  const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
50118
- const readFile = deps.readFile ?? ((p) => readFileSync32(p, "utf8"));
50743
+ const readFile = deps.readFile ?? ((p) => readFileSync33(p, "utf8"));
50119
50744
  let root;
50120
50745
  let absPaths;
50121
50746
  try {
@@ -50124,7 +50749,7 @@ function scanMemoryLinks(deps) {
50124
50749
  } catch {
50125
50750
  return { scanned: 0, unreadable: [], dead: [] };
50126
50751
  }
50127
- const relPaths = absPaths.map((p) => relative15(root, p).split(sep8).join("/"));
50752
+ const relPaths = absPaths.map((p) => relative15(root, p).split(sep9).join("/"));
50128
50753
  const artists = buildArtistIndex(relPaths, root, (abs) => readFile(abs));
50129
50754
  const dead = [];
50130
50755
  const unreadable = [];
@@ -50159,9 +50784,8 @@ function scanMemoryLinks(deps) {
50159
50784
  init_output();
50160
50785
 
50161
50786
  // src/lib/pending-writes.ts
50162
- import { readFileSync as readFileSync33, statSync as statSync19 } from "node:fs";
50787
+ import { readFileSync as readFileSync34, statSync as statSync19 } from "node:fs";
50163
50788
  import { relative as relative16 } from "node:path";
50164
- var import_gray_matter15 = __toESM(require_gray_matter(), 1);
50165
50789
  var EMPTY3 = { writes: [], count: 0 };
50166
50790
  function reportedElsewhere(state) {
50167
50791
  const entities = new Set;
@@ -50207,7 +50831,7 @@ function scanPendingWrites(args) {
50207
50831
  continue;
50208
50832
  let hash2;
50209
50833
  try {
50210
- hash2 = sha256Bytes(readFileSync33(absolutePath));
50834
+ hash2 = sha256Bytes(readFileSync34(absolutePath));
50211
50835
  } catch {
50212
50836
  continue;
50213
50837
  }
@@ -50238,17 +50862,15 @@ function scanPendingWrites(args) {
50238
50862
  continue;
50239
50863
  let content;
50240
50864
  try {
50241
- content = readFileSync33(absolutePath, "utf8");
50865
+ content = readFileSync34(absolutePath, "utf8");
50242
50866
  } catch {
50243
50867
  continue;
50244
50868
  }
50245
50869
  if (!entry2) {
50246
- let frontmatter;
50247
- try {
50248
- frontmatter = import_gray_matter15.default(content).data ?? {};
50249
- } catch {
50870
+ const read = readFrontmatter(content);
50871
+ if (holdsUnreadableUntracked({ entityType, entitySlug, readable: read.ok }))
50250
50872
  continue;
50251
- }
50873
+ const frontmatter = read.ok ? read.parsed.data : {};
50252
50874
  const verdict = classifyUntrackedEntity({
50253
50875
  entityType,
50254
50876
  entitySlug,
@@ -50704,7 +51326,7 @@ async function runStatus(args, deps = {}) {
50704
51326
  }
50705
51327
  }
50706
51328
  if (freshness.length > 0) {
50707
- out.info(row("Memory context", `${freshness.length} file${freshness.length === 1 ? "" : "s"} could not be marked with ${freshness.length === 1 ? "its" : "their"} current memory state. Your edits are untouched.`));
51329
+ out.info(row("Memory context", `${freshness.length} file${freshness.length === 1 ? "" : "s"} could not be marked with ${freshness.length === 1 ? "its" : "their"} current memory state.`));
50708
51330
  for (const r of freshness.slice(0, MAX_SHOWN)) {
50709
51331
  out.info(cont(r.message));
50710
51332
  }
@@ -51149,7 +51771,7 @@ var versionCommand = defineCommand({
51149
51771
  });
51150
51772
 
51151
51773
  // src/commands/voice-gate.ts
51152
- import { readFileSync as readFileSync36 } from "node:fs";
51774
+ import { readFileSync as readFileSync37 } from "node:fs";
51153
51775
 
51154
51776
  // src/lib/voice-gate/api.ts
51155
51777
  var VoiceGateSurfaces = [
@@ -51254,7 +51876,7 @@ async function callVoiceGate(req) {
51254
51876
  import { createHash as createHash7 } from "node:crypto";
51255
51877
  import {
51256
51878
  mkdirSync as mkdirSync13,
51257
- readFileSync as readFileSync34,
51879
+ readFileSync as readFileSync35,
51258
51880
  renameSync as renameSync9,
51259
51881
  unlinkSync as unlinkSync10,
51260
51882
  writeFileSync as writeFileSync9
@@ -51285,7 +51907,7 @@ function emptyFile() {
51285
51907
  }
51286
51908
  function load2(workspaceSlug) {
51287
51909
  try {
51288
- const raw = readFileSync34(attemptsPath(workspaceSlug), "utf8");
51910
+ const raw = readFileSync35(attemptsPath(workspaceSlug), "utf8");
51289
51911
  const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
51290
51912
  return parsed.success ? parsed.data : emptyFile();
51291
51913
  } catch {
@@ -51448,9 +52070,9 @@ function denyUngated(detail) {
51448
52070
  }
51449
52071
 
51450
52072
  // src/lib/voice-gate/payload.ts
51451
- import { readFileSync as readFileSync35, statSync as statSync21 } from "node:fs";
52073
+ import { readFileSync as readFileSync36, statSync as statSync21 } from "node:fs";
51452
52074
  import { homedir as homedir2 } from "node:os";
51453
- import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
52075
+ import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
51454
52076
  var MAX_BODY_BYTES = 256 * 1024;
51455
52077
  var HELPER_BASENAME = "push_gmail_draft.py";
51456
52078
  var DRAFT_FLAGS = ["--to", "--body-file", "--body-stdin"];
@@ -51609,7 +52231,7 @@ function defaultReadBodyFile(path5) {
51609
52231
  if (stat4.size > MAX_BODY_BYTES) {
51610
52232
  throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
51611
52233
  }
51612
- return readFileSync35(path5, "utf8");
52234
+ return readFileSync36(path5, "utf8");
51613
52235
  }
51614
52236
  function parseRecipients(...raw) {
51615
52237
  const all = raw.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
@@ -51644,13 +52266,13 @@ function applyCd(tokens, state) {
51644
52266
  return { dir: expanded, known: true };
51645
52267
  if (!state.known)
51646
52268
  return state;
51647
- return { dir: resolve6(state.dir, expanded), known: true };
52269
+ return { dir: resolve7(state.dir, expanded), known: true };
51648
52270
  }
51649
52271
  function expandHome(path5) {
51650
52272
  if (path5 === "~")
51651
52273
  return homedir2();
51652
52274
  if (path5.startsWith("~/"))
51653
- return resolve6(homedir2(), path5.slice(2));
52275
+ return resolve7(homedir2(), path5.slice(2));
51654
52276
  return path5;
51655
52277
  }
51656
52278
  function classifyHelperSegment(tokens, dirState, readBodyFile) {
@@ -51676,7 +52298,7 @@ function classifyHelperSegment(tokens, dirState, readBodyFile) {
51676
52298
  unreadable: `the draft push uses a relative \`--body-file\` (${bodyFile}) after a \`cd\` ` + `the voice gate cannot resolve, so it cannot tell which file ships. Pass an ` + `absolute \`--body-file\` path.`
51677
52299
  };
51678
52300
  }
51679
- const resolved = isAbsolute4(expanded) ? expanded : resolve6(dirState.dir, expanded);
52301
+ const resolved = isAbsolute4(expanded) ? expanded : resolve7(dirState.dir, expanded);
51680
52302
  let body;
51681
52303
  try {
51682
52304
  body = readBodyFile(resolved);
@@ -51969,7 +52591,7 @@ async function runVoiceGateDirect(args, deps = {}) {
51969
52591
  let text;
51970
52592
  if (args.file) {
51971
52593
  try {
51972
- text = readFileSync36(args.file, "utf8");
52594
+ text = readFileSync37(args.file, "utf8");
51973
52595
  } catch (err) {
51974
52596
  const msg = err instanceof Error ? err.message : "unknown error";
51975
52597
  out.error(`Could not read ${args.file}: ${msg}`);