dravoice 0.1.3 → 0.2.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.
@@ -13,7 +13,7 @@ import {
13
13
  styleDistanceFromDiagnostics,
14
14
  } from "./stylometry.js";
15
15
 
16
- const MAX_ACTIONS = 8;
16
+ const MAX_ACTIONS = 10;
17
17
 
18
18
  const EDITABILITY = {
19
19
  evidence: 1.00,
@@ -36,11 +36,13 @@ export function revisePlanDraftV2({ file, voice, cwd = process.cwd(), maxActions
36
36
  const draftProfile = buildVoiceProfileV2({ documents: [draftDocument] });
37
37
  const familyDiagnostics = familyDiagnosticsFor(sourceProfile, draftProfile);
38
38
  const rollingWindows = rollingWindowsFor({ sourceProfile, draftDocument });
39
+ const paragraphWindows = paragraphWindowsFor({ sourceProfile, draftDocument });
39
40
  const actions = rankedActions({
40
41
  sourceProfile,
41
42
  draftDocument,
42
43
  familyDiagnostics,
43
44
  rollingWindows,
45
+ paragraphWindows,
44
46
  maxActions,
45
47
  });
46
48
 
@@ -61,6 +63,7 @@ export function revisePlanDraftV2({ file, voice, cwd = process.cwd(), maxActions
61
63
  familyDrift: Object.fromEntries(Object.entries(familyDiagnostics).map(([family, item]) => [family, item.drift])),
62
64
  thresholds: Object.fromEntries(Object.entries(familyDiagnostics).map(([family, item]) => [family, item.threshold])),
63
65
  rollingWindows,
66
+ paragraphWindows,
64
67
  },
65
68
  actions,
66
69
  };
@@ -81,7 +84,7 @@ export function renderRevisePlanV2(plan) {
81
84
 
82
85
  for (const [family, score] of Object.entries(plan.summary.familyScores)) {
83
86
  const drift = plan.summary.familyDrift[family];
84
- lines.push(`- ${family}: ${score} (drift ${drift})`);
87
+ lines.push(`- ${familyLabel(family)}: ${score} (drift ${drift})`);
85
88
  }
86
89
 
87
90
  lines.push("");
@@ -93,17 +96,17 @@ export function renderRevisePlanV2(plan) {
93
96
 
94
97
  lines.push("Start here:");
95
98
  plan.actions.forEach((action, index) => {
96
- lines.push(`${index + 1}. ${action.priority} ${action.family} ${action.id}`);
97
- lines.push(` Unit: ${action.unit.type} at line ${action.unit.line}`);
98
- lines.push(` Score: ${action.actionScore}`);
99
- lines.push(` Why flagged: ${action.why}`);
100
- lines.push(` Revise by: ${action.reviseBy}`);
99
+ lines.push(`${index + 1}. ${priorityLabel(action.priority)} ${familyLabel(action.family)}`);
100
+ lines.push(` Where: ${unitLabel(action.unit)}`);
101
+ lines.push(` Priority score: ${action.actionScore}`);
102
+ lines.push(` Why: ${action.why}`);
103
+ lines.push(` Do this: ${action.reviseBy}`);
101
104
  });
102
105
  lines.push("");
103
106
  return lines.join("\n");
104
107
  }
105
108
 
106
- function rankedActions({ sourceProfile, draftDocument, familyDiagnostics, rollingWindows, maxActions }) {
109
+ function rankedActions({ sourceProfile, draftDocument, familyDiagnostics, rollingWindows, paragraphWindows, maxActions }) {
107
110
  const confidence = confidenceWeight(sourceProfile.source.confidence.band);
108
111
  const actions = [
109
112
  ...evidenceActions({ sourceProfile, draftDocument, familyDiagnostics, confidence }),
@@ -111,6 +114,7 @@ function rankedActions({ sourceProfile, draftDocument, familyDiagnostics, rollin
111
114
  ...shapeActions({ sourceProfile, draftDocument, familyDiagnostics, confidence }),
112
115
  ...discourseActions({ sourceProfile, draftDocument, familyDiagnostics, confidence }),
113
116
  ...rollingWindowActions({ rollingWindows, confidence }),
117
+ ...paragraphWindowActions({ paragraphWindows, confidence }),
114
118
  ...documentLevelActions({ sourceProfile, draftDocument, familyDiagnostics, confidence }),
115
119
  ].filter((action) => action.actionScore > 0);
116
120
 
@@ -322,6 +326,41 @@ function rollingWindowsFor({ sourceProfile, draftDocument }) {
322
326
  .slice(0, 4);
323
327
  }
324
328
 
329
+ function paragraphWindowsFor({ sourceProfile, draftDocument }) {
330
+ const result = [];
331
+ for (const [index, paragraph] of draftDocument.paragraphs.entries()) {
332
+ const sentences = draftDocument.sentences.filter((sentence) =>
333
+ sentence.line >= paragraph.line &&
334
+ sentence.line <= (paragraph.lineNumbers?.at(-1) ?? paragraph.line)
335
+ );
336
+ if (sentences.length < 2) {
337
+ continue;
338
+ }
339
+ const paragraphProfile = buildVoiceProfileV2({ documents: [documentForParagraph(draftDocument, paragraph, sentences, index)] });
340
+ const diagnostics = familyDiagnosticsFor(sourceProfile, paragraphProfile);
341
+ const ranked = ["evidence", "rhythm", "discourse", "rhetoricalShape", "lexical"]
342
+ .map((family) => ({ family, ...diagnostics[family] }))
343
+ .sort((left, right) => right.drift - left.drift || (100 - right.score) - (100 - left.score));
344
+ const best = ranked[0];
345
+ if (best?.drift > 0) {
346
+ result.push({
347
+ family: best.family,
348
+ paragraph: index + 1,
349
+ startLine: paragraph.line,
350
+ endLine: paragraph.lineNumbers?.at(-1) ?? paragraph.line,
351
+ distance: best.distance,
352
+ drift: best.drift,
353
+ score: best.score,
354
+ threshold: best.threshold,
355
+ stability: best.stability,
356
+ });
357
+ }
358
+ }
359
+ return result
360
+ .sort((left, right) => right.drift - left.drift || left.startLine - right.startLine)
361
+ .slice(0, 4);
362
+ }
363
+
325
364
  function rollingWindowStarts(sentenceCount, windowSize, stride) {
326
365
  const starts = [];
327
366
  for (let start = 0; start <= sentenceCount - windowSize; start += stride) {
@@ -363,6 +402,29 @@ function documentForSentences(draftDocument, sentences, windowIndex) {
363
402
  };
364
403
  }
365
404
 
405
+ function documentForParagraph(draftDocument, paragraph, sentences, paragraphIndex) {
406
+ const block = {
407
+ type: paragraph.type,
408
+ line: paragraph.line,
409
+ heading: paragraph.heading,
410
+ headingId: paragraph.headingId,
411
+ headingDepth: 0,
412
+ lines: [paragraph.text],
413
+ lineNumbers: paragraph.lineNumbers ?? [paragraph.line],
414
+ };
415
+ return {
416
+ file: `${draftDocument.file ?? "draft"}#paragraph-${paragraphIndex + 1}`,
417
+ path: draftDocument.path,
418
+ headings: [],
419
+ sections: [{ heading: null, blocks: [block] }],
420
+ blocks: [block],
421
+ paragraphs: [paragraph],
422
+ sentences,
423
+ wordCount: sentences.reduce((sum, sentence) => sum + sentence.tokens.length, 0),
424
+ text: paragraph.text,
425
+ };
426
+ }
427
+
366
428
  function rollingWindowActions({ rollingWindows, confidence }) {
367
429
  return rollingWindows.map((window, index) => makeAction({
368
430
  family: window.family,
@@ -378,6 +440,21 @@ function rollingWindowActions({ rollingWindows, confidence }) {
378
440
  }));
379
441
  }
380
442
 
443
+ function paragraphWindowActions({ paragraphWindows, confidence }) {
444
+ return paragraphWindows.map((window, index) => makeAction({
445
+ family: window.family,
446
+ ordinal: `paragraph-${index + 1}`,
447
+ priority: window.family === "evidence" ? "review" : "consider",
448
+ unit: { type: "paragraph", line: window.startLine, endLine: window.endLine },
449
+ confidence,
450
+ drift: window.drift,
451
+ stability: window.stability,
452
+ localMismatch: Math.min(0.45, window.drift / Math.max(1, window.drift + 0.5)),
453
+ why: `Paragraph ${window.paragraph} shows localized ${window.family} drift beyond the writer's calibrated range.`,
454
+ reviseBy: rollingWindowReviseBy(window.family),
455
+ }));
456
+ }
457
+
381
458
  function rollingWindowReviseBy(family) {
382
459
  if (family === "evidence") {
383
460
  return "Add or move concrete support into this local passage, or narrow the unsupported claims in the same window.";
@@ -435,3 +512,29 @@ function resolvePath(cwd, value) {
435
512
  function capitalize(value) {
436
513
  return value.charAt(0).toUpperCase() + value.slice(1);
437
514
  }
515
+
516
+ function familyLabel(family) {
517
+ return {
518
+ evidence: "Evidence",
519
+ rhythm: "Rhythm",
520
+ rhetoricalShape: "Rhetorical shape",
521
+ discourse: "Discourse",
522
+ lexical: "Lexical style",
523
+ register: "Register",
524
+ structure: "Structure",
525
+ }[family] ?? capitalize(family);
526
+ }
527
+
528
+ function priorityLabel(priority) {
529
+ return priority === "review" ? "Review" : "Consider";
530
+ }
531
+
532
+ function unitLabel(unit) {
533
+ if (unit.type === "window") {
534
+ return `sentence window at lines ${unit.line}-${unit.endLine}`;
535
+ }
536
+ if (unit.type === "paragraph") {
537
+ return `paragraph at lines ${unit.line}-${unit.endLine}`;
538
+ }
539
+ return `${unit.type} at line ${unit.line}`;
540
+ }
@@ -159,9 +159,9 @@ function discourseDistance(source, draft) {
159
159
  const transitionDelta = rateMapDistance(source.transitionRates, draft.transitionRates);
160
160
  const callbackDelta = Math.abs((source.sentenceCallbacks ?? 0) - (draft.sentenceCallbacks ?? 0));
161
161
  return weightedMean([
162
- [transitionDelta, 0.55],
163
- [topItemDistance(source.transitionBigrams, draft.transitionBigrams), 0.25],
164
- [topItemDistance(source.transitionTrigrams, draft.transitionTrigrams), 0.10],
162
+ [transitionDelta, 0.65],
163
+ [topItemDistance(source.transitionBigrams, draft.transitionBigrams), 0.10],
164
+ [topItemDistance(source.transitionTrigrams, draft.transitionTrigrams), 0.025],
165
165
  [callbackDelta, 0.20],
166
166
  ]);
167
167
  }
@@ -188,10 +188,14 @@ function shapeDistance(source, draft) {
188
188
 
189
189
  function structureDistance(source, draft) {
190
190
  return weightedMean([
191
- [distributionDelta(source.sectionWords, draft.sectionWords), 0.35],
192
- [distributionDelta(source.headingCount, draft.headingCount), 0.20],
193
- [Math.abs((source.listDocumentRate ?? 0) - (draft.listDocumentRate ?? 0)), 0.18],
194
- [Math.abs((source.quoteDocumentRate ?? 0) - (draft.quoteDocumentRate ?? 0)), 0.12],
191
+ [distributionDelta(source.sectionWords, draft.sectionWords), 0.30],
192
+ [distributionDelta(source.headingCount, draft.headingCount), 0.16],
193
+ [distributionDelta(source.maxHeadingDepth, draft.maxHeadingDepth), 0.12],
194
+ [topItemDistance(source.sectionOrderPatterns, draft.sectionOrderPatterns), 0.12],
195
+ [topItemDistance(source.listPlacementPatterns, draft.listPlacementPatterns), 0.08],
196
+ [topItemDistance(source.quotePlacementPatterns, draft.quotePlacementPatterns), 0.08],
197
+ [Math.abs((source.listDocumentRate ?? 0) - (draft.listDocumentRate ?? 0)), 0.12],
198
+ [Math.abs((source.quoteDocumentRate ?? 0) - (draft.quoteDocumentRate ?? 0)), 0.08],
195
199
  [sequenceDistance(source.openingMoves, draft.openingMoves), 0.15],
196
200
  ]);
197
201
  }
@@ -75,17 +75,20 @@ export function characterNgrams(text, size = 3) {
75
75
 
76
76
  export function distribution(values) {
77
77
  if (!values.length) {
78
- return { count: 0, min: 0, max: 0, mean: 0, median: 0, p25: 0, p75: 0 };
78
+ return { count: 0, min: 0, max: 0, mean: 0, median: 0, p25: 0, p75: 0, stdev: 0 };
79
79
  }
80
80
  const sorted = [...values].sort((a, b) => a - b);
81
+ const mean = sorted.reduce((sum, value) => sum + value, 0) / sorted.length;
82
+ const variance = sorted.reduce((sum, value) => sum + (value - mean) ** 2, 0) / sorted.length;
81
83
  return {
82
84
  count: sorted.length,
83
85
  min: sorted[0],
84
86
  max: sorted[sorted.length - 1],
85
- mean: round(sorted.reduce((sum, value) => sum + value, 0) / sorted.length, 2),
87
+ mean: round(mean, 2),
86
88
  median: percentile(sorted, 0.5),
87
89
  p25: percentile(sorted, 0.25),
88
90
  p75: percentile(sorted, 0.75),
91
+ stdev: round(Math.sqrt(variance), 2),
89
92
  };
90
93
  }
91
94