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.
- package/dist/client/assets/index-CbSQ8xxn.css +1 -0
- package/dist/client/assets/index-JMMJM_G_.js +212 -0
- package/dist/client/index.html +2 -2
- package/dist/plugins/authors-voice/dist/index.d.ts +41 -0
- package/dist/plugins/authors-voice/dist/index.js +206 -0
- package/dist/plugins/authors-voice/package.json +23 -0
- package/dist/plugins/image-gen/dist/index.d.ts +35 -0
- package/dist/plugins/image-gen/dist/index.js +141 -0
- package/dist/plugins/image-gen/package.json +26 -0
- package/dist/plugins/publish/dist/helpers.d.ts +66 -0
- package/dist/plugins/publish/dist/helpers.js +199 -0
- package/dist/plugins/publish/dist/index.d.ts +3 -0
- package/dist/plugins/publish/dist/index.js +1130 -0
- package/dist/plugins/publish/dist/newsletter-tools.d.ts +2 -0
- package/dist/plugins/publish/dist/newsletter-tools.js +394 -0
- package/dist/plugins/publish/package.json +31 -0
- package/dist/plugins/x-api/dist/index.d.ts +27 -0
- package/dist/plugins/x-api/dist/index.js +240 -0
- package/dist/plugins/x-api/package.json +27 -0
- package/dist/server/comments.js +256 -0
- package/dist/server/documents.js +293 -20
- package/dist/server/enrichment.js +114 -0
- package/dist/server/helpers.js +63 -8
- package/dist/server/index.js +94 -40
- package/dist/server/install-skill.js +15 -0
- package/dist/server/logger.js +246 -0
- package/dist/server/markdown-parse.js +71 -14
- package/dist/server/markdown-serialize.js +136 -41
- package/dist/server/mcp.js +538 -99
- package/dist/server/node-blocks.js +22 -4
- package/dist/server/node-fingerprint.js +347 -73
- package/dist/server/node-matcher.js +76 -49
- package/dist/server/pending-overlay.js +862 -0
- package/dist/server/state.js +1178 -98
- package/dist/server/versions.js +18 -0
- package/dist/server/workspaces.js +42 -5
- package/dist/server/ws.js +194 -37
- package/package.json +1 -1
- package/skill/SKILL.md +51 -21
- package/skill/agents/openwriter-enrichment-minion.md +184 -0
- package/skill/docs/enrichment.md +179 -0
- package/dist/client/assets/index-BxI3DazW.js +0 -212
- 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
|
|
23
|
-
* terminator
|
|
24
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
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
|
|
453
|
-
|
|
454
|
-
|
|
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
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
}
|