@plinth-music/cli 0.23.1 → 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.1",
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, {
@@ -22189,7 +22226,7 @@ function buildFrontmatter(doc2, projectionKeys) {
22189
22226
  return { ...doc2.frontmatter, ...projection };
22190
22227
  }
22191
22228
  function serialiseRow(doc2, projectionKeys, bodyAccessor) {
22192
- const out = import_gray_matter.default.stringify(bodyAccessor(doc2), buildFrontmatter(doc2, projectionKeys));
22229
+ const out = stringifyFrontmatter(bodyAccessor(doc2), buildFrontmatter(doc2, projectionKeys));
22193
22230
  return out.endsWith(`
22194
22231
  `) ? out : out + `
22195
22232
  `;
@@ -22829,7 +22866,6 @@ var documentsStrategy = {
22829
22866
  };
22830
22867
 
22831
22868
  // src/lib/entities/meetings.ts
22832
- var import_gray_matter2 = __toESM(require_gray_matter(), 1);
22833
22869
  var MeetingLiveSchema = exports_external.object({
22834
22870
  ...BASE_ROW,
22835
22871
  deleted_at: exports_external.null(),
@@ -22984,7 +23020,7 @@ function serialise(doc2) {
22984
23020
  decisions: doc2.decisions,
22985
23021
  follow_up_summary: doc2.follow_up_summary
22986
23022
  });
22987
- const out = import_gray_matter2.default.stringify(body, fm);
23023
+ const out = stringifyFrontmatter(body, fm);
22988
23024
  return out.endsWith(`
22989
23025
  `) ? out : out + `
22990
23026
  `;
@@ -24475,7 +24511,6 @@ function readContextFile(workspaceSlug, relativePath) {
24475
24511
  }
24476
24512
 
24477
24513
  // src/lib/backlinks/parse.ts
24478
- var import_gray_matter3 = __toESM(require_gray_matter(), 1);
24479
24514
  var RENDERED_BLOCKS = [
24480
24515
  { name: "tasks", begin: TASKS_BLOCK_BEGIN, end: TASKS_BLOCK_END },
24481
24516
  { name: "comments", begin: COMMENTS_BLOCK_BEGIN, end: COMMENTS_BLOCK_END },
@@ -24557,7 +24592,7 @@ function frontmatterLinks(body) {
24557
24592
  return null;
24558
24593
  let data;
24559
24594
  try {
24560
- data = import_gray_matter3.default(body).data;
24595
+ data = matter(body).data;
24561
24596
  } catch {
24562
24597
  return null;
24563
24598
  }
@@ -24825,7 +24860,7 @@ function buildArtistIndex(relPaths, root, readFile) {
24825
24860
  const slug = m[1];
24826
24861
  let name = null;
24827
24862
  try {
24828
- const raw = import_gray_matter4.default(readFile(`${root}${sep2}${rel}`)).data;
24863
+ const raw = matter(readFile(`${root}${sep2}${rel}`)).data;
24829
24864
  if (typeof raw.name === "string" && raw.name.trim().length > 0) {
24830
24865
  name = raw.name.trim();
24831
24866
  }
@@ -25634,7 +25669,8 @@ var TripReasonSchema = exports_external.object({
25634
25669
  entityType: exports_external.string().optional(),
25635
25670
  typeCount: exports_external.number().int().nonnegative().optional(),
25636
25671
  typePopulation: exports_external.number().int().nonnegative().optional(),
25637
- pct: exports_external.number().optional()
25672
+ pct: exports_external.number().optional(),
25673
+ loneRemoved: exports_external.number().int().nonnegative().optional()
25638
25674
  });
25639
25675
  var PendingBatchFileSchema = exports_external.object({
25640
25676
  version: exports_external.literal(1),
@@ -25743,6 +25779,9 @@ function describeTrip(reason) {
25743
25779
  const pct = reason.pct !== undefined ? ` (${Math.round(reason.pct * 100)}%)` : "";
25744
25780
  return `${reason.typeCount}/${reason.typePopulation} ${reason.entityType}${pct} — proportion threshold`;
25745
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
+ }
25746
25785
  return `${reason.count} destructive ops — count threshold`;
25747
25786
  }
25748
25787
  function formatBytes(n) {
@@ -27630,7 +27669,6 @@ function decideRegen(input) {
27630
27669
  }
27631
27670
 
27632
27671
  // src/lib/context/roster.ts
27633
- var import_gray_matter5 = __toESM(require_gray_matter(), 1);
27634
27672
  import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "node:fs";
27635
27673
  import { join as join7 } from "node:path";
27636
27674
  function readRoster(workspaceSlug) {
@@ -27655,7 +27693,7 @@ function readRoster(workspaceSlug) {
27655
27693
  continue;
27656
27694
  }
27657
27695
  try {
27658
- const fm = import_gray_matter5.default(raw).data;
27696
+ const fm = matter(raw).data;
27659
27697
  const name = typeof fm.name === "string" && fm.name.length > 0 ? fm.name : slug;
27660
27698
  const status = typeof fm.status === "string" ? fm.status : null;
27661
27699
  roster.push({ slug, name, status });
@@ -29045,7 +29083,6 @@ var SKILL_LOCATIONS = [
29045
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.";
29046
29084
 
29047
29085
  // src/lib/context/artist-data.ts
29048
- var import_gray_matter6 = __toESM(require_gray_matter(), 1);
29049
29086
  import { readFileSync as readFileSync10 } from "node:fs";
29050
29087
  function artistUuidBySlug(state) {
29051
29088
  const map2 = new Map;
@@ -29172,7 +29209,7 @@ function readEntityFrontmatter(workspaceSlug, entityType, slug) {
29172
29209
  return null;
29173
29210
  }
29174
29211
  try {
29175
- return import_gray_matter6.default(raw).data;
29212
+ return matter(raw).data;
29176
29213
  } catch {
29177
29214
  return null;
29178
29215
  }
@@ -30545,6 +30582,9 @@ function fileDeleteSuppressedLine(f) {
30545
30582
  function entityDeleteSuppressedLine(f) {
30546
30583
  return `[ENTITY_DELETE_SUPPRESSED entityType=${f.entityType} slug=${f.slug} reason=${f.reason}]`;
30547
30584
  }
30585
+ function entityPushHeldLine(f) {
30586
+ return `[PUSH_HELD entityType=${f.entityType} slug=${f.slug} reason=${f.reason}]`;
30587
+ }
30548
30588
  function entityRestoreDeferredLine(f) {
30549
30589
  return `[ENTITY_RESTORE_DEFERRED entityType=${f.entityType} slug=${f.slug} wait_s=${f.waitSeconds}]`;
30550
30590
  }
@@ -30739,6 +30779,34 @@ function recordEntityRejected(args) {
30739
30779
  rejected_at: args.now().toISOString()
30740
30780
  }));
30741
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
+ }
30742
30810
  function recordFileRejected(args) {
30743
30811
  const cat = categorizeError(args.err);
30744
30812
  const withoutStale = removeUnsynced(args.state, "file", args.relPath).state;
@@ -34319,8 +34387,7 @@ var sessionCommand = defineCommand({
34319
34387
  });
34320
34388
 
34321
34389
  // src/commands/start.ts
34322
- var import_gray_matter14 = __toESM(require_gray_matter(), 1);
34323
- 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";
34324
34391
 
34325
34392
  // src/lib/daemon-log.ts
34326
34393
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync9, renameSync as renameSync7, statSync as statSync4 } from "node:fs";
@@ -34386,9 +34453,311 @@ class DaemonLog {
34386
34453
  }
34387
34454
 
34388
34455
  // src/lib/destructive-gate.ts
34389
- var import_gray_matter7 = __toESM(require_gray_matter(), 1);
34390
- import { readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
34456
+ import { readFileSync as readFileSync16, statSync as statSync6 } from "node:fs";
34391
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
34392
34761
  var GUTTING_RATIO_DEFAULT = 0.5;
34393
34762
  var GUTTING_MIN_PRIOR_BYTES = 200;
34394
34763
  function envFloat(name, def) {
@@ -34431,10 +34800,10 @@ function classifyDestructive(event, entityType, state, deps = {}) {
34431
34800
  absolutePath: event.absolutePath
34432
34801
  };
34433
34802
  }
34434
- const readFile = deps.readFile ?? ((p) => readFileSync14(p, "utf8"));
34803
+ const readFile = deps.readFile ?? ((p) => readFileSync16(p, "utf8"));
34435
34804
  const statInode = deps.statInode ?? ((p) => {
34436
34805
  try {
34437
- return statSync5(p).ino;
34806
+ return statSync6(p).ino;
34438
34807
  } catch {
34439
34808
  return;
34440
34809
  }
@@ -34445,13 +34814,11 @@ function classifyDestructive(event, entityType, state, deps = {}) {
34445
34814
  } catch {
34446
34815
  return null;
34447
34816
  }
34448
- let newLen;
34449
- try {
34450
- const body = import_gray_matter7.default(raw).content;
34451
- newLen = (getStrategy(entityType)?.stripLocalBlocks?.(body) ?? body).length;
34452
- } catch {
34817
+ const read = readFrontmatter(raw);
34818
+ if (!read.ok)
34453
34819
  return null;
34454
- }
34820
+ const body = read.parsed.content;
34821
+ const newLen = (getStrategy(entityType)?.stripLocalBlocks?.(body) ?? body).length;
34455
34822
  const liveInode = statInode(event.absolutePath);
34456
34823
  const slug = basename4(event.absolutePath, ".md");
34457
34824
  const entry2 = (liveInode !== undefined ? getEntryByInode(state, entityType, liveInode) : undefined) ?? getEntryBySlug(state, entityType, slug);
@@ -34470,13 +34837,26 @@ function classifyDestructive(event, entityType, state, deps = {}) {
34470
34837
  newLen
34471
34838
  };
34472
34839
  }
34840
+ var LONE_MIN_REMOVED_DEFAULT = 5000;
34841
+ var LONE_MIN_REMOVED_BYTES_DEFAULT = 100 * 1024;
34473
34842
  function defaultThresholds() {
34474
34843
  return {
34475
34844
  absCount: envInt("PLINTH_GATE_ABS_COUNT", 3),
34476
34845
  propMinOps: envInt("PLINTH_GATE_PROP_MIN_OPS", 2),
34477
- 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)
34478
34849
  };
34479
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
+ }
34480
34860
  function evaluateBatch(ops, typePopulations, thresholds = defaultThresholds()) {
34481
34861
  const count = ops.length;
34482
34862
  if (count === 0)
@@ -34504,7 +34884,7 @@ function evaluateBatch(ops, typePopulations, thresholds = defaultThresholds()) {
34504
34884
  };
34505
34885
  }
34506
34886
  }
34507
- return { tripped: false };
34887
+ return evaluateLoneOverwrite(count, ops.map((op) => op.kind === "overwrite" ? op.priorLen - op.newLen : 0), thresholds.loneMinRemoved);
34508
34888
  }
34509
34889
  function dedupeOps(ops) {
34510
34890
  const byKey = new Map;
@@ -34564,135 +34944,10 @@ class DestructiveGate {
34564
34944
  }
34565
34945
 
34566
34946
  // src/lib/resolve-pending.ts
34567
- import { readFileSync as readFileSync20 } from "node:fs";
34568
-
34569
- // src/lib/conflicts.ts
34570
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, readdirSync as readdirSync6, statSync as statSync6, writeFileSync as writeFileSync7 } from "node:fs";
34571
- import { dirname as dirname9, join as join12 } from "node:path";
34572
- function conflictsRoot(workspaceSlug) {
34573
- return join12(plinthHome(), "_conflicts", workspaceSlug);
34574
- }
34575
- function conflictFilePath(args) {
34576
- const { workspaceSlug, entityType, entitySlug, timestamp, side } = args;
34577
- const iso = timestamp.toISOString().replace(/[:.]/g, "-");
34578
- return join12(conflictsRoot(workspaceSlug), `${entityType}.${entitySlug}.${iso}.${side}.md`);
34579
- }
34580
- function stashConflict(args) {
34581
- const target = conflictFilePath(args);
34582
- mkdirSync10(dirname9(target), { recursive: true, mode: 448 });
34583
- writeFileSync7(target, args.contents, { mode: 384 });
34584
- return target;
34585
- }
34586
- function stashEntityConflictOnce(args) {
34587
- const dir = conflictsRoot(args.workspaceSlug);
34588
- const head = `${args.entityType}.${args.entitySlug}.`;
34589
- const tail = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${args.side}\\.md$`);
34590
- let names = [];
34591
- try {
34592
- names = readdirSync6(dir);
34593
- } catch {
34594
- names = [];
34595
- }
34596
- const newest = names.filter((name) => name.startsWith(head) && tail.test(name.slice(head.length))).sort().pop();
34597
- if (newest !== undefined) {
34598
- const path5 = join12(dir, newest);
34599
- try {
34600
- if (readFileSync15(path5, "utf8") === args.contents)
34601
- return { path: path5, reused: true };
34602
- } catch {}
34603
- }
34604
- return { path: stashConflict(args), reused: false };
34605
- }
34606
- function fileConflictPath(args) {
34607
- const { workspaceSlug, relPath, timestamp, side } = args;
34608
- const iso = timestamp.toISOString().replace(/[:.]/g, "-");
34609
- return join12(conflictsRoot(workspaceSlug), `${fileConflictStem(relPath)}.${iso}.${side}`);
34610
- }
34611
- function fileConflictStem(relPath) {
34612
- return `files.${relPath.replace(/[/\\]/g, "__")}`;
34613
- }
34614
- function stashFileConflict(args) {
34615
- const target = fileConflictPath(args);
34616
- mkdirSync10(dirname9(target), { recursive: true, mode: 448 });
34617
- writeFileSync7(target, args.bytes, { mode: 384 });
34618
- return target;
34619
- }
34620
- function stashFileConflictOnce(args) {
34621
- const dir = conflictsRoot(args.workspaceSlug);
34622
- const head = `${fileConflictStem(args.relPath)}.`;
34623
- const tail = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${args.side}$`);
34624
- let names = [];
34625
- try {
34626
- names = readdirSync6(dir);
34627
- } catch {
34628
- names = [];
34629
- }
34630
- const newest = names.filter((n) => n.startsWith(head) && tail.test(n.slice(head.length))).sort().pop();
34631
- if (newest !== undefined) {
34632
- const path5 = join12(dir, newest);
34633
- try {
34634
- if (Buffer.compare(args.bytes, readFileSync15(path5)) === 0)
34635
- return { path: path5, reused: true };
34636
- } catch {}
34637
- }
34638
- return { path: stashFileConflict(args), reused: false };
34639
- }
34640
- var STAMP_RE2 = /\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)(?:\.(local|cloud))?(?:\.md)?$/;
34641
- function parseStamp(stamp) {
34642
- const iso = stamp.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
34643
- const d = new Date(iso);
34644
- return Number.isNaN(d.getTime()) ? null : d;
34645
- }
34646
- function labelFor(file2) {
34647
- const m = STAMP_RE2.exec(file2);
34648
- if (!m)
34649
- return file2;
34650
- const head = file2.slice(0, m.index);
34651
- if (head.startsWith("files.")) {
34652
- return `files/${head.slice("files.".length).replace(/__/g, "/")}`;
34653
- }
34654
- const dot = head.indexOf(".");
34655
- if (dot === -1)
34656
- return head;
34657
- return `${head.slice(0, dot)}/${head.slice(dot + 1)}`;
34658
- }
34659
- function listConflictStashes(workspaceSlug) {
34660
- const dir = conflictsRoot(workspaceSlug);
34661
- let names;
34662
- try {
34663
- names = readdirSync6(dir);
34664
- } catch {
34665
- return [];
34666
- }
34667
- const out = [];
34668
- for (const file2 of names) {
34669
- const absPath = join12(dir, file2);
34670
- let st;
34671
- try {
34672
- st = statSync6(absPath);
34673
- } catch {
34674
- continue;
34675
- }
34676
- if (!st.isFile())
34677
- continue;
34678
- if (file2.startsWith("."))
34679
- continue;
34680
- const m = STAMP_RE2.exec(file2);
34681
- const side = m?.[2];
34682
- out.push({
34683
- file: file2,
34684
- absPath,
34685
- label: labelFor(file2),
34686
- stashedAt: m ? parseStamp(m[1]) : null,
34687
- side: side === "local" || side === "cloud" ? side : null,
34688
- mtimeMs: st.mtimeMs
34689
- });
34690
- }
34691
- return out.sort((a, b) => (b.stashedAt?.getTime() ?? b.mtimeMs) - (a.stashedAt?.getTime() ?? a.mtimeMs));
34692
- }
34947
+ import { readFileSync as readFileSync21 } from "node:fs";
34693
34948
 
34694
34949
  // src/lib/files/push.ts
34695
- import { readFileSync as readFileSync17, statSync as statSync7 } from "node:fs";
34950
+ import { readFileSync as readFileSync18, statSync as statSync7 } from "node:fs";
34696
34951
 
34697
34952
  // src/lib/permanent-deletions.ts
34698
34953
  import { createHash as createHash3, randomUUID } from "node:crypto";
@@ -34706,7 +34961,7 @@ import {
34706
34961
  lstatSync as lstatSync4,
34707
34962
  mkdirSync as mkdirSync11,
34708
34963
  openSync as openSync4,
34709
- readFileSync as readFileSync16,
34964
+ readFileSync as readFileSync17,
34710
34965
  renameSync as renameSync8,
34711
34966
  unlinkSync as unlinkSync8,
34712
34967
  writeSync as writeSync2
@@ -34897,7 +35152,7 @@ function loadPermanentDeletionJournal(workspaceSlug) {
34897
35152
  return emptyJournal(workspaceSlug);
34898
35153
  assertPrivateDirectory(dirname10(target));
34899
35154
  assertOwnerOnlyFile(target);
34900
- const journal = parseJournal(workspaceSlug, readFileSync16(target, "utf8"));
35155
+ const journal = parseJournal(workspaceSlug, readFileSync17(target, "utf8"));
34901
35156
  try {
34902
35157
  fsyncDirectory(dirname10(target));
34903
35158
  } catch (error52) {
@@ -34964,7 +35219,7 @@ function writePermanentDeletionJournal(prior, next) {
34964
35219
  let installed2 = false;
34965
35220
  try {
34966
35221
  assertOwnerOnlyFile(target);
34967
- const candidate = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
35222
+ const candidate = parseJournal(body.workspace_slug, readFileSync17(target, "utf8"));
34968
35223
  installed2 = candidate.revision === body.revision && checksum(candidate) === checksum(body);
34969
35224
  } catch {
34970
35225
  installed2 = false;
@@ -34978,7 +35233,7 @@ function writePermanentDeletionJournal(prior, next) {
34978
35233
  }
34979
35234
  }
34980
35235
  assertOwnerOnlyFile(target);
34981
- const installed = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
35236
+ const installed = parseJournal(body.workspace_slug, readFileSync17(target, "utf8"));
34982
35237
  if (installed.revision !== body.revision || checksum(installed) !== checksum(body)) {
34983
35238
  throw new PermanentDeletionJournalError("renamed journal revision did not validate", "durability_indeterminate");
34984
35239
  }
@@ -34986,7 +35241,7 @@ function writePermanentDeletionJournal(prior, next) {
34986
35241
  fsyncDirectory(parent);
34987
35242
  } catch (firstError) {
34988
35243
  assertOwnerOnlyFile(target);
34989
- const reopened = parseJournal(body.workspace_slug, readFileSync16(target, "utf8"));
35244
+ const reopened = parseJournal(body.workspace_slug, readFileSync17(target, "utf8"));
34990
35245
  if (reopened.revision !== body.revision || checksum(reopened) !== checksum(body)) {
34991
35246
  throw new PermanentDeletionJournalError("journal changed after rename", "durability_indeterminate");
34992
35247
  }
@@ -35485,7 +35740,7 @@ async function pushFilesUpsertLocked(args) {
35485
35740
  state: next
35486
35741
  };
35487
35742
  }
35488
- bytes = readFileSync17(absolutePath);
35743
+ bytes = readFileSync18(absolutePath);
35489
35744
  hash2 = sha256Bytes(bytes);
35490
35745
  inode = st.ino;
35491
35746
  }
@@ -35747,12 +36002,64 @@ async function pushFilesDelete(args) {
35747
36002
  }
35748
36003
 
35749
36004
  // src/lib/push.ts
35750
- var import_gray_matter10 = __toESM(require_gray_matter(), 1);
35751
- 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";
35752
36006
  import { basename as basename5, relative as relative5 } from "node:path";
35753
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
+
35754
36062
  // src/lib/frontmatter-id.ts
35755
- var import_gray_matter8 = __toESM(require_gray_matter(), 1);
35756
36063
  function eolOf(content) {
35757
36064
  return /\r\n/.test(content) ? `\r
35758
36065
  ` : `
@@ -35789,158 +36096,16 @@ function withFrontmatterId(raw, parsed, id) {
35789
36096
  }
35790
36097
  function frontmatterIdWriteIsSound(next, id, expectedBody) {
35791
36098
  try {
35792
- const check2 = import_gray_matter8.default(next);
36099
+ const check2 = matter(next);
35793
36100
  return check2.data?.["id"] === id && check2.content === expectedBody;
35794
36101
  } catch {
35795
36102
  return false;
35796
36103
  }
35797
36104
  }
35798
36105
 
35799
- // src/lib/pull-write.ts
35800
- var import_gray_matter9 = __toESM(require_gray_matter(), 1);
35801
- import { readFileSync as readFileSync18 } from "node:fs";
35802
- function computeMirrorHashes(strategy, content) {
35803
- const parsed = import_gray_matter9.default(content);
35804
- const fm = parsed.data ?? {};
35805
- const lifted = splitUserFrontmatter(strategy, fm);
35806
- const userFm = userFrontmatter(strategy, lifted.frontmatter);
35807
- const hashBody = hashableBody(strategy, parsed.content);
35808
- return {
35809
- bodyHash: stableHash(hashBody),
35810
- frontmatterHash: stableHash({ ...userFm, ...lifted.topLevel }),
35811
- bodyLen: hashBody.length,
35812
- frontmatterId: fm["id"]
35813
- };
35814
- }
35815
- function hashableBody(strategy, body) {
35816
- return strategy.stripLocalBlocks?.(body) ?? body;
35817
- }
35818
- function relocateForSlugChange(args) {
35819
- const { workspaceSlug, strategy, fromSlug, toSlug, acked, echo, now } = args;
35820
- if (fromSlug === toSlug)
35821
- return { kind: "not_needed", writeSlug: toSlug };
35822
- if (!echo) {
35823
- return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "unguarded" };
35824
- }
35825
- if (strategy.mirrorRelativePath(fromSlug).split("/").length !== 2) {
35826
- return { kind: "skipped", writeSlug: toSlug, fromSlug, reason: "foldered" };
35827
- }
35828
- let fromPath;
35829
- try {
35830
- fromPath = entityFilePath(workspaceSlug, strategy.type, fromSlug);
35831
- entityFilePath(workspaceSlug, strategy.type, toSlug);
35832
- } catch (err) {
35833
- if (err instanceof MirrorPathError) {
35834
- return {
35835
- kind: "skipped",
35836
- writeSlug: toSlug,
35837
- fromSlug,
35838
- reason: "unresolvable"
35839
- };
35840
- }
35841
- throw err;
35842
- }
35843
- let raw = null;
35844
- try {
35845
- raw = readFileSync18(fromPath, "utf8");
35846
- } catch (err) {
35847
- const code = err?.code;
35848
- if (code === "ENOENT") {
35849
- return { kind: "absent", writeSlug: toSlug, fromSlug };
35850
- }
35851
- return {
35852
- kind: "refused",
35853
- writeSlug: fromSlug,
35854
- fromSlug,
35855
- reason: "source_unreadable"
35856
- };
35857
- }
35858
- let diverged = true;
35859
- try {
35860
- const hashes = computeMirrorHashes(strategy, raw);
35861
- diverged = hashes.bodyHash !== acked.bodyHash || hashes.frontmatterHash !== acked.frontmatterHash;
35862
- } catch {
35863
- diverged = true;
35864
- }
35865
- const stashPath = diverged ? stashConflict({
35866
- workspaceSlug,
35867
- entityType: strategy.type,
35868
- entitySlug: fromSlug,
35869
- timestamp: now(),
35870
- contents: raw,
35871
- side: "local"
35872
- }) : undefined;
35873
- try {
35874
- let registered = false;
35875
- const result = moveEntityFile(workspaceSlug, strategy.type, fromSlug, toSlug, () => {
35876
- if (echo.watcher === "live") {
35877
- echo.suppressor.registerDelete(fromPath);
35878
- registered = true;
35879
- }
35880
- });
35881
- if (result === "moved") {
35882
- return { kind: "moved", writeSlug: toSlug, fromSlug, stashPath };
35883
- }
35884
- if (registered && echo.watcher === "live") {
35885
- echo.suppressor.cancelDelete(fromPath);
35886
- }
35887
- return { kind: "absent", writeSlug: toSlug, fromSlug };
35888
- } catch (err) {
35889
- if (err instanceof MirrorDestinationOccupiedError) {
35890
- return {
35891
- kind: "refused",
35892
- writeSlug: fromSlug,
35893
- fromSlug,
35894
- reason: "destination_occupied",
35895
- stashPath
35896
- };
35897
- }
35898
- if (err instanceof MirrorPathError) {
35899
- return {
35900
- kind: "skipped",
35901
- writeSlug: toSlug,
35902
- fromSlug,
35903
- reason: "unresolvable"
35904
- };
35905
- }
35906
- throw err;
35907
- }
35908
- }
35909
- function writeMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, suppressor, pathSlug) {
35910
- const targetSlug = pathSlug ?? doc2.slug;
35911
- const content = renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug);
35912
- if (suppressor) {
35913
- const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
35914
- suppressor.registerWrite(target, computeMirrorHashes(strategy, content));
35915
- }
35916
- writeMirrorFile(workspaceSlug, strategy.type, targetSlug, content);
35917
- return content;
35918
- }
35919
- function renderMirrorWithBodyPreserve(workspaceSlug, strategy, doc2, targetSlug) {
35920
- let content = strategy.serialise(doc2);
35921
- if (strategy.preserveLocalBodyOnPull) {
35922
- const target = entityFilePath(workspaceSlug, strategy.type, targetSlug);
35923
- let existing = null;
35924
- try {
35925
- existing = readFileSync18(target, "utf8");
35926
- } catch (err) {
35927
- if (err?.code !== "ENOENT")
35928
- throw err;
35929
- }
35930
- if (existing !== null) {
35931
- const existingParsed = import_gray_matter9.default(existing);
35932
- const cloudParsed = import_gray_matter9.default(content);
35933
- const stitched = import_gray_matter9.default.stringify(existingParsed.content, cloudParsed.data);
35934
- content = stitched.endsWith(`
35935
- `) ? stitched : stitched + `
35936
- `;
35937
- }
35938
- }
35939
- return content;
35940
- }
35941
-
35942
36106
  // src/lib/push.ts
35943
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";
35944
36109
  function deleteDeclinedSentence(relPath) {
35945
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.`;
35946
36111
  }
@@ -36049,8 +36214,8 @@ function stampStateFromMirror(args) {
36049
36214
  const { strategy, workspaceSlug, doc: doc2, state, priorMemoryHash } = args;
36050
36215
  const targetSlug = args.pathSlug ?? doc2.slug;
36051
36216
  const mirrorPath = entityFilePath(workspaceSlug, strategy.type, targetSlug);
36052
- const mirrorRaw = readFileSync19(mirrorPath, "utf8");
36053
- const mirrorParsed = import_gray_matter10.default(mirrorRaw);
36217
+ const mirrorRaw = readFileSync20(mirrorPath, "utf8");
36218
+ const mirrorParsed = matter(mirrorRaw);
36054
36219
  const mirrorFm = mirrorParsed.data ?? {};
36055
36220
  const mirrorLifted = splitUserFrontmatter(strategy, mirrorFm);
36056
36221
  const mirrorUserFm = userFrontmatter(strategy, mirrorLifted.frontmatter);
@@ -36202,7 +36367,7 @@ async function pushFile(args) {
36202
36367
  }
36203
36368
  let raw;
36204
36369
  try {
36205
- raw = readFileSync19(absolutePath, "utf8");
36370
+ raw = readFileSync20(absolutePath, "utf8");
36206
36371
  } catch (err) {
36207
36372
  if (err?.code === "ENOENT") {
36208
36373
  return runPushDelete({
@@ -36222,23 +36387,32 @@ async function pushFile(args) {
36222
36387
  }
36223
36388
  throw err;
36224
36389
  }
36225
- let parsed;
36226
- try {
36227
- parsed = import_gray_matter10.default(raw);
36228
- } catch {
36229
- return {
36230
- outcome: { kind: "ignored", absolutePath, reason: "frontmatter parse error" },
36231
- state
36232
- };
36233
- }
36234
- const fm = parsed.data ?? {};
36235
- const slugFromFm = typeof fm.slug === "string" && fm.slug.length > 0 ? fm.slug : null;
36236
36390
  let parsedSlug = null;
36237
36391
  try {
36238
36392
  const rel = relative5(workspaceMirrorPath(workspaceSlug), absolutePath);
36239
36393
  parsedSlug = strategy.parseMirrorPath(rel)?.slug ?? null;
36240
36394
  } catch {}
36241
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;
36242
36416
  const slug = identity === "path" ? slugFromPath : slugFromFm ?? slugFromPath;
36243
36417
  if (!slug) {
36244
36418
  return {
@@ -36252,14 +36426,14 @@ async function pushFile(args) {
36252
36426
  if (strategy.classifyFieldErrors) {
36253
36427
  const desiredBody = spliceSyncErrorBlock(parsed.content, fieldErrors);
36254
36428
  if (desiredBody.replace(/\s+$/, "") !== parsed.content.replace(/\s+$/, "")) {
36255
- let rewritten = import_gray_matter10.default.stringify(desiredBody, parsed.data ?? {});
36429
+ let rewritten = stringifyFrontmatter(desiredBody, parsed.data ?? {});
36256
36430
  if (!rewritten.endsWith(`
36257
36431
  `))
36258
36432
  rewritten += `
36259
36433
  `;
36260
36434
  writeMirrorFile(workspaceSlug, entityType, slug, rewritten);
36261
36435
  raw = rewritten;
36262
- parsed = import_gray_matter10.default(rewritten);
36436
+ parsed = parseUncached(rewritten);
36263
36437
  }
36264
36438
  }
36265
36439
  const liveInode = safeInode2(absolutePath);
@@ -36537,7 +36711,7 @@ async function runPushUpsert(args) {
36537
36711
  }
36538
36712
  const wireProjection = (md) => {
36539
36713
  try {
36540
- const p = import_gray_matter10.default(md);
36714
+ const p = matter(md);
36541
36715
  const f = p.data ?? {};
36542
36716
  const l = splitUserFrontmatter(strategy, f);
36543
36717
  const u = userFrontmatter(strategy, l.frontmatter);
@@ -36552,7 +36726,7 @@ async function runPushUpsert(args) {
36552
36726
  if (liveEntry !== snapshotEntry) {
36553
36727
  let onDisk = null;
36554
36728
  try {
36555
- onDisk = readFileSync19(args.absolutePath, "utf8");
36729
+ onDisk = readFileSync20(args.absolutePath, "utf8");
36556
36730
  } catch {
36557
36731
  onDisk = null;
36558
36732
  }
@@ -37014,7 +37188,7 @@ function writeCreatedIdBack(args) {
37014
37188
  if (entityFilePath(args.workspaceSlug, args.strategy.type, args.slug) !== args.absolutePath) {
37015
37189
  return;
37016
37190
  }
37017
- const readBack = args.readBack ?? ((p) => readFileSync19(p, "utf8"));
37191
+ const readBack = args.readBack ?? ((p) => readFileSync20(p, "utf8"));
37018
37192
  if (readBack(args.absolutePath) !== args.raw)
37019
37193
  return;
37020
37194
  args.suppressor?.registerWrite(args.absolutePath, computeMirrorHashes(args.strategy, write.content));
@@ -37486,8 +37660,8 @@ async function pushRename(args) {
37486
37660
  };
37487
37661
  if (!updated.body_hash || !updated.frontmatter_hash) {
37488
37662
  try {
37489
- const mirrorRaw = readFileSync19(toPath, "utf8");
37490
- const mirrorParsed = import_gray_matter10.default(mirrorRaw);
37663
+ const mirrorRaw = readFileSync20(toPath, "utf8");
37664
+ const mirrorParsed = matter(mirrorRaw);
37491
37665
  const renameFm = mirrorParsed.data ?? {};
37492
37666
  const renameLifted = splitUserFrontmatter(strategy, renameFm);
37493
37667
  const renameUserFm = userFrontmatter(strategy, renameLifted.frontmatter);
@@ -37523,7 +37697,7 @@ async function pushRename(args) {
37523
37697
  if (patchResult.reason === "slug_conflict") {
37524
37698
  let contents2 = "";
37525
37699
  try {
37526
- contents2 = readFileSync19(toPath, "utf8");
37700
+ contents2 = readFileSync20(toPath, "utf8");
37527
37701
  } catch {}
37528
37702
  const stashPath2 = stashConflict({
37529
37703
  workspaceSlug,
@@ -37571,7 +37745,7 @@ async function pushRename(args) {
37571
37745
  }
37572
37746
  let contents = "";
37573
37747
  try {
37574
- contents = readFileSync19(toPath, "utf8");
37748
+ contents = readFileSync20(toPath, "utf8");
37575
37749
  } catch {}
37576
37750
  const stashPath = stashConflict({
37577
37751
  workspaceSlug,
@@ -37704,7 +37878,7 @@ async function discardBatchOps(batch, deps) {
37704
37878
  const path5 = opPath(deps.workspaceSlug, op);
37705
37879
  let local = null;
37706
37880
  try {
37707
- local = readFileSync20(path5, "utf8");
37881
+ local = readFileSync21(path5, "utf8");
37708
37882
  } catch {
37709
37883
  local = null;
37710
37884
  }
@@ -37743,7 +37917,7 @@ async function discardFileOp(op, deps) {
37743
37917
  const abs = filesSubtreePath(deps.workspaceSlug, op.path);
37744
37918
  let local = null;
37745
37919
  try {
37746
- local = readFileSync20(abs);
37920
+ local = readFileSync21(abs);
37747
37921
  } catch {
37748
37922
  local = null;
37749
37923
  }
@@ -37780,7 +37954,7 @@ function opPath(workspaceSlug, op) {
37780
37954
  }
37781
37955
 
37782
37956
  // src/lib/files/classify.ts
37783
- import { relative as relative6, sep as sep5 } from "node:path";
37957
+ import { relative as relative6, sep as sep6 } from "node:path";
37784
37958
  var FILES_SUBTREE = "files";
37785
37959
  function classifyFilesPath(args) {
37786
37960
  let root;
@@ -37790,20 +37964,20 @@ function classifyFilesPath(args) {
37790
37964
  return null;
37791
37965
  }
37792
37966
  const rel = relative6(root, args.absolutePath);
37793
- if (rel.startsWith("..") || rel.startsWith(sep5))
37967
+ if (rel.startsWith("..") || rel.startsWith(sep6))
37794
37968
  return null;
37795
- const prefix = FILES_SUBTREE + sep5;
37969
+ const prefix = FILES_SUBTREE + sep6;
37796
37970
  if (!rel.startsWith(prefix))
37797
37971
  return null;
37798
37972
  const sub = rel.slice(prefix.length);
37799
37973
  if (sub.length === 0)
37800
37974
  return null;
37801
- const posix = sep5 === "/" ? sub : sub.split(sep5).join("/");
37975
+ const posix = sep6 === "/" ? sub : sub.split(sep6).join("/");
37802
37976
  return validateFilesRelPath(posix) ? { relPath: posix } : null;
37803
37977
  }
37804
37978
 
37805
37979
  // src/lib/memory-facts/sync.ts
37806
- 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";
37807
37981
 
37808
37982
  // src/lib/memory-facts/api.ts
37809
37983
  var FactCommonSchema = exports_external.object({
@@ -38547,7 +38721,7 @@ function classifyMemoryFactPath(args) {
38547
38721
  const rel = relative7(root, args.absolutePath);
38548
38722
  if (rel === "" || rel.startsWith("..") || isAbsolute2(rel))
38549
38723
  return null;
38550
- const mirrorRel = `${MEMORY_FACTS_DIR}/${rel.split(sep6).join("/")}`;
38724
+ const mirrorRel = `${MEMORY_FACTS_DIR}/${rel.split(sep7).join("/")}`;
38551
38725
  return { relPath: mirrorRel, fact: parseMemoryFactRelPath(mirrorRel) };
38552
38726
  }
38553
38727
  function classifyMemoryFactEvent(args) {
@@ -38644,7 +38818,7 @@ function evaluateFileBatch(ops, population, thresholds = defaultThresholds()) {
38644
38818
  }
38645
38819
  };
38646
38820
  }
38647
- return { tripped: false };
38821
+ return evaluateLoneOverwrite(count, ops.map((op) => op.kind === "overwrite" ? op.priorSize - op.newSize : 0), thresholds.loneMinRemovedBytes);
38648
38822
  }
38649
38823
  function dedupeFileOps(ops) {
38650
38824
  const byKey = new Map;
@@ -38715,7 +38889,7 @@ class FileDestructiveGate {
38715
38889
  }
38716
38890
 
38717
38891
  // src/lib/files/reconcile.ts
38718
- import { readFileSync as readFileSync21, statSync as statSync10 } from "node:fs";
38892
+ import { readFileSync as readFileSync22, statSync as statSync10 } from "node:fs";
38719
38893
 
38720
38894
  // src/lib/unsynced.ts
38721
38895
  import { join as join14, relative as relative8 } from "node:path";
@@ -39711,7 +39885,7 @@ class NodeFsHandler {
39711
39885
  this._addToNodeFs(path5, initialAdd, wh, depth + 1);
39712
39886
  }
39713
39887
  }).on(EV.ERROR, this._boundHandleError);
39714
- return new Promise((resolve5, reject) => {
39888
+ return new Promise((resolve6, reject) => {
39715
39889
  if (!stream)
39716
39890
  return reject();
39717
39891
  stream.once(STR_END, () => {
@@ -39720,7 +39894,7 @@ class NodeFsHandler {
39720
39894
  return;
39721
39895
  }
39722
39896
  const wasThrottled = throttler ? throttler.clear() : false;
39723
- resolve5(undefined);
39897
+ resolve6(undefined);
39724
39898
  previous.getChildren().filter((item) => {
39725
39899
  return item !== directory && !current.has(item);
39726
39900
  }).forEach((item) => {
@@ -40479,7 +40653,7 @@ var chokidar_default = { watch, FSWatcher };
40479
40653
 
40480
40654
  // src/lib/watcher.ts
40481
40655
  import { statSync as statSync9 } from "node:fs";
40482
- import { sep as sep7 } from "node:path";
40656
+ import { sep as sep8 } from "node:path";
40483
40657
 
40484
40658
  // src/lib/rename-classifier.ts
40485
40659
  class RenameClassifier {
@@ -40610,7 +40784,7 @@ function watcherIgnoreMatcher(workspaceRoot) {
40610
40784
  return (absolutePath) => patterns.some((re) => re.test(absolutePath));
40611
40785
  }
40612
40786
  function escSep() {
40613
- return sep7 === "/" ? "/" : "\\\\";
40787
+ return sep8 === "/" ? "/" : "\\\\";
40614
40788
  }
40615
40789
 
40616
40790
  class DebounceMap {
@@ -40754,7 +40928,7 @@ function readBlobForPush(args) {
40754
40928
  }
40755
40929
  let bytes;
40756
40930
  try {
40757
- bytes = readFileSync21(absolutePath);
40931
+ bytes = readFileSync22(absolutePath);
40758
40932
  } catch (err) {
40759
40933
  if (err?.code === "ENOENT")
40760
40934
  return { kind: "gone" };
@@ -40897,7 +41071,7 @@ async function reconcileFilesSubtree(args) {
40897
41071
  ops: pendingOps
40898
41072
  });
40899
41073
  quarantined = ops.length;
40900
- 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]`);
40901
41075
  } else {
40902
41076
  for (const c of guttingCandidates) {
40903
41077
  await pushDivergedBlob(c.op.relPath, c.op.absolutePath, c.prepared);
@@ -41064,7 +41238,7 @@ function applyArtistMemoryOnPull(args) {
41064
41238
  const cloudMemory = live.memory;
41065
41239
  const generated = live.memory_generated === true;
41066
41240
  const local = readArtistMemoryFile(workspaceSlug, slug);
41067
- const refuse = (localContent, canonical2) => {
41241
+ const refuse2 = (localContent, canonical2) => {
41068
41242
  const refused = refuseGeneratedMemoryEdit({
41069
41243
  state,
41070
41244
  workspaceSlug,
@@ -41093,7 +41267,7 @@ function applyArtistMemoryOnPull(args) {
41093
41267
  const cloudHash = stableHash(cloudMemory);
41094
41268
  if (cloudHash === ackedMemoryHash && local !== null) {
41095
41269
  if (generated && stableHash(local) !== ackedMemoryHash) {
41096
- refuse(local, cloudMemory);
41270
+ refuse2(local, cloudMemory);
41097
41271
  return { memoryHash: cloudHash, state };
41098
41272
  }
41099
41273
  if (generated) {
@@ -41105,7 +41279,7 @@ function applyArtistMemoryOnPull(args) {
41105
41279
  const localHash = stableHash(local);
41106
41280
  if (localHash !== ackedMemoryHash && localHash !== cloudHash) {
41107
41281
  if (generated) {
41108
- refuse(local, cloudMemory);
41282
+ refuse2(local, cloudMemory);
41109
41283
  return { memoryHash: cloudHash, state };
41110
41284
  }
41111
41285
  const stashPath = stashConflict({
@@ -41815,7 +41989,7 @@ function anyUnknown(state) {
41815
41989
  }
41816
41990
 
41817
41991
  // src/lib/memory/write.ts
41818
- import { readFileSync as readFileSync22, statSync as statSync11 } from "node:fs";
41992
+ import { readFileSync as readFileSync23, statSync as statSync11 } from "node:fs";
41819
41993
 
41820
41994
  // src/lib/files/extracted-text.ts
41821
41995
  import { createHash as createHash5 } from "node:crypto";
@@ -41896,7 +42070,6 @@ function extractedTextHash(rendered) {
41896
42070
  }
41897
42071
 
41898
42072
  // src/lib/memory/splice.ts
41899
- var import_gray_matter11 = __toESM(require_gray_matter(), 1);
41900
42073
  var OPEN_RE = /^---\r?\n/;
41901
42074
  function findFrontmatter(content) {
41902
42075
  const open2 = OPEN_RE.exec(content);
@@ -41982,7 +42155,7 @@ function spliceAnnotation(content, lines) {
41982
42155
  }
41983
42156
  function parsesAsFrontmatter(content) {
41984
42157
  try {
41985
- import_gray_matter11.default(content, { cache: false });
42158
+ matter(content, { cache: false });
41986
42159
  return true;
41987
42160
  } catch {
41988
42161
  return false;
@@ -42010,7 +42183,7 @@ function writeDocumentAnnotation(args) {
42010
42183
  }
42011
42184
  let current;
42012
42185
  try {
42013
- current = readFileSync22(target, "utf8");
42186
+ current = readFileSync23(target, "utf8");
42014
42187
  } catch {
42015
42188
  return { kind: "absent" };
42016
42189
  }
@@ -42048,18 +42221,32 @@ function writeFileTextAnnotation(args) {
42048
42221
  const current = readExtractedText(args.workspaceSlug, args.relPath);
42049
42222
  if (current === null)
42050
42223
  return { kind: "absent" };
42051
- 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)) {
42052
42228
  return { kind: "diverged", relPath: extractedTextRelPath(args.relPath) };
42053
42229
  }
42054
42230
  const spliced = spliceAnnotation(current, args.lines);
42055
- if (!spliced.changed)
42056
- return { kind: "unchanged" };
42231
+ if (!spliced.changed) {
42232
+ return offAck ? { kind: "unchanged", textHash: onDisk } : { kind: "unchanged" };
42233
+ }
42057
42234
  writeExtractedText(args.workspaceSlug, args.relPath, spliced.content);
42058
42235
  return { kind: "written", textHash: extractedTextHash(spliced.content) };
42059
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
+ }
42060
42244
  function divergedNotice(relPath, state) {
42061
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.`;
42062
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
+ }
42063
42250
  function unreadableNotice(relPath, why) {
42064
42251
  const reason = why.replace(/\s+/g, " ").trim().slice(0, 120);
42065
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.`;
@@ -42089,6 +42276,10 @@ function heldFileTexts(state) {
42089
42276
  textHash: f.text_hash
42090
42277
  }));
42091
42278
  }
42279
+ function priorFileLines(state, id) {
42280
+ return getMemorySourceEntries(state, "workspace_files")[id]?.rendered.split(`
42281
+ `);
42282
+ }
42092
42283
  async function writeChanged(args) {
42093
42284
  const docs = new Map(heldDocuments(args.state).map((d) => [d.id, d.slug]));
42094
42285
  const texts = new Map(heldFileTexts(args.state).map((f) => [f.id, f]));
@@ -42188,7 +42379,8 @@ async function writeChanged(args) {
42188
42379
  workspaceSlug: args.workspaceSlug,
42189
42380
  relPath: file2.relPath,
42190
42381
  textHash: args.freshTextHashes?.get(answer2.id) ?? file2.textHash,
42191
- lines: answer2.lines
42382
+ lines: answer2.lines,
42383
+ priorLines: priorFileLines(args.state, answer2.id)
42192
42384
  });
42193
42385
  if (res.kind !== "diverged" && res.kind !== "absent") {
42194
42386
  args.onWritten?.("workspace_files", answer2.id, res.textHash);
@@ -42215,7 +42407,7 @@ async function writeChanged(args) {
42215
42407
  }
42216
42408
  const textPath = extractedTextRelPath(file2.relPath);
42217
42409
  if (outcome.kind === "diverged") {
42218
- report.push({ path: textPath, record: divergedNotice(textPath, answer2.entry.state) });
42410
+ report.push({ path: textPath, record: generatedDivergedNotice(textPath) });
42219
42411
  } else {
42220
42412
  clear.push(textPath);
42221
42413
  }
@@ -42224,9 +42416,11 @@ async function writeChanged(args) {
42224
42416
  if (outcome.textHash !== undefined) {
42225
42417
  textHashes.set(file2.relPath, { to: outcome.textHash });
42226
42418
  }
42419
+ } else if (outcome.kind === "unchanged" && outcome.textHash !== undefined) {
42420
+ textHashes.set(file2.relPath, { to: outcome.textHash });
42227
42421
  } else if (outcome.kind === "diverged") {
42228
42422
  diverged++;
42229
- notices.push(divergedNotice(outcome.relPath, answer2.entry.state));
42423
+ notices.push(generatedDivergedNotice(outcome.relPath));
42230
42424
  }
42231
42425
  }
42232
42426
  if (args.abortIfStale?.() === true) {
@@ -43948,8 +44142,7 @@ class PullSuppressor {
43948
44142
  }
43949
44143
 
43950
44144
  // src/lib/discover-entities.ts
43951
- var import_gray_matter12 = __toESM(require_gray_matter(), 1);
43952
- import { readFileSync as readFileSync23, statSync as statSync12 } from "node:fs";
44145
+ import { readFileSync as readFileSync24, statSync as statSync12 } from "node:fs";
43953
44146
  var DISCOVERY_WRITE_CAP_DEFAULT = 10;
43954
44147
  function discoveryWriteCap() {
43955
44148
  const raw = process.env.PLINTH_DISCOVERY_WRITE_CAP;
@@ -43976,6 +44169,11 @@ function classifyUntrackedEntity(args) {
43976
44169
  }
43977
44170
  return { kind: "create" };
43978
44171
  }
44172
+ function holdsUnreadableUntracked(args) {
44173
+ if (args.readable)
44174
+ return false;
44175
+ return classifyUntrackedEntity({ ...args, frontmatter: {} }).kind === "create";
44176
+ }
43979
44177
  function classifyRenameDestination(args) {
43980
44178
  const occupant = getEntryBySlug(args.state, args.entityType, args.entitySlug);
43981
44179
  if (occupant && occupant.id !== args.movedEntryId) {
@@ -44046,13 +44244,20 @@ function planDiscoveryWrites(args) {
44046
44244
  if (getEntryBySlug(state, entityType, entitySlug))
44047
44245
  continue;
44048
44246
  counts.discovered++;
44049
- let frontmatter;
44247
+ let raw;
44050
44248
  try {
44051
- frontmatter = import_gray_matter12.default(readFileSync23(absolutePath, "utf8")).data ?? {};
44249
+ raw = readFileSync24(absolutePath, "utf8");
44052
44250
  } catch {
44053
44251
  counts.discovered--;
44054
44252
  continue;
44055
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 : {};
44056
44261
  const verdict = classifyUntrackedEntity({ entityType, entitySlug, frontmatter });
44057
44262
  if (verdict.kind === "hold") {
44058
44263
  if (verdict.held === "artist")
@@ -44093,7 +44298,7 @@ async function discoverUntrackedEntities(args) {
44093
44298
  }
44094
44299
  const cap = discoveryWriteCap();
44095
44300
  const creates = planned.filter((w) => w.kind === "create");
44096
- 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;
44097
44302
  if (creates.length > cap) {
44098
44303
  counts.heldCap = creates.length;
44099
44304
  output?.log(`[DISCOVERY_HELD reason=cap creates=${creates.length} cap=${cap} source=entity-discovery]`);
@@ -44116,6 +44321,19 @@ async function runPlannedWrites(args) {
44116
44321
  let state = args.state;
44117
44322
  for (const write of planned) {
44118
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
+ }
44119
44337
  if (write.kind === "rename") {
44120
44338
  state = await runDiscoveredRename({
44121
44339
  write,
@@ -44276,8 +44494,7 @@ function recordDiscoveryFailure(args) {
44276
44494
  }
44277
44495
 
44278
44496
  // src/lib/frontmatter-id-backfill.ts
44279
- var import_gray_matter13 = __toESM(require_gray_matter(), 1);
44280
- import { readFileSync as readFileSync24, statSync as statSync13 } from "node:fs";
44497
+ import { readFileSync as readFileSync25, statSync as statSync13 } from "node:fs";
44281
44498
  var FRONTMATTER_ID_BACKFILL_VERSION = 1;
44282
44499
  function backfillWorthReporting(r) {
44283
44500
  return r.written > 0 || r.failed > 0 || r.skipped.foreignId > 0 || r.skipped.unwritable > 0 || r.skipped.slugCollision > 0;
@@ -44320,7 +44537,7 @@ function safeInode3(path5) {
44320
44537
  function backfillFrontmatterIds(args) {
44321
44538
  const { workspaceSlug, state } = args;
44322
44539
  const compose = args.compose ?? withFrontmatterId;
44323
- const readRaw = args.readFile ?? ((p) => readFileSync24(p, "utf8"));
44540
+ const readRaw = args.readFile ?? ((p) => readFileSync25(p, "utf8"));
44324
44541
  const result = emptyResult();
44325
44542
  if (state.frontmatter_id_backfill_version === FRONTMATTER_ID_BACKFILL_VERSION) {
44326
44543
  return result;
@@ -44369,7 +44586,7 @@ function backfillFrontmatterIds(args) {
44369
44586
  let rawBody;
44370
44587
  let parsedData;
44371
44588
  try {
44372
- const parsed = import_gray_matter13.default(raw);
44589
+ const parsed = parseUncached(raw);
44373
44590
  parsedData = parsed.data ?? {};
44374
44591
  existing = parsedData["id"];
44375
44592
  rawBody = parsed.content;
@@ -44448,11 +44665,11 @@ function backfillFrontmatterIds(args) {
44448
44665
  }
44449
44666
 
44450
44667
  // src/lib/reconcile.ts
44451
- import { readFileSync as readFileSync25, statSync as statSync14 } from "node:fs";
44668
+ import { readFileSync as readFileSync26, statSync as statSync14 } from "node:fs";
44452
44669
  async function reconcileDivergedRows(args) {
44453
44670
  const { workspaceSlug, baseUrl, pat, fetchImpl, output, isFrozen } = args;
44454
44671
  const now = args.now ?? (() => new Date);
44455
- let state = args.state;
44672
+ let state = settleFrontmatterHolds(args.state, workspaceSlug);
44456
44673
  let scanned = 0;
44457
44674
  let diverged = 0;
44458
44675
  let pushed = 0;
@@ -44560,7 +44777,7 @@ async function reconcileDivergedRows(args) {
44560
44777
  }
44561
44778
  let content;
44562
44779
  try {
44563
- content = readFileSync25(absolutePath, "utf8");
44780
+ content = readFileSync26(absolutePath, "utf8");
44564
44781
  } catch (err) {
44565
44782
  if (err?.code === "ENOENT") {
44566
44783
  state = clearEntityCloudRowDeleted(state, strategy.mirrorRelativePath(entry2.slug));
@@ -44568,6 +44785,20 @@ async function reconcileDivergedRows(args) {
44568
44785
  }
44569
44786
  throw err;
44570
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
+ }
44571
44802
  const hashes = computeMirrorHashes(strategy, content);
44572
44803
  if (hashes.frontmatterId === undefined)
44573
44804
  sawTrackedFileWithoutId = true;
@@ -44618,7 +44849,7 @@ async function reconcileDivergedRows(args) {
44618
44849
  ops: pendingOps
44619
44850
  });
44620
44851
  quarantined = ops.length;
44621
- 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]`);
44622
44853
  } else {
44623
44854
  for (const c of guttingCandidates) {
44624
44855
  const op = c.op;
@@ -45849,7 +46080,7 @@ class Semaphore {
45849
46080
  this.active += 1;
45850
46081
  return;
45851
46082
  }
45852
- await new Promise((resolve6) => this.waiters.push(resolve6));
46083
+ await new Promise((resolve7) => this.waiters.push(resolve7));
45853
46084
  }
45854
46085
  release() {
45855
46086
  const next = this.waiters.shift();
@@ -45911,9 +46142,9 @@ function createThrottledFetch(baseFetch, opts = {}) {
45911
46142
  // src/commands/sync.ts
45912
46143
  import {
45913
46144
  existsSync as existsSync9,
45914
- lstatSync as lstatSync6,
46145
+ lstatSync as lstatSync7,
45915
46146
  readdirSync as readdirSync7,
45916
- readFileSync as readFileSync29,
46147
+ readFileSync as readFileSync30,
45917
46148
  statSync as statSync17,
45918
46149
  unlinkSync as unlinkSync9
45919
46150
  } from "node:fs";
@@ -46097,17 +46328,17 @@ function refreshConflictsView(workspaceSlug) {
46097
46328
  }
46098
46329
 
46099
46330
  // src/lib/files/pull.ts
46100
- 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";
46101
46332
  import { dirname as dirname13 } from "node:path";
46102
46333
 
46103
46334
  // src/lib/permanent-disposition.ts
46104
46335
  import { createHash as createHash6 } from "node:crypto";
46105
- import { readFileSync as readFileSync26 } from "node:fs";
46336
+ import { readFileSync as readFileSync27 } from "node:fs";
46106
46337
  function sha256(value) {
46107
46338
  return createHash6("sha256").update(value).digest("hex");
46108
46339
  }
46109
46340
  function verifyStash(path5, expectedHash) {
46110
- const actual = sha256(readFileSync26(path5));
46341
+ const actual = sha256(readFileSync27(path5));
46111
46342
  if (actual !== expectedHash) {
46112
46343
  throw new Error(`permanent deletion stash verification failed for ${path5}`);
46113
46344
  }
@@ -46151,7 +46382,7 @@ function disposePermanentDocumentLocked(args) {
46151
46382
  const target = entityFilePath(args.workspaceSlug, args.strategy.type, retiredEntry?.slug ?? t.logicalKey);
46152
46383
  let raw;
46153
46384
  try {
46154
- raw = readFileSync26(target, "utf8");
46385
+ raw = readFileSync27(target, "utf8");
46155
46386
  } catch (error52) {
46156
46387
  if (error52?.code !== "ENOENT")
46157
46388
  throw error52;
@@ -46249,7 +46480,7 @@ function disposePermanentFileLocked(args) {
46249
46480
  const target = filesSubtreePath(args.workspaceSlug, t.logicalKey);
46250
46481
  let bytes;
46251
46482
  try {
46252
- bytes = readFileSync26(target);
46483
+ bytes = readFileSync27(target);
46253
46484
  } catch (error52) {
46254
46485
  if (error52?.code !== "ENOENT")
46255
46486
  throw error52;
@@ -46741,7 +46972,7 @@ async function applyFileRow(args) {
46741
46972
  }
46742
46973
  let localBytes;
46743
46974
  try {
46744
- localBytes = readFileSync27(abs);
46975
+ localBytes = readFileSync28(abs);
46745
46976
  } catch {
46746
46977
  localBytes = undefined;
46747
46978
  }
@@ -46958,7 +47189,7 @@ function stashUnpushedLocalEdit(args) {
46958
47189
  return { reason: "symlink" };
46959
47190
  let st;
46960
47191
  try {
46961
- st = lstatSync5(abs);
47192
+ st = lstatSync6(abs);
46962
47193
  } catch (err) {
46963
47194
  if (err?.code === "ENOENT")
46964
47195
  return null;
@@ -46968,7 +47199,7 @@ function stashUnpushedLocalEdit(args) {
46968
47199
  return { reason: "not_a_file" };
46969
47200
  let localBytes;
46970
47201
  try {
46971
- localBytes = readFileSync27(abs);
47202
+ localBytes = readFileSync28(abs);
46972
47203
  } catch (err) {
46973
47204
  return { reason: "unreadable", code: errCode(err) };
46974
47205
  }
@@ -47018,7 +47249,7 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
47018
47249
  }
47019
47250
 
47020
47251
  // src/lib/files/rejection-sync.ts
47021
- import { readFileSync as readFileSync28, statSync as statSync16 } from "node:fs";
47252
+ import { readFileSync as readFileSync29, statSync as statSync16 } from "node:fs";
47022
47253
 
47023
47254
  // src/lib/files/rejections-api.ts
47024
47255
  var RejectionsFeedSchema = exports_external.object({
@@ -47151,7 +47382,7 @@ function settledDisposition(args) {
47151
47382
  return null;
47152
47383
  let bytes;
47153
47384
  try {
47154
- bytes = readFileSync28(absolute2);
47385
+ bytes = readFileSync29(absolute2);
47155
47386
  } catch {
47156
47387
  return null;
47157
47388
  }
@@ -47267,7 +47498,7 @@ function unlinkIfProvablyOurs(workspaceSlug, strategy, entry2, suppressor) {
47267
47498
  }
47268
47499
  let raw;
47269
47500
  try {
47270
- raw = readFileSync29(target, "utf8");
47501
+ raw = readFileSync30(target, "utf8");
47271
47502
  } catch (err) {
47272
47503
  if (err?.code === "ENOENT") {
47273
47504
  return { didUnlink: false };
@@ -48144,13 +48375,16 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
48144
48375
  if (!suppressor && existingEntry && existingEntry.updated_at !== live.updated_at && relocation.kind !== "moved") {
48145
48376
  let localRaw = null;
48146
48377
  try {
48147
- localRaw = readFileSync29(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
48378
+ localRaw = readFileSync30(entityFilePath(workspaceSlug, strategy.type, writeSlug), "utf8");
48148
48379
  } catch {
48149
48380
  localRaw = null;
48150
48381
  }
48151
48382
  if (localRaw !== null) {
48152
- const localHashes = computeMirrorHashes(strategy, localRaw);
48153
- 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
+ }
48154
48388
  if (diverged) {
48155
48389
  const stashPath = stashConflict({
48156
48390
  workspaceSlug,
@@ -48168,7 +48402,7 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
48168
48402
  const target = entityFilePath(workspaceSlug, strategy.type, writeSlug);
48169
48403
  let localRaw = null;
48170
48404
  try {
48171
- localRaw = lstatSync6(target).isFile() ? readFileSync29(target, "utf8") : null;
48405
+ localRaw = lstatSync7(target).isFile() ? readFileSync30(target, "utf8") : null;
48172
48406
  } catch {
48173
48407
  localRaw = null;
48174
48408
  }
@@ -48334,7 +48568,7 @@ function backfillStateInodes(workspaceSlug, state, statFn = (p) => statSync18(p)
48334
48568
  return { state, changed: false };
48335
48569
  return { state: { ...state, entries: nextEntries }, changed: true };
48336
48570
  }
48337
- function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync30(p, "utf8")) {
48571
+ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync31(p, "utf8")) {
48338
48572
  let changed = false;
48339
48573
  const nextEntries = {};
48340
48574
  for (const [entityType, bucket] of Object.entries(state.entries)) {
@@ -48346,7 +48580,7 @@ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync3
48346
48580
  }
48347
48581
  try {
48348
48582
  const path5 = entityFilePath(workspaceSlug, entityType, entry2.slug);
48349
- const bodyLen = import_gray_matter14.default(readFn(path5)).content.length;
48583
+ const bodyLen = parseUncached(readFn(path5)).content.length;
48350
48584
  nextBucket[id] = { ...entry2, body_len: bodyLen };
48351
48585
  changed = true;
48352
48586
  } catch {
@@ -48359,6 +48593,18 @@ function backfillBodyLengths(workspaceSlug, state, readFn = (p) => readFileSync3
48359
48593
  return { state, changed: false };
48360
48594
  return { state: { ...state, entries: nextEntries }, changed: true };
48361
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
+ }
48362
48608
  function makeWatchHandler(deps) {
48363
48609
  const {
48364
48610
  workspaceSlug,
@@ -48475,7 +48721,7 @@ function makeWatchHandler(deps) {
48475
48721
  spinner.update(`Watching ${workspaceSlug} · ${watchingSummary(getState())}`);
48476
48722
  return;
48477
48723
  }
48478
- const bytes = readFileSync30(absolutePath);
48724
+ const bytes = readFileSync31(absolutePath);
48479
48725
  const hash2 = sha256Bytes(bytes);
48480
48726
  if (suppressor?.shouldSuppressUpsert(absolutePath, {
48481
48727
  bodyHash: hash2,
@@ -48539,7 +48785,7 @@ function makeWatchHandler(deps) {
48539
48785
  if (suppressor) {
48540
48786
  let raw = null;
48541
48787
  try {
48542
- raw = readFileSync30(absolutePath, "utf8");
48788
+ raw = readFileSync31(absolutePath, "utf8");
48543
48789
  } catch {
48544
48790
  raw = null;
48545
48791
  }
@@ -48578,7 +48824,7 @@ function makeWatchHandler(deps) {
48578
48824
  if (suppressor) {
48579
48825
  let raw = null;
48580
48826
  try {
48581
- raw = readFileSync30(absolutePath, "utf8");
48827
+ raw = readFileSync31(absolutePath, "utf8");
48582
48828
  } catch {
48583
48829
  raw = null;
48584
48830
  }
@@ -48617,7 +48863,7 @@ function makeWatchHandler(deps) {
48617
48863
  if (suppressor) {
48618
48864
  let raw = null;
48619
48865
  try {
48620
- raw = readFileSync30(absolutePath, "utf8");
48866
+ raw = readFileSync31(absolutePath, "utf8");
48621
48867
  } catch {
48622
48868
  raw = null;
48623
48869
  }
@@ -48656,7 +48902,7 @@ function makeWatchHandler(deps) {
48656
48902
  if (suppressor) {
48657
48903
  let raw = null;
48658
48904
  try {
48659
- raw = readFileSync30(absolutePath, "utf8");
48905
+ raw = readFileSync31(absolutePath, "utf8");
48660
48906
  } catch {
48661
48907
  raw = null;
48662
48908
  }
@@ -48695,7 +48941,7 @@ function makeWatchHandler(deps) {
48695
48941
  if (suppressor) {
48696
48942
  let raw = null;
48697
48943
  try {
48698
- raw = readFileSync30(absolutePath, "utf8");
48944
+ raw = readFileSync31(absolutePath, "utf8");
48699
48945
  } catch {
48700
48946
  raw = null;
48701
48947
  }
@@ -48741,11 +48987,11 @@ function makeWatchHandler(deps) {
48741
48987
  const strategy = getStrategy(classifiedFrom.entityType);
48742
48988
  let toRaw = null;
48743
48989
  try {
48744
- toRaw = readFileSync30(event.toPath, "utf8");
48990
+ toRaw = readFileSync31(event.toPath, "utf8");
48745
48991
  } catch {
48746
48992
  toRaw = null;
48747
48993
  }
48748
- 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);
48749
48995
  if (isEcho) {
48750
48996
  output?.log(`[RENAME_SUPPRESSED entityType=${classifiedFrom.entityType} ` + `from=${event.fromSlug} id=${event.id} reason=pull_echo]`);
48751
48997
  return;
@@ -48753,6 +48999,9 @@ function makeWatchHandler(deps) {
48753
48999
  await handle({ kind: "upsert", absolutePath: event.toPath });
48754
49000
  return;
48755
49001
  }
49002
+ if (destinationIsUnreadable(event.toPath)) {
49003
+ await handle({ kind: "upsert", absolutePath: event.toPath });
49004
+ }
48756
49005
  const base3 = getState();
48757
49006
  const result2 = await pushRename({
48758
49007
  workspaceSlug,
@@ -48810,7 +49059,7 @@ function makeWatchHandler(deps) {
48810
49059
  if (event.kind === "upsert") {
48811
49060
  try {
48812
49061
  ownEcho = suppressor?.shouldSuppressUpsert(absolutePath, {
48813
- bodyHash: stableHash(readFileSync30(absolutePath, "utf8")),
49062
+ bodyHash: stableHash(readFileSync31(absolutePath, "utf8")),
48814
49063
  frontmatterHash: ""
48815
49064
  }) === true;
48816
49065
  } catch {
@@ -48865,11 +49114,11 @@ function makeWatchHandler(deps) {
48865
49114
  if (strategy) {
48866
49115
  let raw = null;
48867
49116
  try {
48868
- raw = readFileSync30(absolutePath, "utf8");
49117
+ raw = readFileSync31(absolutePath, "utf8");
48869
49118
  } catch {
48870
49119
  raw = null;
48871
49120
  }
48872
- if (raw !== null && suppressor.shouldSuppressUpsert(absolutePath, computeMirrorHashes(strategy, raw))) {
49121
+ if (raw !== null && isOwnWriteEcho(suppressor, absolutePath, strategy, raw)) {
48873
49122
  return;
48874
49123
  }
48875
49124
  }
@@ -48920,7 +49169,8 @@ function makeWatchHandler(deps) {
48920
49169
  echo: suppressor ? { watcher: "live", suppressor } : undefined
48921
49170
  }
48922
49171
  });
48923
- 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));
48924
49174
  reportOutcome(spinner, workspaceSlug, getState(), result.outcome);
48925
49175
  } catch (err) {
48926
49176
  const msg = err instanceof Error ? err.message : "unknown push error";
@@ -49468,7 +49718,7 @@ async function runStart(workspaceSlug, deps = {}) {
49468
49718
  return;
49469
49719
  }
49470
49720
  refreshFrozen();
49471
- 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)}]`);
49472
49722
  spinner.warn(`Held ${batch.length} destructive op${batch.length === 1 ? "" : "s"} pending confirm → ${stashPath}. Run \`plinth confirm\` to review.`);
49473
49723
  spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
49474
49724
  }
@@ -49524,7 +49774,7 @@ async function runStart(workspaceSlug, deps = {}) {
49524
49774
  return;
49525
49775
  }
49526
49776
  refreshFrozen();
49527
- 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]`);
49528
49778
  spinner.warn(`Held ${batch.length} destructive file op${batch.length === 1 ? "" : "s"} pending confirm → ${stashPath}. Run \`plinth confirm\` to review.`);
49529
49779
  spinner.update(`Watching ${slug} · ${watchingSummary(state)}`);
49530
49780
  }
@@ -49616,6 +49866,9 @@ async function runStart(workspaceSlug, deps = {}) {
49616
49866
  afterPoll: () => {
49617
49867
  (async () => {
49618
49868
  state = loadState(slug);
49869
+ const unsettled = state;
49870
+ const settled = settleFrontmatterHolds(unsettled, slug);
49871
+ state = rebaseState(state, unsettled, settled);
49619
49872
  if (draining)
49620
49873
  return;
49621
49874
  draining = true;
@@ -49837,12 +50090,12 @@ async function runStart(workspaceSlug, deps = {}) {
49837
50090
  try {
49838
50091
  await Promise.race([
49839
50092
  watcher.drainPending().then(() => Promise.all([gate.drain(), fileGate.drain()])),
49840
- new Promise((resolve6) => setTimeout(() => resolve6(-1), SHUTDOWN_DRAIN_TIMEOUT_MS))
50093
+ new Promise((resolve7) => setTimeout(() => resolve7(-1), SHUTDOWN_DRAIN_TIMEOUT_MS))
49841
50094
  ]);
49842
50095
  } finally {
49843
50096
  await Promise.race([
49844
50097
  watcher.stop(),
49845
- new Promise((resolve6) => setTimeout(resolve6, SHUTDOWN_DRAIN_TIMEOUT_MS))
50098
+ new Promise((resolve7) => setTimeout(resolve7, SHUTDOWN_DRAIN_TIMEOUT_MS))
49846
50099
  ]);
49847
50100
  clearDaemonPid(slug);
49848
50101
  releaseDaemonLock(lockHandle);
@@ -50397,7 +50650,7 @@ function assessContextWalls(workspaceSlug, deps = {}) {
50397
50650
 
50398
50651
  // src/lib/build-stamp.ts
50399
50652
  import { execFileSync as execFileSync4 } from "node:child_process";
50400
- 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";
50401
50654
  import { dirname as dirname14, join as join19 } from "node:path";
50402
50655
  import { fileURLToPath } from "node:url";
50403
50656
  var BUILD_STAMP_FILENAME = "build-stamp.json";
@@ -50421,7 +50674,7 @@ function readHead(repoDir) {
50421
50674
  }
50422
50675
  function readBuildStamp(distDir) {
50423
50676
  try {
50424
- const parsed = JSON.parse(readFileSync31(stampPath(distDir), "utf8"));
50677
+ const parsed = JSON.parse(readFileSync32(stampPath(distDir), "utf8"));
50425
50678
  if (parsed && typeof parsed.sha === "string" && parsed.sha.length > 0 && typeof parsed.dirty === "boolean" && typeof parsed.built_at === "string") {
50426
50679
  return { sha: parsed.sha, dirty: parsed.dirty, built_at: parsed.built_at };
50427
50680
  }
@@ -50482,12 +50735,12 @@ function checkBuildStaleness(deps = {}) {
50482
50735
  }
50483
50736
 
50484
50737
  // src/lib/memory-links.ts
50485
- import { readFileSync as readFileSync32 } from "node:fs";
50486
- 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";
50487
50740
  var MEMORY_FILE_RE = /^(workspace-memory\.md|user-memory\.md|artists\/[^/]+\/memory\.md|memory\/[^/]+\.md|memory\/private\/[^/]+\.md)$/i;
50488
50741
  function scanMemoryLinks(deps) {
50489
50742
  const listFiles = deps.listFiles ?? ((r) => walkSubtreeFiles(r, isVcsInternal));
50490
- const readFile = deps.readFile ?? ((p) => readFileSync32(p, "utf8"));
50743
+ const readFile = deps.readFile ?? ((p) => readFileSync33(p, "utf8"));
50491
50744
  let root;
50492
50745
  let absPaths;
50493
50746
  try {
@@ -50496,7 +50749,7 @@ function scanMemoryLinks(deps) {
50496
50749
  } catch {
50497
50750
  return { scanned: 0, unreadable: [], dead: [] };
50498
50751
  }
50499
- const relPaths = absPaths.map((p) => relative15(root, p).split(sep8).join("/"));
50752
+ const relPaths = absPaths.map((p) => relative15(root, p).split(sep9).join("/"));
50500
50753
  const artists = buildArtistIndex(relPaths, root, (abs) => readFile(abs));
50501
50754
  const dead = [];
50502
50755
  const unreadable = [];
@@ -50531,9 +50784,8 @@ function scanMemoryLinks(deps) {
50531
50784
  init_output();
50532
50785
 
50533
50786
  // src/lib/pending-writes.ts
50534
- import { readFileSync as readFileSync33, statSync as statSync19 } from "node:fs";
50787
+ import { readFileSync as readFileSync34, statSync as statSync19 } from "node:fs";
50535
50788
  import { relative as relative16 } from "node:path";
50536
- var import_gray_matter15 = __toESM(require_gray_matter(), 1);
50537
50789
  var EMPTY3 = { writes: [], count: 0 };
50538
50790
  function reportedElsewhere(state) {
50539
50791
  const entities = new Set;
@@ -50579,7 +50831,7 @@ function scanPendingWrites(args) {
50579
50831
  continue;
50580
50832
  let hash2;
50581
50833
  try {
50582
- hash2 = sha256Bytes(readFileSync33(absolutePath));
50834
+ hash2 = sha256Bytes(readFileSync34(absolutePath));
50583
50835
  } catch {
50584
50836
  continue;
50585
50837
  }
@@ -50610,17 +50862,15 @@ function scanPendingWrites(args) {
50610
50862
  continue;
50611
50863
  let content;
50612
50864
  try {
50613
- content = readFileSync33(absolutePath, "utf8");
50865
+ content = readFileSync34(absolutePath, "utf8");
50614
50866
  } catch {
50615
50867
  continue;
50616
50868
  }
50617
50869
  if (!entry2) {
50618
- let frontmatter;
50619
- try {
50620
- frontmatter = import_gray_matter15.default(content).data ?? {};
50621
- } catch {
50870
+ const read = readFrontmatter(content);
50871
+ if (holdsUnreadableUntracked({ entityType, entitySlug, readable: read.ok }))
50622
50872
  continue;
50623
- }
50873
+ const frontmatter = read.ok ? read.parsed.data : {};
50624
50874
  const verdict = classifyUntrackedEntity({
50625
50875
  entityType,
50626
50876
  entitySlug,
@@ -51076,7 +51326,7 @@ async function runStatus(args, deps = {}) {
51076
51326
  }
51077
51327
  }
51078
51328
  if (freshness.length > 0) {
51079
- 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.`));
51080
51330
  for (const r of freshness.slice(0, MAX_SHOWN)) {
51081
51331
  out.info(cont(r.message));
51082
51332
  }
@@ -51521,7 +51771,7 @@ var versionCommand = defineCommand({
51521
51771
  });
51522
51772
 
51523
51773
  // src/commands/voice-gate.ts
51524
- import { readFileSync as readFileSync36 } from "node:fs";
51774
+ import { readFileSync as readFileSync37 } from "node:fs";
51525
51775
 
51526
51776
  // src/lib/voice-gate/api.ts
51527
51777
  var VoiceGateSurfaces = [
@@ -51626,7 +51876,7 @@ async function callVoiceGate(req) {
51626
51876
  import { createHash as createHash7 } from "node:crypto";
51627
51877
  import {
51628
51878
  mkdirSync as mkdirSync13,
51629
- readFileSync as readFileSync34,
51879
+ readFileSync as readFileSync35,
51630
51880
  renameSync as renameSync9,
51631
51881
  unlinkSync as unlinkSync10,
51632
51882
  writeFileSync as writeFileSync9
@@ -51657,7 +51907,7 @@ function emptyFile() {
51657
51907
  }
51658
51908
  function load2(workspaceSlug) {
51659
51909
  try {
51660
- const raw = readFileSync34(attemptsPath(workspaceSlug), "utf8");
51910
+ const raw = readFileSync35(attemptsPath(workspaceSlug), "utf8");
51661
51911
  const parsed = AttemptsFileSchema.safeParse(JSON.parse(raw));
51662
51912
  return parsed.success ? parsed.data : emptyFile();
51663
51913
  } catch {
@@ -51820,9 +52070,9 @@ function denyUngated(detail) {
51820
52070
  }
51821
52071
 
51822
52072
  // src/lib/voice-gate/payload.ts
51823
- import { readFileSync as readFileSync35, statSync as statSync21 } from "node:fs";
52073
+ import { readFileSync as readFileSync36, statSync as statSync21 } from "node:fs";
51824
52074
  import { homedir as homedir2 } from "node:os";
51825
- import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
52075
+ import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
51826
52076
  var MAX_BODY_BYTES = 256 * 1024;
51827
52077
  var HELPER_BASENAME = "push_gmail_draft.py";
51828
52078
  var DRAFT_FLAGS = ["--to", "--body-file", "--body-stdin"];
@@ -51981,7 +52231,7 @@ function defaultReadBodyFile(path5) {
51981
52231
  if (stat4.size > MAX_BODY_BYTES) {
51982
52232
  throw new Error(`draft body is ${stat4.size} bytes, over the ${MAX_BODY_BYTES}-byte gate limit`);
51983
52233
  }
51984
- return readFileSync35(path5, "utf8");
52234
+ return readFileSync36(path5, "utf8");
51985
52235
  }
51986
52236
  function parseRecipients(...raw) {
51987
52237
  const all = raw.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
@@ -52016,13 +52266,13 @@ function applyCd(tokens, state) {
52016
52266
  return { dir: expanded, known: true };
52017
52267
  if (!state.known)
52018
52268
  return state;
52019
- return { dir: resolve6(state.dir, expanded), known: true };
52269
+ return { dir: resolve7(state.dir, expanded), known: true };
52020
52270
  }
52021
52271
  function expandHome(path5) {
52022
52272
  if (path5 === "~")
52023
52273
  return homedir2();
52024
52274
  if (path5.startsWith("~/"))
52025
- return resolve6(homedir2(), path5.slice(2));
52275
+ return resolve7(homedir2(), path5.slice(2));
52026
52276
  return path5;
52027
52277
  }
52028
52278
  function classifyHelperSegment(tokens, dirState, readBodyFile) {
@@ -52048,7 +52298,7 @@ function classifyHelperSegment(tokens, dirState, readBodyFile) {
52048
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.`
52049
52299
  };
52050
52300
  }
52051
- const resolved = isAbsolute4(expanded) ? expanded : resolve6(dirState.dir, expanded);
52301
+ const resolved = isAbsolute4(expanded) ? expanded : resolve7(dirState.dir, expanded);
52052
52302
  let body;
52053
52303
  try {
52054
52304
  body = readBodyFile(resolved);
@@ -52341,7 +52591,7 @@ async function runVoiceGateDirect(args, deps = {}) {
52341
52591
  let text;
52342
52592
  if (args.file) {
52343
52593
  try {
52344
- text = readFileSync36(args.file, "utf8");
52594
+ text = readFileSync37(args.file, "utf8");
52345
52595
  } catch (err) {
52346
52596
  const msg = err instanceof Error ? err.message : "unknown error";
52347
52597
  out.error(`Could not read ${args.file}: ${msg}`);