dravoice 0.1.3 → 0.1.4
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/README.md +32 -9
- package/package.json +1 -1
- package/src/index.js +106 -13
- package/src/v2/analyzers/discourse.js +7 -1
- package/src/v2/analyzers/evidence.js +3 -3
- package/src/v2/analyzers/register.js +28 -4
- package/src/v2/analyzers/rhetorical-shape.js +7 -1
- package/src/v2/analyzers/structure.js +18 -1
- package/src/v2/benchmark.js +83 -0
- package/src/v2/doctor.js +308 -0
- package/src/v2/document-model.js +77 -6
- package/src/v2/inspect.js +2 -2
- package/src/v2/profile.js +126 -11
- package/src/v2/review.js +142 -16
- package/src/v2/revise-plan.js +111 -8
- package/src/v2/stylometry.js +11 -7
package/src/v2/review.js
CHANGED
|
@@ -10,11 +10,11 @@ const REVIEW_MODES = {
|
|
|
10
10
|
exitOnDrift: false,
|
|
11
11
|
},
|
|
12
12
|
balanced: {
|
|
13
|
-
findingThresholds: { evidence: 65, rhythm: 55, rhetoricalShape: 50 },
|
|
13
|
+
findingThresholds: { evidence: 65, rhythm: 55, rhetoricalShape: 50, discourse: 55, lexical: 55, register: 55, structure: 55 },
|
|
14
14
|
exitOnDrift: false,
|
|
15
15
|
},
|
|
16
16
|
strict: {
|
|
17
|
-
findingThresholds: { evidence: 75, rhythm: 70, rhetoricalShape: 65 },
|
|
17
|
+
findingThresholds: { evidence: 75, rhythm: 70, rhetoricalShape: 65, discourse: 65, lexical: 65, register: 70, structure: 91 },
|
|
18
18
|
exitOnDrift: true,
|
|
19
19
|
},
|
|
20
20
|
};
|
|
@@ -34,6 +34,7 @@ export function reviewVoiceDraftV2({ file, voice, cwd = process.cwd(), mode = "b
|
|
|
34
34
|
draftDocument.wordCount < sourceProfile.calibration.minimumDraftSize.words ||
|
|
35
35
|
draftDocument.sentences.length < sourceProfile.calibration.minimumDraftSize.sentences
|
|
36
36
|
) {
|
|
37
|
+
const familyConfidence = familyConfidenceFor(sourceProfile, {});
|
|
37
38
|
return {
|
|
38
39
|
schemaVersion: 2,
|
|
39
40
|
file: displayPath(filePath, cwd),
|
|
@@ -41,6 +42,8 @@ export function reviewVoiceDraftV2({ file, voice, cwd = process.cwd(), mode = "b
|
|
|
41
42
|
mode: reviewMode,
|
|
42
43
|
fit: { band: "insufficient-evidence", distance: 0 },
|
|
43
44
|
familyScores: {},
|
|
45
|
+
familyConfidence,
|
|
46
|
+
suppressedFindings: suppressedFindingsFor(sourceProfile, familyConfidence),
|
|
44
47
|
corpusConfidence: sourceProfile.source.confidence,
|
|
45
48
|
},
|
|
46
49
|
findings: [],
|
|
@@ -51,7 +54,8 @@ export function reviewVoiceDraftV2({ file, voice, cwd = process.cwd(), mode = "b
|
|
|
51
54
|
const familyDiagnostics = familyDiagnosticsFor(sourceProfile, draftProfile);
|
|
52
55
|
const familyScores = Object.fromEntries(Object.entries(familyDiagnostics).map(([family, item]) => [family, item.score]));
|
|
53
56
|
const distance = styleDistanceFromDiagnostics(familyDiagnostics);
|
|
54
|
-
const
|
|
57
|
+
const familyConfidence = familyConfidenceFor(sourceProfile, familyDiagnostics);
|
|
58
|
+
const findings = reviewFindings(sourceProfile, draftProfile, familyScores, reviewMode, familyConfidence);
|
|
55
59
|
const fit = {
|
|
56
60
|
band: fitBand(distance, findings, familyDiagnostics),
|
|
57
61
|
distance,
|
|
@@ -64,6 +68,8 @@ export function reviewVoiceDraftV2({ file, voice, cwd = process.cwd(), mode = "b
|
|
|
64
68
|
mode: reviewMode,
|
|
65
69
|
fit,
|
|
66
70
|
familyScores,
|
|
71
|
+
familyConfidence,
|
|
72
|
+
suppressedFindings: suppressedFindingsFor(sourceProfile, familyConfidence),
|
|
67
73
|
familyDistances: Object.fromEntries(Object.entries(familyDiagnostics).map(([family, item]) => [family, item.distance])),
|
|
68
74
|
familyDrift: Object.fromEntries(Object.entries(familyDiagnostics).map(([family, item]) => [family, item.drift])),
|
|
69
75
|
thresholds: Object.fromEntries(Object.entries(familyDiagnostics).map(([family, item]) => [family, item.threshold])),
|
|
@@ -76,71 +82,175 @@ export function reviewVoiceDraftV2({ file, voice, cwd = process.cwd(), mode = "b
|
|
|
76
82
|
|
|
77
83
|
export function renderVoiceReviewV2(result) {
|
|
78
84
|
const lines = [
|
|
79
|
-
"Voice
|
|
85
|
+
"Voice review notes, not AI detection.",
|
|
80
86
|
"",
|
|
81
87
|
result.file,
|
|
82
|
-
`
|
|
88
|
+
`Fit: ${capitalize(result.summary.fit.band)} (${result.summary.fit.distance} style distance)`,
|
|
83
89
|
`Corpus confidence: ${capitalize(result.summary.corpusConfidence.band)} - ${result.summary.corpusConfidence.message}`,
|
|
84
90
|
"Family scores:",
|
|
85
91
|
];
|
|
86
92
|
|
|
87
93
|
for (const [family, score] of Object.entries(result.summary.familyScores)) {
|
|
88
|
-
lines.push(`- ${family}: ${score}`);
|
|
94
|
+
lines.push(`- ${familyLabel(family)}: ${score}`);
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
lines.push("");
|
|
92
98
|
if (result.findings.length === 0) {
|
|
93
|
-
lines.push("No high-confidence
|
|
99
|
+
lines.push("No high-confidence voice drift findings.");
|
|
100
|
+
appendSuppressedFindings(lines, result.summary.suppressedFindings);
|
|
94
101
|
lines.push("");
|
|
95
102
|
return lines.join("\n");
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
lines.push("Start here:");
|
|
99
|
-
|
|
100
|
-
lines.push(`${
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
result.findings.slice(0, 6).forEach((finding, index) => {
|
|
107
|
+
lines.push(`${index + 1}. ${priorityLabel(finding.priority)} ${familyLabel(finding.family)}`);
|
|
108
|
+
if (finding.confidence) {
|
|
109
|
+
lines.push(`Confidence: ${capitalize(finding.confidence.band)}; stability ${finding.confidence.stability}`);
|
|
110
|
+
}
|
|
111
|
+
lines.push(`Why: ${finding.why}`);
|
|
112
|
+
lines.push(`Do this: ${finding.action}`);
|
|
103
113
|
lines.push("");
|
|
104
|
-
}
|
|
114
|
+
});
|
|
115
|
+
appendSuppressedFindings(lines, result.summary.suppressedFindings);
|
|
105
116
|
return lines.join("\n");
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
function reviewFindings(source, draft, scores, mode) {
|
|
119
|
+
function reviewFindings(source, draft, scores, mode, familyConfidence) {
|
|
109
120
|
const modeConfig = REVIEW_MODES[mode];
|
|
110
121
|
if (source.source?.confidence?.band === "weak" || !modeConfig.findingThresholds) {
|
|
111
122
|
return [];
|
|
112
123
|
}
|
|
113
124
|
const findings = [];
|
|
114
|
-
if (
|
|
125
|
+
if (shouldFlag("evidence", scores, modeConfig, familyConfidence)) {
|
|
115
126
|
findings.push({
|
|
116
127
|
id: "v2.evidence-drift",
|
|
117
128
|
family: "evidence",
|
|
118
129
|
priority: "review",
|
|
130
|
+
confidence: familyConfidence.evidence,
|
|
119
131
|
why: `Source evidence sentence rate is ${source.families.evidence.features.evidenceSentenceRate}; draft rate is ${draft.families.evidence.features.evidenceSentenceRate}.`,
|
|
120
132
|
action: "Add concrete support before broad claims: a scene, quote, number, citation, URL, sensory detail, or specific example.",
|
|
121
133
|
});
|
|
122
134
|
}
|
|
123
|
-
if (
|
|
135
|
+
if (shouldFlag("rhythm", scores, modeConfig, familyConfidence)) {
|
|
124
136
|
findings.push({
|
|
125
137
|
id: "v2.rhythm-drift",
|
|
126
138
|
family: "rhythm",
|
|
127
139
|
priority: "consider",
|
|
140
|
+
confidence: familyConfidence.rhythm,
|
|
128
141
|
why: `Source median sentence/paragraph length is ${source.families.rhythm.features.sentenceWords.median}/${source.families.rhythm.features.paragraphWords.median}; draft is ${draft.families.rhythm.features.sentenceWords.median}/${draft.families.rhythm.features.paragraphWords.median}.`,
|
|
129
142
|
action: "Revise sentence and paragraph pacing toward the learned range.",
|
|
130
143
|
});
|
|
131
144
|
}
|
|
132
|
-
if (
|
|
145
|
+
if (shouldFlag("rhetoricalShape", scores, modeConfig, familyConfidence)) {
|
|
133
146
|
findings.push({
|
|
134
147
|
id: "v2.shape-drift",
|
|
135
148
|
family: "rhetoricalShape",
|
|
136
149
|
priority: "consider",
|
|
150
|
+
confidence: familyConfidence.rhetoricalShape,
|
|
137
151
|
why: `Source opening shape is ${source.families.rhetoricalShape.features.openingMoves.slice(0, 3).join(" -> ")}; draft opening shape is ${draft.families.rhetoricalShape.features.openingMoves.slice(0, 3).join(" -> ")}.`,
|
|
138
152
|
action: "Rework the opening so it uses a compatible scene, claim, contrast, reflection, or example sequence.",
|
|
139
153
|
});
|
|
140
154
|
}
|
|
155
|
+
if (shouldFlag("discourse", scores, modeConfig, familyConfidence)) {
|
|
156
|
+
findings.push({
|
|
157
|
+
id: "v2.discourse-drift",
|
|
158
|
+
family: "discourse",
|
|
159
|
+
priority: "consider",
|
|
160
|
+
confidence: familyConfidence.discourse,
|
|
161
|
+
why: `Source transition rates are ${rateMapLabel(source.families.discourse.features.transitionRates)}; draft rates are ${rateMapLabel(draft.families.discourse.features.transitionRates)}.`,
|
|
162
|
+
action: "Revise repeated sentence turns, callbacks, and transitions so the draft does not lean on a different discourse pattern.",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (shouldFlag("lexical", scores, modeConfig, familyConfidence)) {
|
|
166
|
+
findings.push({
|
|
167
|
+
id: "v2.lexical-drift",
|
|
168
|
+
family: "lexical",
|
|
169
|
+
priority: "consider",
|
|
170
|
+
confidence: familyConfidence.lexical,
|
|
171
|
+
why: `Function-word, masked character, punctuation, or boundary-token habits drift from the calibrated source profile.`,
|
|
172
|
+
action: "Revise diction and punctuation where it improves the article; do not stuff source topic words or add artificial imperfections.",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (shouldFlag("register", scores, modeConfig, familyConfidence)) {
|
|
176
|
+
findings.push({
|
|
177
|
+
id: "v2.register-drift",
|
|
178
|
+
family: "register",
|
|
179
|
+
priority: "consider",
|
|
180
|
+
confidence: familyConfidence.register,
|
|
181
|
+
why: `Source primary register is ${source.families.register.features.primary.value}; draft primary register is ${draft.families.register.features.primary.value}.`,
|
|
182
|
+
action: "Bring the stance closer to the learned genre mix while preserving the draft's real subject and audience.",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (shouldFlag("structure", scores, modeConfig, familyConfidence)) {
|
|
186
|
+
findings.push({
|
|
187
|
+
id: "v2.structure-drift",
|
|
188
|
+
family: "structure",
|
|
189
|
+
priority: "consider",
|
|
190
|
+
confidence: familyConfidence.structure,
|
|
191
|
+
why: `Source heading/list/quote and section-size patterns differ from the draft's document structure.`,
|
|
192
|
+
action: "Adjust section shape, opening order, list use, or quote placement only where the article benefits from that structure.",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
141
195
|
return findings;
|
|
142
196
|
}
|
|
143
197
|
|
|
198
|
+
function shouldFlag(family, scores, modeConfig, familyConfidence) {
|
|
199
|
+
return familyConfidence[family]?.usableForFindings &&
|
|
200
|
+
Number.isFinite(scores[family]) &&
|
|
201
|
+
scores[family] < modeConfig.findingThresholds[family];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function familyConfidenceFor(sourceProfile, familyDiagnostics) {
|
|
205
|
+
const profileDiagnostics = sourceProfile.calibration?.familyDiagnostics ?? {};
|
|
206
|
+
return Object.fromEntries(Object.keys(sourceProfile.families ?? {}).map((family) => {
|
|
207
|
+
const profile = profileDiagnostics[family] ?? {};
|
|
208
|
+
const runtime = familyDiagnostics[family] ?? {};
|
|
209
|
+
return [family, {
|
|
210
|
+
band: sourceProfile.families[family]?.confidence ?? "low",
|
|
211
|
+
stability: runtime.stability ?? profile.stability ?? 0.45,
|
|
212
|
+
observations: profile.observations ?? runtime.observations ?? 0,
|
|
213
|
+
usableForFindings: profile.usableForFindings ?? sourceProfile.source?.confidence?.band !== "weak",
|
|
214
|
+
minimumEvidence: profile.minimumEvidence ?? null,
|
|
215
|
+
}];
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function suppressedFindingsFor(sourceProfile, familyConfidence) {
|
|
220
|
+
return Object.entries(familyConfidence)
|
|
221
|
+
.filter(([, confidence]) => !confidence.usableForFindings)
|
|
222
|
+
.map(([family, confidence]) => ({
|
|
223
|
+
family,
|
|
224
|
+
reason: suppressedReasonFor(sourceProfile, confidence),
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function suppressedReasonFor(sourceProfile, confidence) {
|
|
229
|
+
if (sourceProfile.source?.confidence?.band === "weak") {
|
|
230
|
+
return "weak corpus confidence; add more representative source documents before trusting this family.";
|
|
231
|
+
}
|
|
232
|
+
const minimum = confidence.minimumEvidence;
|
|
233
|
+
if (minimum && (!minimum.documentsMet || !minimum.sentencesMet || !minimum.wordsMet)) {
|
|
234
|
+
return `minimum evidence not met; requires ${minimum.requiredDocuments} document(s), ${minimum.requiredSentences} sentence(s), and ${minimum.requiredWords} word(s).`;
|
|
235
|
+
}
|
|
236
|
+
return "calibration stability is too low for a high-confidence finding.";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function appendSuppressedFindings(lines, suppressedFindings = []) {
|
|
240
|
+
if (!suppressedFindings.length) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
lines.push("");
|
|
244
|
+
lines.push("Suppressed findings:");
|
|
245
|
+
suppressedFindings.slice(0, 6).forEach((item) => {
|
|
246
|
+
lines.push(`- ${familyLabel(item.family)}: ${item.reason}`);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function rateMapLabel(value = {}) {
|
|
251
|
+
return Object.entries(value).map(([key, rate]) => `${key}:${rate}`).join(", ") || "none";
|
|
252
|
+
}
|
|
253
|
+
|
|
144
254
|
function normalizeReviewMode(mode) {
|
|
145
255
|
const normalized = String(mode ?? "balanced").toLowerCase();
|
|
146
256
|
if (!Object.hasOwn(REVIEW_MODES, normalized)) {
|
|
@@ -175,3 +285,19 @@ function resolvePath(cwd, value) {
|
|
|
175
285
|
function capitalize(value) {
|
|
176
286
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
177
287
|
}
|
|
288
|
+
|
|
289
|
+
function familyLabel(family) {
|
|
290
|
+
return {
|
|
291
|
+
evidence: "Evidence",
|
|
292
|
+
rhythm: "Rhythm",
|
|
293
|
+
rhetoricalShape: "Rhetorical shape",
|
|
294
|
+
discourse: "Discourse",
|
|
295
|
+
lexical: "Lexical style",
|
|
296
|
+
register: "Register",
|
|
297
|
+
structure: "Structure",
|
|
298
|
+
}[family] ?? capitalize(family);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function priorityLabel(priority) {
|
|
302
|
+
return priority === "review" ? "Review" : "Consider";
|
|
303
|
+
}
|
package/src/v2/revise-plan.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
styleDistanceFromDiagnostics,
|
|
14
14
|
} from "./stylometry.js";
|
|
15
15
|
|
|
16
|
-
const MAX_ACTIONS =
|
|
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}
|
|
97
|
-
lines.push(`
|
|
98
|
-
lines.push(`
|
|
99
|
-
lines.push(` Why
|
|
100
|
-
lines.push(`
|
|
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
|
+
}
|
package/src/v2/stylometry.js
CHANGED
|
@@ -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.
|
|
163
|
-
[topItemDistance(source.transitionBigrams, draft.transitionBigrams), 0.
|
|
164
|
-
[topItemDistance(source.transitionTrigrams, draft.transitionTrigrams), 0.
|
|
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.
|
|
192
|
-
[distributionDelta(source.headingCount, draft.headingCount), 0.
|
|
193
|
-
[
|
|
194
|
-
[
|
|
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
|
}
|