dravoice 0.1.1 → 0.1.3
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/LICENSE +21 -21
- package/README.md +102 -36
- package/bin/dravoice.js +11 -10
- package/package.json +47 -45
- package/src/index.js +874 -197
- package/src/v2/analyzers/discourse.js +63 -52
- package/src/v2/analyzers/evidence.js +73 -38
- package/src/v2/analyzers/lexical.js +114 -58
- package/src/v2/analyzers/register.js +46 -34
- package/src/v2/analyzers/rhetorical-shape.js +59 -48
- package/src/v2/analyzers/rhythm.js +39 -47
- package/src/v2/analyzers/structure.js +24 -24
- package/src/v2/benchmark.js +574 -568
- package/src/v2/brief.js +154 -146
- package/src/v2/config.js +78 -0
- package/src/v2/document-model.js +351 -260
- package/src/v2/inspect.js +67 -67
- package/src/v2/io-utils.js +51 -0
- package/src/v2/profile.js +155 -129
- package/src/v2/prompt.js +65 -64
- package/src/v2/review.js +177 -219
- package/src/v2/revise-plan.js +130 -33
- package/src/v2/stylometry.js +123 -17
- package/src/v2/text-utils.js +123 -123
package/src/v2/review.js
CHANGED
|
@@ -1,219 +1,177 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { buildVoiceProfileV2, loadVoicePackV2 } from "./profile.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const REVIEW_MODES = {
|
|
8
|
-
loose: {
|
|
9
|
-
findingThresholds: null,
|
|
10
|
-
exitOnDrift: false,
|
|
11
|
-
},
|
|
12
|
-
balanced: {
|
|
13
|
-
findingThresholds: { evidence: 65, rhythm: 55, rhetoricalShape: 50 },
|
|
14
|
-
exitOnDrift: false,
|
|
15
|
-
},
|
|
16
|
-
strict: {
|
|
17
|
-
findingThresholds: { evidence: 75, rhythm: 70, rhetoricalShape: 65 },
|
|
18
|
-
exitOnDrift: true,
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export function reviewVoiceDraftV2({ file, voice, cwd = process.cwd(), mode = "balanced" }) {
|
|
23
|
-
const reviewMode = normalizeReviewMode(mode);
|
|
24
|
-
const sourceProfile = typeof voice === "string" ? loadVoicePackV2(voice) : voice;
|
|
25
|
-
const filePath =
|
|
26
|
-
const draftDocument = parseDocument({
|
|
27
|
-
filePath,
|
|
28
|
-
rootDir: cwd,
|
|
29
|
-
contents:
|
|
30
|
-
});
|
|
31
|
-
const draftProfile = buildVoiceProfileV2({ documents: [draftDocument] });
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
draftDocument.wordCount < sourceProfile.calibration.minimumDraftSize.words ||
|
|
35
|
-
draftDocument.sentences.length < sourceProfile.calibration.minimumDraftSize.sentences
|
|
36
|
-
) {
|
|
37
|
-
return {
|
|
38
|
-
schemaVersion: 2,
|
|
39
|
-
file: displayPath(filePath, cwd),
|
|
40
|
-
summary: {
|
|
41
|
-
mode: reviewMode,
|
|
42
|
-
fit: { band: "insufficient-evidence", distance: 0 },
|
|
43
|
-
familyScores: {},
|
|
44
|
-
corpusConfidence: sourceProfile.source.confidence,
|
|
45
|
-
},
|
|
46
|
-
findings: [],
|
|
47
|
-
exitCode: 0,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
distance,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
if (scores.rhetoricalShape < modeConfig.findingThresholds.rhetoricalShape) {
|
|
180
|
-
findings.push({
|
|
181
|
-
id: "v2.shape-drift",
|
|
182
|
-
family: "rhetoricalShape",
|
|
183
|
-
priority: "consider",
|
|
184
|
-
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(" -> ")}.`,
|
|
185
|
-
action: "Rework the opening so it uses a compatible scene, claim, contrast, reflection, or example sequence.",
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
return findings;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function normalizeReviewMode(mode) {
|
|
192
|
-
const normalized = String(mode ?? "balanced").toLowerCase();
|
|
193
|
-
if (!Object.hasOwn(REVIEW_MODES, normalized)) {
|
|
194
|
-
throw new Error(`Unsupported review mode: ${mode}. Expected loose, balanced, or strict.`);
|
|
195
|
-
}
|
|
196
|
-
return normalized;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function fitBand(distance, findings) {
|
|
200
|
-
if (findings.some((finding) => finding.priority === "review") || distance >= 35) {
|
|
201
|
-
return "drift";
|
|
202
|
-
}
|
|
203
|
-
if (findings.length > 0 || distance >= 20) {
|
|
204
|
-
return "watch";
|
|
205
|
-
}
|
|
206
|
-
return "close";
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function displayPath(filePath, cwd) {
|
|
210
|
-
const relative = path.relative(cwd, filePath);
|
|
211
|
-
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
212
|
-
return relative.split(path.sep).join("/");
|
|
213
|
-
}
|
|
214
|
-
return filePath.split(path.sep).join("/");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function capitalize(value) {
|
|
218
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
219
|
-
}
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parseDocument } from "./document-model.js";
|
|
3
|
+
import { readUtf8FileBounded } from "./io-utils.js";
|
|
4
|
+
import { buildVoiceProfileV2, loadVoicePackV2 } from "./profile.js";
|
|
5
|
+
import { familyDiagnosticsFor, styleDistanceFromDiagnostics } from "./stylometry.js";
|
|
6
|
+
|
|
7
|
+
const REVIEW_MODES = {
|
|
8
|
+
loose: {
|
|
9
|
+
findingThresholds: null,
|
|
10
|
+
exitOnDrift: false,
|
|
11
|
+
},
|
|
12
|
+
balanced: {
|
|
13
|
+
findingThresholds: { evidence: 65, rhythm: 55, rhetoricalShape: 50 },
|
|
14
|
+
exitOnDrift: false,
|
|
15
|
+
},
|
|
16
|
+
strict: {
|
|
17
|
+
findingThresholds: { evidence: 75, rhythm: 70, rhetoricalShape: 65 },
|
|
18
|
+
exitOnDrift: true,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function reviewVoiceDraftV2({ file, voice, cwd = process.cwd(), mode = "balanced" }) {
|
|
23
|
+
const reviewMode = normalizeReviewMode(mode);
|
|
24
|
+
const sourceProfile = typeof voice === "string" ? loadVoicePackV2(voice) : voice;
|
|
25
|
+
const filePath = resolvePath(cwd, file);
|
|
26
|
+
const draftDocument = parseDocument({
|
|
27
|
+
filePath,
|
|
28
|
+
rootDir: cwd,
|
|
29
|
+
contents: readUtf8FileBounded(filePath, { label: "Draft file", maxBytes: 2 * 1024 * 1024 }),
|
|
30
|
+
});
|
|
31
|
+
const draftProfile = buildVoiceProfileV2({ documents: [draftDocument] });
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
draftDocument.wordCount < sourceProfile.calibration.minimumDraftSize.words ||
|
|
35
|
+
draftDocument.sentences.length < sourceProfile.calibration.minimumDraftSize.sentences
|
|
36
|
+
) {
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: 2,
|
|
39
|
+
file: displayPath(filePath, cwd),
|
|
40
|
+
summary: {
|
|
41
|
+
mode: reviewMode,
|
|
42
|
+
fit: { band: "insufficient-evidence", distance: 0 },
|
|
43
|
+
familyScores: {},
|
|
44
|
+
corpusConfidence: sourceProfile.source.confidence,
|
|
45
|
+
},
|
|
46
|
+
findings: [],
|
|
47
|
+
exitCode: 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const familyDiagnostics = familyDiagnosticsFor(sourceProfile, draftProfile);
|
|
52
|
+
const familyScores = Object.fromEntries(Object.entries(familyDiagnostics).map(([family, item]) => [family, item.score]));
|
|
53
|
+
const distance = styleDistanceFromDiagnostics(familyDiagnostics);
|
|
54
|
+
const findings = reviewFindings(sourceProfile, draftProfile, familyScores, reviewMode);
|
|
55
|
+
const fit = {
|
|
56
|
+
band: fitBand(distance, findings, familyDiagnostics),
|
|
57
|
+
distance,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
schemaVersion: 2,
|
|
62
|
+
file: displayPath(filePath, cwd),
|
|
63
|
+
summary: {
|
|
64
|
+
mode: reviewMode,
|
|
65
|
+
fit,
|
|
66
|
+
familyScores,
|
|
67
|
+
familyDistances: Object.fromEntries(Object.entries(familyDiagnostics).map(([family, item]) => [family, item.distance])),
|
|
68
|
+
familyDrift: Object.fromEntries(Object.entries(familyDiagnostics).map(([family, item]) => [family, item.drift])),
|
|
69
|
+
thresholds: Object.fromEntries(Object.entries(familyDiagnostics).map(([family, item]) => [family, item.threshold])),
|
|
70
|
+
corpusConfidence: sourceProfile.source.confidence,
|
|
71
|
+
},
|
|
72
|
+
findings,
|
|
73
|
+
exitCode: REVIEW_MODES[reviewMode].exitOnDrift && fit.band === "drift" ? 1 : 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function renderVoiceReviewV2(result) {
|
|
78
|
+
const lines = [
|
|
79
|
+
"Voice rewrite notes, not AI detection.",
|
|
80
|
+
"",
|
|
81
|
+
result.file,
|
|
82
|
+
`Voice fit: ${capitalize(result.summary.fit.band)} (${result.summary.fit.distance} distance)`,
|
|
83
|
+
`Corpus confidence: ${capitalize(result.summary.corpusConfidence.band)} - ${result.summary.corpusConfidence.message}`,
|
|
84
|
+
"Family scores:",
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
for (const [family, score] of Object.entries(result.summary.familyScores)) {
|
|
88
|
+
lines.push(`- ${family}: ${score}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
lines.push("");
|
|
92
|
+
if (result.findings.length === 0) {
|
|
93
|
+
lines.push("No high-confidence V2 voice drift findings.");
|
|
94
|
+
lines.push("");
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
lines.push("Start here:");
|
|
99
|
+
for (const finding of result.findings.slice(0, 6)) {
|
|
100
|
+
lines.push(`${finding.priority} ${finding.family} ${finding.id}`);
|
|
101
|
+
lines.push(`Why flagged: ${finding.why}`);
|
|
102
|
+
lines.push(`Revise by: ${finding.action}`);
|
|
103
|
+
lines.push("");
|
|
104
|
+
}
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function reviewFindings(source, draft, scores, mode) {
|
|
109
|
+
const modeConfig = REVIEW_MODES[mode];
|
|
110
|
+
if (source.source?.confidence?.band === "weak" || !modeConfig.findingThresholds) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const findings = [];
|
|
114
|
+
if (scores.evidence < modeConfig.findingThresholds.evidence) {
|
|
115
|
+
findings.push({
|
|
116
|
+
id: "v2.evidence-drift",
|
|
117
|
+
family: "evidence",
|
|
118
|
+
priority: "review",
|
|
119
|
+
why: `Source evidence sentence rate is ${source.families.evidence.features.evidenceSentenceRate}; draft rate is ${draft.families.evidence.features.evidenceSentenceRate}.`,
|
|
120
|
+
action: "Add concrete support before broad claims: a scene, quote, number, citation, URL, sensory detail, or specific example.",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (scores.rhythm < modeConfig.findingThresholds.rhythm) {
|
|
124
|
+
findings.push({
|
|
125
|
+
id: "v2.rhythm-drift",
|
|
126
|
+
family: "rhythm",
|
|
127
|
+
priority: "consider",
|
|
128
|
+
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
|
+
action: "Revise sentence and paragraph pacing toward the learned range.",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (scores.rhetoricalShape < modeConfig.findingThresholds.rhetoricalShape) {
|
|
133
|
+
findings.push({
|
|
134
|
+
id: "v2.shape-drift",
|
|
135
|
+
family: "rhetoricalShape",
|
|
136
|
+
priority: "consider",
|
|
137
|
+
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
|
+
action: "Rework the opening so it uses a compatible scene, claim, contrast, reflection, or example sequence.",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return findings;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeReviewMode(mode) {
|
|
145
|
+
const normalized = String(mode ?? "balanced").toLowerCase();
|
|
146
|
+
if (!Object.hasOwn(REVIEW_MODES, normalized)) {
|
|
147
|
+
throw new Error(`Unsupported review mode: ${mode}. Expected loose, balanced, or strict.`);
|
|
148
|
+
}
|
|
149
|
+
return normalized;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function fitBand(distance, findings, familyDiagnostics) {
|
|
153
|
+
const maxDrift = Math.max(0, ...Object.values(familyDiagnostics).map((item) => item.drift));
|
|
154
|
+
if (findings.some((finding) => finding.priority === "review") || distance >= 35 || maxDrift >= 1.25) {
|
|
155
|
+
return "drift";
|
|
156
|
+
}
|
|
157
|
+
if (findings.length > 0 || distance >= 20 || maxDrift > 0) {
|
|
158
|
+
return "watch";
|
|
159
|
+
}
|
|
160
|
+
return "close";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function displayPath(filePath, cwd) {
|
|
164
|
+
const relative = path.relative(cwd, filePath);
|
|
165
|
+
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
166
|
+
return relative.split(path.sep).join("/");
|
|
167
|
+
}
|
|
168
|
+
return filePath.split(path.sep).join("/");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolvePath(cwd, value) {
|
|
172
|
+
return path.isAbsolute(value) ? value : path.resolve(cwd, value);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function capitalize(value) {
|
|
176
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
177
|
+
}
|