openwriter 0.14.0 → 0.16.0

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.
Files changed (43) hide show
  1. package/dist/client/assets/index-CbSQ8xxn.css +1 -0
  2. package/dist/client/assets/index-JMMJM_G_.js +212 -0
  3. package/dist/client/index.html +2 -2
  4. package/dist/plugins/authors-voice/dist/index.d.ts +41 -0
  5. package/dist/plugins/authors-voice/dist/index.js +206 -0
  6. package/dist/plugins/authors-voice/package.json +23 -0
  7. package/dist/plugins/image-gen/dist/index.d.ts +35 -0
  8. package/dist/plugins/image-gen/dist/index.js +141 -0
  9. package/dist/plugins/image-gen/package.json +26 -0
  10. package/dist/plugins/publish/dist/helpers.d.ts +66 -0
  11. package/dist/plugins/publish/dist/helpers.js +199 -0
  12. package/dist/plugins/publish/dist/index.d.ts +3 -0
  13. package/dist/plugins/publish/dist/index.js +1130 -0
  14. package/dist/plugins/publish/dist/newsletter-tools.d.ts +2 -0
  15. package/dist/plugins/publish/dist/newsletter-tools.js +394 -0
  16. package/dist/plugins/publish/package.json +31 -0
  17. package/dist/plugins/x-api/dist/index.d.ts +27 -0
  18. package/dist/plugins/x-api/dist/index.js +240 -0
  19. package/dist/plugins/x-api/package.json +27 -0
  20. package/dist/server/comments.js +256 -0
  21. package/dist/server/documents.js +293 -20
  22. package/dist/server/enrichment.js +114 -0
  23. package/dist/server/helpers.js +63 -8
  24. package/dist/server/index.js +94 -40
  25. package/dist/server/install-skill.js +15 -0
  26. package/dist/server/logger.js +246 -0
  27. package/dist/server/markdown-parse.js +71 -14
  28. package/dist/server/markdown-serialize.js +136 -41
  29. package/dist/server/mcp.js +538 -99
  30. package/dist/server/node-blocks.js +22 -4
  31. package/dist/server/node-fingerprint.js +347 -73
  32. package/dist/server/node-matcher.js +76 -49
  33. package/dist/server/pending-overlay.js +862 -0
  34. package/dist/server/state.js +1178 -98
  35. package/dist/server/versions.js +18 -0
  36. package/dist/server/workspaces.js +42 -5
  37. package/dist/server/ws.js +194 -37
  38. package/package.json +1 -1
  39. package/skill/SKILL.md +51 -21
  40. package/skill/agents/openwriter-enrichment-minion.md +184 -0
  41. package/skill/docs/enrichment.md +179 -0
  42. package/dist/client/assets/index-BxI3DazW.js +0 -212
  43. package/dist/client/assets/index-OV13QtgQ.css +0 -1
@@ -19,14 +19,14 @@
19
19
  * - Insert (any block still unmatched → fresh ID)
20
20
  * Phase 3: orphans = previousNodes entries no rule claimed (= deletes)
21
21
  *
22
- * Fingerprints use math signals (per-sentence char count, 3-char prefix/suffix,
23
- * terminator, word-length sequence) plus full word arrays for math-collision
24
- * disambiguation. Documented in node-fingerprint.ts.
22
+ * Fingerprints carry one tuple per sentence: char count, content hash,
23
+ * terminator type. Hash equality identifies "same sentence text" in 8 bytes.
24
+ * Documented in node-fingerprint.ts.
25
25
  *
26
26
  * adr: adr/node-identity-matcher.md
27
27
  */
28
28
  import { generateNodeId } from './helpers.js';
29
- import { fingerprintAll, isExactMatch, isSameContent, sentenceArraysEqual, sentenceTuplesEqual, } from './node-fingerprint.js';
29
+ import { fingerprintAll, isExactMatch, isSameContent, sentenceArraysEqual, } from './node-fingerprint.js';
30
30
  /**
31
31
  * Run the matcher.
32
32
  *
@@ -51,9 +51,9 @@ export function matchNodes(previousNodes, newBlocks, options = {}) {
51
51
  applyMergeRule(unmatched, previousNodes, claimedPrevIds, pinned);
52
52
  applyTypeChangeRule(unmatched, previousNodes, claimedPrevIds, pinned);
53
53
  applyEditRule(unmatched, previousNodes, claimedPrevIds, pinned);
54
- applySlotContinuityRule(unmatched, previousNodes, claimedPrevIds, pinned);
54
+ applySlotContinuityRule(unmatched, previousNodes, claimedPrevIds, pinned, graveyard);
55
55
  applyGraveyardRestoreRule(unmatched, graveyard, claimedGraveIds, pinned);
56
- applyInsertRule(unmatched, pinned);
56
+ applyInsertRule(unmatched, pinned, previousNodes, claimedPrevIds, graveyard, claimedGraveIds);
57
57
  const orphaned = previousNodes
58
58
  .filter((prev) => !claimedPrevIds.has(prev.id))
59
59
  .map((prev) => ({ id: prev.id, fingerprint: prev.fingerprint }));
@@ -387,12 +387,31 @@ function applyEditRule(unmatched, previousNodes, claimedPrevIds, pinned) {
387
387
  // ----------------------------------------------------------------------
388
388
  // Slot-continuity fallback
389
389
  // ----------------------------------------------------------------------
390
- function applySlotContinuityRule(unmatched, previousNodes, claimedPrevIds, pinned) {
390
+ function applySlotContinuityRule(unmatched, previousNodes, claimedPrevIds, pinned, graveyard) {
391
391
  let progress = true;
392
392
  while (progress) {
393
393
  progress = false;
394
394
  for (let ui = 0; ui < unmatched.length; ui++) {
395
395
  const candidate = unmatched[ui];
396
+ // Skip candidates that carry an explicit ID already known to the
397
+ // identity graph (in previousNodes or graveyard). Such IDs are real
398
+ // signals — the load-time matcher's previous pin, a restored snapshot,
399
+ // or a paste-back from graveyard. Don't override them with positional
400
+ // guessing; let applyInsertRule preserve them (for previousNodes hits)
401
+ // or applyGraveyardRestoreRule restore them (for graveyard hits).
402
+ //
403
+ // Transient IDs (not in either set — e.g. a fresh ID typed in the
404
+ // editor by a user replacing a block in place) still go through
405
+ // slot-continuity per the "slot is innocent" principle.
406
+ //
407
+ // adr: adr/node-identity-matcher.md
408
+ if (candidate.block.id) {
409
+ const id = candidate.block.id;
410
+ const inPrev = previousNodes.some((p) => p.id === id);
411
+ const inGrave = graveyard.some((g) => g.id === id);
412
+ if (inPrev || inGrave)
413
+ continue;
414
+ }
396
415
  const matchingOrphans = previousNodes.filter((orphan) => {
397
416
  if (claimedPrevIds.has(orphan.id))
398
417
  return false;
@@ -440,36 +459,22 @@ function applySlotContinuityRule(unmatched, previousNodes, claimedPrevIds, pinne
440
459
  }
441
460
  }
442
461
  /**
443
- * Lightweight content overlap signal used by slot-continuity scoring.
444
- * Per sentence-pair: +1 f, +1 l, +1 t, +2 wls-equal, +3×shared-words,
445
- * +10 full word-array equality. Word-level overlap is the disambiguator
446
- * when math signals collide.
462
+ * Lightweight content overlap signal used by slot-continuity scoring to
463
+ * disambiguate between multiple candidate orphans in the same slot range.
464
+ *
465
+ * Per sentence pair across both blocks: +1 for each hash that appears in
466
+ * both arrays. Since hashes fold sentence text + terminator together, this
467
+ * counts the number of fully-shared sentences between the two blocks — the
468
+ * matcher's only meaningful similarity question.
447
469
  */
448
470
  function sentenceSignalOverlapScore(a, b) {
449
471
  if (!a.sentences || !b.sentences)
450
472
  return 0;
473
+ const seen = new Set(a.sentences);
451
474
  let score = 0;
452
- for (const sa of a.sentences) {
453
- for (const sb of b.sentences) {
454
- if (sa.f === sb.f)
455
- score++;
456
- if (sa.l === sb.l)
457
- score++;
458
- if (sa.t === sb.t)
459
- score++;
460
- if (arraysEqual(sa.wls, sb.wls))
461
- score += 2;
462
- if (Array.isArray(sa.w) && Array.isArray(sb.w)) {
463
- const aSet = new Set(sa.w);
464
- let shared = 0;
465
- for (const w of sb.w)
466
- if (aSet.has(w))
467
- shared++;
468
- score += shared * 3;
469
- if (arraysEqual(sa.w, sb.w))
470
- score += 10;
471
- }
472
- }
475
+ for (const h of b.sentences) {
476
+ if (seen.has(h))
477
+ score++;
473
478
  }
474
479
  return score;
475
480
  }
@@ -506,11 +511,44 @@ function applyGraveyardRestoreRule(unmatched, graveyard, claimedGraveIds, pinned
506
511
  // ----------------------------------------------------------------------
507
512
  // Insert rule (last resort)
508
513
  // ----------------------------------------------------------------------
509
- function applyInsertRule(unmatched, pinned) {
514
+ function applyInsertRule(unmatched, pinned, previousNodes, claimedPrevIds, graveyard, claimedGraveIds) {
510
515
  for (let i = unmatched.length - 1; i >= 0; i--) {
511
516
  const candidate = unmatched[i];
517
+ // Preserve an existing block ID if the TipTap node already carried one
518
+ // (e.g. agent-assigned via applyChangesToDocument, or freshly minted by
519
+ // the doc-update path). Minting a new ID here would diverge the server
520
+ // copy from the browser copy — the browser still has the original ID,
521
+ // so subsequent updates targeting the new ID can't be resolved and the
522
+ // browser's autosave later overwrites server state with stale content.
523
+ //
524
+ // If the preserved ID also lives in previousNodes or the graveyard, we
525
+ // must claim it from those sets so the same ID doesn't simultaneously
526
+ // appear in pinned AND in orphaned/remaining-graveyard. Without claiming,
527
+ // bb000001 would end up listed in both `nodes:` and `graveyard:` in the
528
+ // output frontmatter — the matcher's identity invariant says an ID lives
529
+ // in exactly one place.
530
+ //
531
+ // If the preserved ID is already claimed (another block earlier in this
532
+ // pass took it, e.g. an exact-match), fall back to a fresh ID to keep IDs
533
+ // globally unique.
534
+ //
535
+ // adr: adr/node-identity-matcher.md
536
+ let id;
537
+ const preservedId = candidate.block.id;
538
+ if (preservedId &&
539
+ !claimedPrevIds.has(preservedId) &&
540
+ !claimedGraveIds.has(preservedId)) {
541
+ id = preservedId;
542
+ if (previousNodes.some((p) => p.id === preservedId))
543
+ claimedPrevIds.add(preservedId);
544
+ if (graveyard.some((g) => g.id === preservedId))
545
+ claimedGraveIds.add(preservedId);
546
+ }
547
+ else {
548
+ id = generateNodeId();
549
+ }
512
550
  pinned.push({
513
- id: generateNodeId(),
551
+ id,
514
552
  position: candidate.position,
515
553
  fingerprint: candidate.fingerprint,
516
554
  block: candidate.block,
@@ -544,21 +582,10 @@ function slotHighBound(previousNodes, claimedPrevIds, pinned, orphanIdx) {
544
582
  function shareAnySentenceTuple(a, b) {
545
583
  if (!Array.isArray(a) || !Array.isArray(b))
546
584
  return false;
547
- for (const sa of a) {
548
- for (const sb of b) {
549
- if (sentenceTuplesEqual(sa, sb))
550
- return true;
551
- }
585
+ const seen = new Set(a);
586
+ for (const sb of b) {
587
+ if (seen.has(sb))
588
+ return true;
552
589
  }
553
590
  return false;
554
591
  }
555
- function arraysEqual(a, b) {
556
- if (!Array.isArray(a) || !Array.isArray(b))
557
- return false;
558
- if (a.length !== b.length)
559
- return false;
560
- for (let i = 0; i < a.length; i++)
561
- if (a[i] !== b[i])
562
- return false;
563
- return true;
564
- }