@maestroagora/agora 1.2.2 → 1.3.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/.agents/plugins/marketplace.json +21 -21
- package/.claude-plugin/marketplace.json +13 -13
- package/.claude-plugin/plugin.json +21 -21
- package/.codex-plugin/plugin.json +33 -33
- package/LICENSE +21 -21
- package/README.md +58 -5
- package/assets/agora-orbit.svg +158 -158
- package/package.json +58 -55
- package/scripts/install.mjs +400 -400
- package/scripts/voice/check.mjs +175 -0
- package/scripts/voice/features.mjs +359 -0
- package/scripts/voice/gates.mjs +244 -0
- package/scripts/voice/ingest.mjs +226 -0
- package/scripts/voice/lexicon.mjs +162 -0
- package/scripts/voice/pipeline.mjs +186 -0
- package/scripts/voice/profile.mjs +528 -0
- package/scripts/voice-measure.mjs +369 -0
- package/skills/agora/SKILL.md +161 -15
- package/skills/agora/references/agora-craft.md +391 -0
- package/skills/agora/references/agora-marketing.md +653 -23
- package/skills/agora/references/agora-voice.md +262 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// The frozen measurement pipeline.
|
|
2
|
+
//
|
|
3
|
+
// Stylometric values move when the tokenizer, segmenter, or normalization rules
|
|
4
|
+
// change, so a comparison across two pipelines is not a comparison. Every
|
|
5
|
+
// profile records these version strings and `voice check` refuses to score a
|
|
6
|
+
// draft measured under a different one.
|
|
7
|
+
//
|
|
8
|
+
// Raise the version when the behaviour of the corresponding stage changes.
|
|
9
|
+
|
|
10
|
+
import { ABBREVIATIONS } from "./lexicon.mjs";
|
|
11
|
+
|
|
12
|
+
export const TOKENIZER_VERSION = "agora-word/1";
|
|
13
|
+
export const SEGMENTER_VERSION = "agora-sentence/1";
|
|
14
|
+
export const LEXICON_VERSION = "agora-lexicon/1";
|
|
15
|
+
// No dependency parser ships with this package. Clause figures are punctuation
|
|
16
|
+
// and conjunction proxies, labelled as proxies wherever they are reported.
|
|
17
|
+
export const PARSER_VERSION = "none: clause metrics are conjunction proxies";
|
|
18
|
+
|
|
19
|
+
export const PIPELINE = Object.freeze({
|
|
20
|
+
tokenizer: TOKENIZER_VERSION,
|
|
21
|
+
segmenter: SEGMENTER_VERSION,
|
|
22
|
+
lexicon: LEXICON_VERSION,
|
|
23
|
+
parser: PARSER_VERSION,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Typography is addressed by numeric code point throughout. This repository
|
|
27
|
+
// holds a zero U+2014 and zero curly-quote invariant across every file, so no
|
|
28
|
+
// source file may contain the characters it needs to match.
|
|
29
|
+
const CODE_POINTS = {
|
|
30
|
+
curlySingle: [0x2018, 0x2019, 0x201a, 0x201b],
|
|
31
|
+
curlyDouble: [0x201c, 0x201d, 0x201e, 0x201f],
|
|
32
|
+
emDash: [0x2014],
|
|
33
|
+
enDash: [0x2013],
|
|
34
|
+
ellipsis: [0x2026],
|
|
35
|
+
nonBreakingSpace: [0x00a0],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function charactersOf(name) {
|
|
39
|
+
return CODE_POINTS[name].map((point) => String.fromCodePoint(point)).join("");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function characterClass(name) {
|
|
43
|
+
return new RegExp("[" + charactersOf(name) + "]", "g");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const CURLY_SINGLE = characterClass("curlySingle");
|
|
47
|
+
const CURLY_DOUBLE = characterClass("curlyDouble");
|
|
48
|
+
const EM_DASH = characterClass("emDash");
|
|
49
|
+
const EN_DASH = characterClass("enDash");
|
|
50
|
+
const ELLIPSIS_CHARACTER = characterClass("ellipsis");
|
|
51
|
+
const ELLIPSIS_ANY = new RegExp("[" + charactersOf("ellipsis") + "]|\\.\\.\\.", "g");
|
|
52
|
+
const NON_BREAKING_SPACE = characterClass("nonBreakingSpace");
|
|
53
|
+
const WORD_PATTERN_SOURCE = "[\\p{L}\\p{N}]+(?:['-][\\p{L}\\p{N}]+)*";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalize text for measurement. Curly quotes fold to straight ones so that a
|
|
57
|
+
* curly and a straight contraction are the same token. Counts of the original
|
|
58
|
+
* characters are taken before this runs, by countTypography.
|
|
59
|
+
*/
|
|
60
|
+
export function normalize(text) {
|
|
61
|
+
return text
|
|
62
|
+
.normalize("NFC")
|
|
63
|
+
.replace(/\r\n?/g, "\n")
|
|
64
|
+
.replace(CURLY_SINGLE, "'")
|
|
65
|
+
.replace(CURLY_DOUBLE, '"')
|
|
66
|
+
.replace(ELLIPSIS_CHARACTER, "...")
|
|
67
|
+
.replace(NON_BREAKING_SPACE, " ");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Counts of typography the profile records as habits rather than as tokens. */
|
|
71
|
+
export function countTypography(text) {
|
|
72
|
+
return {
|
|
73
|
+
em_dash: (text.match(EM_DASH) || []).length,
|
|
74
|
+
en_dash: (text.match(EN_DASH) || []).length,
|
|
75
|
+
curly_single: (text.match(CURLY_SINGLE) || []).length,
|
|
76
|
+
curly_double: (text.match(CURLY_DOUBLE) || []).length,
|
|
77
|
+
ellipsis: (text.match(ELLIPSIS_ANY) || []).length,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function wordPattern() {
|
|
82
|
+
return new RegExp(WORD_PATTERN_SOURCE, "gu");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hasWord(text) {
|
|
86
|
+
return new RegExp(WORD_PATTERN_SOURCE, "u").test(text);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Word tokens, lowercased. Hyphenated and apostrophed forms stay one token. */
|
|
90
|
+
export function tokenize(text) {
|
|
91
|
+
return (normalize(text).match(wordPattern()) || []).map((token) => token.toLowerCase());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function endsWithAbbreviation(chunk) {
|
|
95
|
+
const trailing = chunk.match(/([\p{L}.]+)\.$/u);
|
|
96
|
+
if (!trailing) return false;
|
|
97
|
+
const candidate = trailing[1].replace(/\.$/, "").toLowerCase();
|
|
98
|
+
if (ABBREVIATIONS.has(candidate)) return true;
|
|
99
|
+
// A single capital letter before a period is an initial, not a full stop.
|
|
100
|
+
return /^\p{Lu}$/u.test(trailing[1]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Split a paragraph into sentences. A terminator ends a sentence when it is
|
|
105
|
+
* followed by whitespace and an opening character, and when the text before it
|
|
106
|
+
* is neither a frozen abbreviation nor an initial.
|
|
107
|
+
*/
|
|
108
|
+
export function segmentSentences(paragraph) {
|
|
109
|
+
const text = normalize(paragraph).replace(/\s+/g, " ").trim();
|
|
110
|
+
if (text === "") return [];
|
|
111
|
+
|
|
112
|
+
const sentences = [];
|
|
113
|
+
let start = 0;
|
|
114
|
+
let index = 0;
|
|
115
|
+
while (index < text.length) {
|
|
116
|
+
if (!".!?".includes(text[index])) {
|
|
117
|
+
index += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
let end = index;
|
|
121
|
+
while (end + 1 < text.length && ".!?".includes(text[end + 1])) end += 1;
|
|
122
|
+
let after = end + 1;
|
|
123
|
+
while (after < text.length && "\"')]".includes(text[after])) after += 1;
|
|
124
|
+
|
|
125
|
+
if (after >= text.length) {
|
|
126
|
+
sentences.push(text.slice(start).trim());
|
|
127
|
+
start = text.length;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
const breaks =
|
|
131
|
+
text[after] === " " &&
|
|
132
|
+
/^["'(\[]?[\p{Lu}\p{N}]/u.test(text.slice(after + 1)) &&
|
|
133
|
+
!endsWithAbbreviation(text.slice(start, end + 1));
|
|
134
|
+
if (breaks) {
|
|
135
|
+
sentences.push(text.slice(start, after).trim());
|
|
136
|
+
start = after + 1;
|
|
137
|
+
}
|
|
138
|
+
index = end + 1;
|
|
139
|
+
}
|
|
140
|
+
if (start < text.length) {
|
|
141
|
+
const tail = text.slice(start).trim();
|
|
142
|
+
if (tail !== "") sentences.push(tail);
|
|
143
|
+
}
|
|
144
|
+
return sentences.filter(hasWord);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Paragraphs are blank-line separated blocks that contain at least one word. */
|
|
148
|
+
export function segmentParagraphs(text) {
|
|
149
|
+
return normalize(text)
|
|
150
|
+
.split(/\n{2,}/)
|
|
151
|
+
.map((block) => block.replace(/\n/g, " ").trim())
|
|
152
|
+
.filter((block) => block !== "" && hasWord(block));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Linear-interpolation percentile, the R-7 definition. Frozen: a different
|
|
157
|
+
* percentile definition moves every reported tail value.
|
|
158
|
+
*/
|
|
159
|
+
export function percentile(sorted, fraction) {
|
|
160
|
+
if (sorted.length === 0) return null;
|
|
161
|
+
if (sorted.length === 1) return sorted[0];
|
|
162
|
+
const position = (sorted.length - 1) * fraction;
|
|
163
|
+
const lower = Math.floor(position);
|
|
164
|
+
const upper = Math.ceil(position);
|
|
165
|
+
if (lower === upper) return sorted[lower];
|
|
166
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function mean(values) {
|
|
170
|
+
if (values.length === 0) return null;
|
|
171
|
+
return values.reduce((total, value) => total + value, 0) / values.length;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function standardDeviation(values) {
|
|
175
|
+
if (values.length < 2) return null;
|
|
176
|
+
const average = mean(values);
|
|
177
|
+
const variance = values.reduce((total, value) => total + (value - average) ** 2, 0) / (values.length - 1);
|
|
178
|
+
return Math.sqrt(variance);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Round to fixed places so repeated runs emit byte-identical output. */
|
|
182
|
+
export function round(value, places = 2) {
|
|
183
|
+
if (value === null || value === undefined || Number.isNaN(value)) return null;
|
|
184
|
+
const factor = 10 ** places;
|
|
185
|
+
return Math.round(value * factor + Number.EPSILON * factor) / factor;
|
|
186
|
+
}
|