dravoice 0.1.2 → 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 -63
- package/src/v2/analyzers/evidence.js +82 -82
- package/src/v2/analyzers/lexical.js +114 -114
- package/src/v2/analyzers/register.js +46 -34
- package/src/v2/analyzers/rhetorical-shape.js +59 -59
- 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 +227 -203
- package/src/v2/prompt.js +65 -64
- package/src/v2/review.js +177 -173
- package/src/v2/revise-plan.js +437 -433
- package/src/v2/stylometry.js +342 -332
- package/src/v2/text-utils.js +123 -123
package/src/v2/brief.js
CHANGED
|
@@ -1,146 +1,154 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { loadVoicePackV2 } from "./profile.js";
|
|
6
|
-
|
|
7
|
-
export function voiceArticleBriefV2({ voice, topic, evidence, cwd = process.cwd() }) {
|
|
8
|
-
const profile = typeof voice === "string" ? loadVoicePackV2(resolvePath(cwd, voice)) : voice;
|
|
9
|
-
const evidenceResult = evidence ? evidenceAnchorsFromFile({ evidence, cwd }) : {
|
|
10
|
-
source: null,
|
|
11
|
-
anchors: [],
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
schemaVersion: 2,
|
|
16
|
-
generatedBy: "dravoice-v2-brief",
|
|
17
|
-
topic,
|
|
18
|
-
voice: {
|
|
19
|
-
corpusConfidence: profile.source.confidence,
|
|
20
|
-
sourceFileCount: profile.source.documentCount,
|
|
21
|
-
sourceWordCount: profile.source.wordCount,
|
|
22
|
-
primaryRegister: profile.families.register.features.primary.value,
|
|
23
|
-
evidenceSentenceRate: profile.families.evidence.features.evidenceSentenceRate,
|
|
24
|
-
featureFamilies: Object.keys(profile.families),
|
|
25
|
-
draftingRules: profile.guidance.draftingRules.slice(0, 5),
|
|
26
|
-
},
|
|
27
|
-
workingThesis: `Draft a grounded article about ${topic}. Let the supplied evidence set the size of each claim before broadening the lesson.`,
|
|
28
|
-
evidence: evidenceResult,
|
|
29
|
-
missingEvidence: missingEvidenceFor({ topic, evidenceAnchors: evidenceResult.anchors }),
|
|
30
|
-
outline: outlineFor(profile),
|
|
31
|
-
voiceCautions: [
|
|
32
|
-
...profile.guidance.avoid,
|
|
33
|
-
"Mark unsupported claims as [specific evidence needed] instead of inventing proof.",
|
|
34
|
-
].slice(0, 5),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function renderVoiceBriefV2(brief) {
|
|
39
|
-
const lines = [
|
|
40
|
-
`# Article Brief: ${brief.topic}`,
|
|
41
|
-
"",
|
|
42
|
-
"## Voice Source",
|
|
43
|
-
"",
|
|
44
|
-
`- Corpus confidence: ${capitalize(brief.voice.corpusConfidence.band)} - ${brief.voice.corpusConfidence.message}`,
|
|
45
|
-
`- Source files: ${brief.voice.sourceFileCount}`,
|
|
46
|
-
`- Primary register: ${brief.voice.primaryRegister}`,
|
|
47
|
-
`- Evidence sentence rate: ${brief.voice.evidenceSentenceRate}`,
|
|
48
|
-
"",
|
|
49
|
-
"## Working Thesis",
|
|
50
|
-
"",
|
|
51
|
-
`- ${brief.workingThesis}`,
|
|
52
|
-
"",
|
|
53
|
-
"## Evidence Anchors",
|
|
54
|
-
"",
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
if (brief.evidence.anchors.length) {
|
|
58
|
-
for (const item of brief.evidence.anchors) {
|
|
59
|
-
const typeList = item.types.length ? ` (${item.types.join(", ")})` : "";
|
|
60
|
-
lines.push(`- ${brief.evidence.source}:${item.line}${typeList} - ${item.text}`);
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
lines.push("- [specific evidence needed] Add notes, dates, quotes, examples, or source links before drafting broad claims.");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
lines.push("", "## Missing Evidence", "");
|
|
67
|
-
lines.push(...brief.missingEvidence.map((item) => `- ${item}`));
|
|
68
|
-
|
|
69
|
-
lines.push("", "## Outline", "");
|
|
70
|
-
lines.push(...brief.outline.map((item, index) => `${index + 1}. ${item}`));
|
|
71
|
-
|
|
72
|
-
lines.push("", "## Voice Cautions", "");
|
|
73
|
-
lines.push(...brief.voiceCautions.map((item) => `- ${item}`));
|
|
74
|
-
|
|
75
|
-
lines.push(
|
|
76
|
-
"",
|
|
77
|
-
"## Drafting Prompt",
|
|
78
|
-
"",
|
|
79
|
-
`Write the article about ${brief.topic} using the evidence anchors above. Keep claims close to concrete support, follow the voice cautions, and write [specific evidence needed] anywhere the brief does not supply enough ground.`,
|
|
80
|
-
"",
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
return lines.join("\n");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function evidenceAnchorsFromFile({ evidence, cwd }) {
|
|
87
|
-
const evidencePath = resolvePath(cwd, evidence);
|
|
88
|
-
const contents =
|
|
89
|
-
const document = parseDocument({
|
|
90
|
-
filePath: evidencePath,
|
|
91
|
-
rootDir: cwd,
|
|
92
|
-
contents,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
source: displayPath(evidencePath, cwd),
|
|
97
|
-
anchors: document.sentences
|
|
98
|
-
.map((sentence) => ({
|
|
99
|
-
line: sentence.line,
|
|
100
|
-
text: sentence.text,
|
|
101
|
-
types: evidenceTypes(sentence.text),
|
|
102
|
-
}))
|
|
103
|
-
.filter((sentence) => sentence.types.length > 0)
|
|
104
|
-
.slice(0, 8),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function missingEvidenceFor({ topic, evidenceAnchors }) {
|
|
109
|
-
const items = [
|
|
110
|
-
`Add [specific evidence needed] for the central claim about ${topic}.`,
|
|
111
|
-
"Add [specific evidence needed] for any number, date, quote, source, or example the article depends on.",
|
|
112
|
-
];
|
|
113
|
-
if (evidenceAnchors.length === 0) {
|
|
114
|
-
items.unshift("No evidence anchors were detected; collect concrete notes before asking for a full draft.");
|
|
115
|
-
}
|
|
116
|
-
return items;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function outlineFor(profile) {
|
|
120
|
-
const opening = profile.families.rhetoricalShape.features.openingMoves.slice(0, 3).join(" -> ");
|
|
121
|
-
const sentenceMedian = profile.families.rhythm.features.sentenceWords.median;
|
|
122
|
-
return [
|
|
123
|
-
opening
|
|
124
|
-
? `Start from a concrete artifact or observation, keeping the opening shape compatible with: ${opening}.`
|
|
125
|
-
: "Start from a concrete artifact or observation before making the larger claim.",
|
|
126
|
-
"Name the pressure, question, or practical stakes that make the evidence matter.",
|
|
127
|
-
`Develop the article in the learned register with sentence pacing near the ${sentenceMedian}-word median where it fits.`,
|
|
128
|
-
"Close by returning to the evidence and leaving the reader with a practical handle, not a generic conclusion.",
|
|
129
|
-
];
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function resolvePath(cwd, value) {
|
|
133
|
-
return path.isAbsolute(value) ? value : path.join(cwd, value);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function displayPath(filePath, rootDir) {
|
|
137
|
-
const relative = path.relative(rootDir, filePath);
|
|
138
|
-
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
139
|
-
return relative.split(path.sep).join("/");
|
|
140
|
-
}
|
|
141
|
-
return filePath.split(path.sep).join("/");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function capitalize(value) {
|
|
145
|
-
return String(value ?? "").charAt(0).toUpperCase() + String(value ?? "").slice(1);
|
|
146
|
-
}
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { evidenceTypes } from "./analyzers/evidence.js";
|
|
3
|
+
import { parseDocument } from "./document-model.js";
|
|
4
|
+
import { readUtf8FileBounded } from "./io-utils.js";
|
|
5
|
+
import { loadVoicePackV2 } from "./profile.js";
|
|
6
|
+
|
|
7
|
+
export function voiceArticleBriefV2({ voice, topic, evidence, cwd = process.cwd() }) {
|
|
8
|
+
const profile = typeof voice === "string" ? loadVoicePackV2(resolvePath(cwd, voice)) : voice;
|
|
9
|
+
const evidenceResult = evidence ? evidenceAnchorsFromFile({ evidence, cwd }) : {
|
|
10
|
+
source: null,
|
|
11
|
+
anchors: [],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
schemaVersion: 2,
|
|
16
|
+
generatedBy: "dravoice-v2-brief",
|
|
17
|
+
topic,
|
|
18
|
+
voice: {
|
|
19
|
+
corpusConfidence: profile.source.confidence,
|
|
20
|
+
sourceFileCount: profile.source.documentCount,
|
|
21
|
+
sourceWordCount: profile.source.wordCount,
|
|
22
|
+
primaryRegister: profile.families.register.features.primary.value,
|
|
23
|
+
evidenceSentenceRate: profile.families.evidence.features.evidenceSentenceRate,
|
|
24
|
+
featureFamilies: Object.keys(profile.families),
|
|
25
|
+
draftingRules: profile.guidance.draftingRules.slice(0, 5),
|
|
26
|
+
},
|
|
27
|
+
workingThesis: `Draft a grounded article about ${topic}. Let the supplied evidence set the size of each claim before broadening the lesson.`,
|
|
28
|
+
evidence: evidenceResult,
|
|
29
|
+
missingEvidence: missingEvidenceFor({ topic, evidenceAnchors: evidenceResult.anchors }),
|
|
30
|
+
outline: outlineFor(profile),
|
|
31
|
+
voiceCautions: [
|
|
32
|
+
...profile.guidance.avoid,
|
|
33
|
+
"Mark unsupported claims as [specific evidence needed] instead of inventing proof.",
|
|
34
|
+
].slice(0, 5),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderVoiceBriefV2(brief) {
|
|
39
|
+
const lines = [
|
|
40
|
+
`# Article Brief: ${safeInline(brief.topic)}`,
|
|
41
|
+
"",
|
|
42
|
+
"## Voice Source",
|
|
43
|
+
"",
|
|
44
|
+
`- Corpus confidence: ${capitalize(brief.voice.corpusConfidence.band)} - ${brief.voice.corpusConfidence.message}`,
|
|
45
|
+
`- Source files: ${brief.voice.sourceFileCount}`,
|
|
46
|
+
`- Primary register: ${brief.voice.primaryRegister}`,
|
|
47
|
+
`- Evidence sentence rate: ${brief.voice.evidenceSentenceRate}`,
|
|
48
|
+
"",
|
|
49
|
+
"## Working Thesis",
|
|
50
|
+
"",
|
|
51
|
+
`- ${safeInline(brief.workingThesis)}`,
|
|
52
|
+
"",
|
|
53
|
+
"## Evidence Anchors",
|
|
54
|
+
"",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
if (brief.evidence.anchors.length) {
|
|
58
|
+
for (const item of brief.evidence.anchors) {
|
|
59
|
+
const typeList = item.types.length ? ` (${item.types.join(", ")})` : "";
|
|
60
|
+
lines.push(`- ${safeInline(brief.evidence.source)}:${item.line}${typeList} - ${safeInline(item.text)}`);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
lines.push("- [specific evidence needed] Add notes, dates, quotes, examples, or source links before drafting broad claims.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
lines.push("", "## Missing Evidence", "");
|
|
67
|
+
lines.push(...brief.missingEvidence.map((item) => `- ${safeInline(item)}`));
|
|
68
|
+
|
|
69
|
+
lines.push("", "## Outline", "");
|
|
70
|
+
lines.push(...brief.outline.map((item, index) => `${index + 1}. ${safeInline(item)}`));
|
|
71
|
+
|
|
72
|
+
lines.push("", "## Voice Cautions", "");
|
|
73
|
+
lines.push(...brief.voiceCautions.map((item) => `- ${safeInline(item)}`));
|
|
74
|
+
|
|
75
|
+
lines.push(
|
|
76
|
+
"",
|
|
77
|
+
"## Drafting Prompt",
|
|
78
|
+
"",
|
|
79
|
+
`Write the article about ${safeInline(brief.topic)} using the evidence anchors above. Keep claims close to concrete support, follow the voice cautions, and write [specific evidence needed] anywhere the brief does not supply enough ground.`,
|
|
80
|
+
"",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function evidenceAnchorsFromFile({ evidence, cwd }) {
|
|
87
|
+
const evidencePath = resolvePath(cwd, evidence);
|
|
88
|
+
const contents = readUtf8FileBounded(evidencePath, { label: "Evidence file", maxBytes: 1024 * 1024 });
|
|
89
|
+
const document = parseDocument({
|
|
90
|
+
filePath: evidencePath,
|
|
91
|
+
rootDir: cwd,
|
|
92
|
+
contents,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
source: displayPath(evidencePath, cwd),
|
|
97
|
+
anchors: document.sentences
|
|
98
|
+
.map((sentence) => ({
|
|
99
|
+
line: sentence.line,
|
|
100
|
+
text: sentence.text,
|
|
101
|
+
types: evidenceTypes(sentence.text),
|
|
102
|
+
}))
|
|
103
|
+
.filter((sentence) => sentence.types.length > 0)
|
|
104
|
+
.slice(0, 8),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function missingEvidenceFor({ topic, evidenceAnchors }) {
|
|
109
|
+
const items = [
|
|
110
|
+
`Add [specific evidence needed] for the central claim about ${topic}.`,
|
|
111
|
+
"Add [specific evidence needed] for any number, date, quote, source, or example the article depends on.",
|
|
112
|
+
];
|
|
113
|
+
if (evidenceAnchors.length === 0) {
|
|
114
|
+
items.unshift("No evidence anchors were detected; collect concrete notes before asking for a full draft.");
|
|
115
|
+
}
|
|
116
|
+
return items;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function outlineFor(profile) {
|
|
120
|
+
const opening = profile.families.rhetoricalShape.features.openingMoves.slice(0, 3).join(" -> ");
|
|
121
|
+
const sentenceMedian = profile.families.rhythm.features.sentenceWords.median;
|
|
122
|
+
return [
|
|
123
|
+
opening
|
|
124
|
+
? `Start from a concrete artifact or observation, keeping the opening shape compatible with: ${opening}.`
|
|
125
|
+
: "Start from a concrete artifact or observation before making the larger claim.",
|
|
126
|
+
"Name the pressure, question, or practical stakes that make the evidence matter.",
|
|
127
|
+
`Develop the article in the learned register with sentence pacing near the ${sentenceMedian}-word median where it fits.`,
|
|
128
|
+
"Close by returning to the evidence and leaving the reader with a practical handle, not a generic conclusion.",
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolvePath(cwd, value) {
|
|
133
|
+
return path.isAbsolute(value) ? value : path.join(cwd, value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function displayPath(filePath, rootDir) {
|
|
137
|
+
const relative = path.relative(rootDir, filePath);
|
|
138
|
+
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
139
|
+
return relative.split(path.sep).join("/");
|
|
140
|
+
}
|
|
141
|
+
return filePath.split(path.sep).join("/");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function capitalize(value) {
|
|
145
|
+
return String(value ?? "").charAt(0).toUpperCase() + String(value ?? "").slice(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function safeInline(value) {
|
|
149
|
+
return String(value ?? "")
|
|
150
|
+
.replace(/[\0-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
|
151
|
+
.replace(/\s+/g, " ")
|
|
152
|
+
.replace(/`/g, "'")
|
|
153
|
+
.trim();
|
|
154
|
+
}
|
package/src/v2/config.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { writeUtf8FileSafely } from "./io-utils.js";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_EXAMPLES_DIR = "./articles";
|
|
6
|
+
export const DEFAULT_VOICE_DIR = "./dravoice-voice";
|
|
7
|
+
export const DEFAULT_PROMPT_FORMAT = "agents";
|
|
8
|
+
export const DEFAULT_PROMPT_OUT = "AGENTS.md";
|
|
9
|
+
|
|
10
|
+
const CONFIG_FILE = ".dravoice.yml";
|
|
11
|
+
|
|
12
|
+
export function configPathFor(cwd) {
|
|
13
|
+
return path.join(cwd, CONFIG_FILE);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function loadProjectConfig(cwd) {
|
|
17
|
+
const configPath = configPathFor(cwd);
|
|
18
|
+
if (!fs.existsSync(configPath)) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
const config = {};
|
|
22
|
+
const contents = fs.readFileSync(configPath, "utf8");
|
|
23
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
24
|
+
const line = rawLine.trim();
|
|
25
|
+
if (!line || line.startsWith("#")) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const match = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(rawLine);
|
|
29
|
+
if (!match) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const [, key, value] = match;
|
|
33
|
+
if (["voice", "examples", "promptFormat", "promptOut"].includes(key)) {
|
|
34
|
+
config[key] = unquoteYamlScalar(value.trim());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return config;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function writeProjectConfig(cwd, updates = {}) {
|
|
41
|
+
const existing = loadProjectConfig(cwd);
|
|
42
|
+
const config = {
|
|
43
|
+
voice: updates.voice ?? existing.voice ?? DEFAULT_VOICE_DIR,
|
|
44
|
+
examples: updates.examples ?? existing.examples ?? DEFAULT_EXAMPLES_DIR,
|
|
45
|
+
promptFormat: updates.promptFormat ?? existing.promptFormat ?? DEFAULT_PROMPT_FORMAT,
|
|
46
|
+
promptOut: updates.promptOut ?? existing.promptOut ?? DEFAULT_PROMPT_OUT,
|
|
47
|
+
};
|
|
48
|
+
writeUtf8FileSafely(
|
|
49
|
+
configPathFor(cwd),
|
|
50
|
+
[
|
|
51
|
+
"# Dravoice project defaults",
|
|
52
|
+
`voice: ${displayPath(config.voice)}`,
|
|
53
|
+
`examples: ${displayPath(config.examples)}`,
|
|
54
|
+
`promptFormat: ${config.promptFormat}`,
|
|
55
|
+
`promptOut: ${displayPath(config.promptOut)}`,
|
|
56
|
+
"",
|
|
57
|
+
].join("\n"),
|
|
58
|
+
);
|
|
59
|
+
return config;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveConfiguredPath(cwd, value) {
|
|
63
|
+
if (!value) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return path.isAbsolute(value) ? value : path.join(cwd, value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function displayPath(value) {
|
|
70
|
+
return String(value).replace(/[\\/]+/g, "/");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function unquoteYamlScalar(value) {
|
|
74
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
75
|
+
return value.slice(1, -1);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|