@ijfw/memory-server 1.5.5 → 1.6.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.
Files changed (72) hide show
  1. package/bin/ijfw-dashboard +20 -1
  2. package/package.json +4 -3
  3. package/src/audit-roster.js +89 -12
  4. package/src/brain/tiered-llm.js +57 -7
  5. package/src/cross-orchestrator-cli.js +344 -4
  6. package/src/cross-project-search.js +39 -1
  7. package/src/dashboard-server.js +7 -1
  8. package/src/dream/runner.mjs +560 -8
  9. package/src/handlers/brain-handler.js +101 -1
  10. package/src/importers/discover.js +1 -1
  11. package/src/memory/bench-metrics.js +289 -0
  12. package/src/memory/benchmark.js +1 -1
  13. package/src/memory/search.js +53 -1
  14. package/src/orchestrator/plan-checker.js +1 -1
  15. package/src/profile/audit.js +671 -0
  16. package/src/profile/capture.js +871 -0
  17. package/src/profile/derive-dialectic.js +242 -0
  18. package/src/profile/derive-heuristic.js +733 -0
  19. package/src/profile/derive.js +156 -0
  20. package/src/profile/egress.js +306 -0
  21. package/src/profile/eval/build-real-probes.mjs +197 -0
  22. package/src/profile/eval/corpus-from-reddit.mjs +166 -0
  23. package/src/profile/eval/corpus-from-reddit.test.mjs +121 -0
  24. package/src/profile/eval/corpus-from-transcripts.mjs +264 -0
  25. package/src/profile/eval/gate-b-behavior.mjs +420 -0
  26. package/src/profile/eval/gate-b-decision-run.mjs +171 -0
  27. package/src/profile/eval/gate-b-decision-run.test.mjs +141 -0
  28. package/src/profile/eval/gate-b-run.mjs +417 -0
  29. package/src/profile/eval/gate-b-run.test.mjs +204 -0
  30. package/src/profile/eval/gate-c-capture.mjs +323 -0
  31. package/src/profile/eval/harness.mjs +551 -0
  32. package/src/profile/eval/instrument-validation.mjs +248 -0
  33. package/src/profile/eval/instrument-validation.test.mjs +125 -0
  34. package/src/profile/eval/multi-subject-harness.mjs +106 -0
  35. package/src/profile/eval/multi-subject-harness.test.mjs +99 -0
  36. package/src/profile/eval/personas.test.mjs +83 -0
  37. package/src/profile/eval/plumbing.test.mjs +69 -0
  38. package/src/profile/eval/prereg.mjs +130 -0
  39. package/src/profile/eval/prereg.test.mjs +78 -0
  40. package/src/profile/eval/real-corpus.test.mjs +103 -0
  41. package/src/profile/eval/real-personas.mjs +109 -0
  42. package/src/profile/eval/run-real-corpus-concurrent.mjs +407 -0
  43. package/src/profile/eval/run-real-corpus.mjs +358 -0
  44. package/src/profile/eval/slug-quality.mjs +464 -0
  45. package/src/profile/eval/stylometry-features.js +85 -0
  46. package/src/profile/eval/stylometry-reference.js +16 -0
  47. package/src/profile/eval/stylometry.js +224 -0
  48. package/src/profile/eval/stylometry.test.mjs +103 -0
  49. package/src/profile/eval/synthetic-personas.js +91 -0
  50. package/src/profile/eval/verifier-features.mjs +170 -0
  51. package/src/profile/eval/verifier-logreg.mjs +74 -0
  52. package/src/profile/eval/verifier-pair.mjs +122 -0
  53. package/src/profile/eval/verifier-reference.mjs +68 -0
  54. package/src/profile/eval/verifier-scorer.mjs +30 -0
  55. package/src/profile/eval/wrong-target-control.mjs +168 -0
  56. package/src/profile/eval/wrong-target-control.test.mjs +124 -0
  57. package/src/profile/exemplar-capture.js +232 -0
  58. package/src/profile/exemplar-retrieve.js +138 -0
  59. package/src/profile/exemplar-store.js +314 -0
  60. package/src/profile/lock.js +64 -0
  61. package/src/profile/merge.js +624 -0
  62. package/src/profile/path-policy.js +213 -0
  63. package/src/profile/precision-stamp.mjs +151 -0
  64. package/src/profile/render-brief.js +717 -0
  65. package/src/profile/schema.js +244 -0
  66. package/src/profile/sensitivity.js +249 -0
  67. package/src/profile/serve.js +345 -0
  68. package/src/profile/store.js +261 -0
  69. package/src/profile/telemetry.js +289 -0
  70. package/src/recovery/checkpoint.js +7 -1
  71. package/src/server.js +185 -14
  72. package/src/.registry-meta-key.pem +0 -3
@@ -0,0 +1,464 @@
1
+ /**
2
+ * profile/eval/slug-quality.mjs — SLICE S2: slug-quality PRECISION gate.
3
+ *
4
+ * THE DECISION THIS MODULE OWNS: may a learned preference slug EVER auto-inject?
5
+ *
6
+ * The personalization layer is an EVIDENCE-ADMISSION system — nothing becomes
7
+ * "you" without a citation. A derived preference slug is only allowed to
8
+ * auto-act (inject silently, high bar) when the SURFACED slug set is precise
9
+ * enough that we trust it not to put words in the user's mouth. The founder's
10
+ * call is PRECISION >= 0.8 to auto-act; below it we show-and-confirm.
11
+ *
12
+ * This module turns that policy into a measurable, pre-registered gate:
13
+ *
14
+ * 1. LABEL each surfaced slug, three-way, against a HELD-OUT TEST window:
15
+ * - 'correct-actionable-preference' : the slug semantically matches a
16
+ * preference the user expressed in the TEST window (an actionable "you").
17
+ * - 'real-but-not-a-preference' : the slug matches something the user
18
+ * really said in the TEST window, but it is NOT a preference (grounded,
19
+ * but not actionable as a profile directive).
20
+ * - 'wrong' : the slug matches NOTHING in the TEST
21
+ * window — fabricated / noise / hallucinated. Cannot inject, ever.
22
+ *
23
+ * 2. SCORE precision = #correct / #surfaced. Recall is reported HONESTLY
24
+ * (high-precision / low-recall is a feature here, not a bug — a cite-or-drop
25
+ * gate would rather drop a real preference than inject a wrong one).
26
+ *
27
+ * 3. GATE: eligible-to-inject ONLY when precision >= the pre-registered bar
28
+ * (0.8). The bar is frozen + tamper-evident via the SAME prereg rig Gate B
29
+ * uses (buildPreReg / hashPreReg), so the 0.8 cannot be quietly loosened to
30
+ * pass — re-registering after an edit changes the runId hash.
31
+ *
32
+ * CIRCULARITY GUARD (the ea15479 void): the gold here is the HELD-OUT TEST
33
+ * window's preferences — NEVER the train target a brief injects. Scoring a slug
34
+ * against the very phrase the system was handed to inject is teaching-to-the-test
35
+ * and was voided once already. We assert the test ids are disjoint from any
36
+ * provided train ids before scoring; a leaky corpus throws.
37
+ *
38
+ * REUSE, do NOT re-derive:
39
+ * - semanticPrecisionRecall (gate-c-capture.mjs) — lexical-semantic matcher
40
+ * - precisionRecall (harness.mjs) — exact-slug floor + vectors
41
+ * - bootstrapCI (harness.mjs re-export)— CI on the per-unit vectors
42
+ * - buildPreReg / hashPreReg (prereg.mjs) — tamper-evident bar freeze
43
+ * - wrongTargetControl shape (wrong-target-control.mjs) — negative-control idiom
44
+ *
45
+ * Zero deps. ESM. No serving/capture code is touched — this is eval-only infra
46
+ * the runtime CALLS (eligibleSlugsForInjection) but does not live inside.
47
+ *
48
+ * Cites (methodology): LaMP time-based split [2304.11406] (held-out TEST) ·
49
+ * pre-registration tamper-evidence (prereg.mjs, Gate B v2 T6).
50
+ */
51
+
52
+ import crypto from 'node:crypto';
53
+ import { semanticPrecisionRecall } from './gate-c-capture.mjs';
54
+ import { bootstrapCI } from './harness.mjs';
55
+ import { buildPreReg, hashPreReg } from './prereg.mjs';
56
+
57
+ /** The founder's call: precision required to AUTO-act (inject silently). */
58
+ export const SLUG_PRECISION_BAR = 0.8;
59
+ /** Default lexical-semantic match threshold (reused matcher's documented default). */
60
+ export const SLUG_SEMANTIC_THRESHOLD = 0.5;
61
+
62
+ /** The three quality labels a surfaced slug can receive. */
63
+ export const SLUG_LABELS = Object.freeze({
64
+ CORRECT: 'correct-actionable-preference',
65
+ REAL_NOT_PREF: 'real-but-not-a-preference',
66
+ WRONG: 'wrong',
67
+ });
68
+
69
+ /**
70
+ * Normalize a phrase to the SAME subject slug space the heuristic derive uses
71
+ * (phraseToSubject) so a TEST-window gold phrase compares apples-to-apples with a
72
+ * surfaced slug. Identical 5-line rule, re-stated here (derive does not export it)
73
+ * — matches gate-c-capture.toSubject so the two evals are on one scale.
74
+ */
75
+ function toSubject(phrase) {
76
+ return String(phrase || '')
77
+ .toLowerCase()
78
+ .replace(/[^a-z0-9\s]+/g, ' ')
79
+ .replace(/\s+/g, ' ')
80
+ .trim()
81
+ .slice(0, 80);
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // POLARITY GUARD — the precision-gate-specific hardening over the reused Gate C
86
+ // matcher. The lexical-semantic Jaccard match (semanticPrecisionRecall) is
87
+ // POLARITY-BLIND: "use tabs not spaces" and "use spaces not tabs" reduce to the
88
+ // SAME content tokens {tabs, spaces} and so match at Jaccard 1.0. For a CAPTURE-
89
+ // RECALL metric (Gate C) that is tolerable. For a PRECISION gate that decides
90
+ // silent AUTO-INJECTION it is not — labeling the user's OPPOSITE preference as
91
+ // "correct" is exactly the false-positive this gate exists to prevent (it would
92
+ // put the inverse of what the user wants into the profile). So before a semantic
93
+ // match is ALLOWED to count as a correct preference, we reject it when the two
94
+ // phrases share content tokens but disagree on NEGATION polarity.
95
+ //
96
+ // Signal (dependency-free, no antonym lexicon to maintain): for each content
97
+ // token shared by both phrases, record whether it sits BEFORE or AFTER the first
98
+ // negation marker ("not"/"never"/"no"/"avoid"/"instead of"/"rather than") in each
99
+ // phrase. If a shared token flips sides (before in one, after in the other), the
100
+ // two phrases assert OPPOSITE polarity over the same subject — a conflict. This
101
+ // catches the "X not Y" vs "Y not X" inversion without hardcoding domain antonyms.
102
+ // ---------------------------------------------------------------------------
103
+
104
+ const NEGATION_MARKERS = new Set(['not', 'never', 'no', 'avoid', "don't", 'dont', 'without', 'instead', 'rather']);
105
+
106
+ /** Side of the first negation marker each content token sits on: -1 before, 1 after. */
107
+ function negationSides(phrase) {
108
+ const toks = toSubject(phrase).split(/\s+/).filter(Boolean);
109
+ let negAt = -1;
110
+ for (let i = 0; i < toks.length; i += 1) {
111
+ if (NEGATION_MARKERS.has(toks[i])) { negAt = i; break; }
112
+ }
113
+ const sides = new Map();
114
+ for (let i = 0; i < toks.length; i += 1) {
115
+ const t = toks[i];
116
+ if (t.length < 3 || STOPWORDS_LITE.has(t) || NEGATION_MARKERS.has(t)) continue;
117
+ // No negation in the phrase -> neutral side 0 (cannot conflict on its own).
118
+ sides.set(t, negAt === -1 ? 0 : (i < negAt ? -1 : 1));
119
+ }
120
+ return sides;
121
+ }
122
+
123
+ // Small content stoplist shared with the matcher's spirit (function words that
124
+ // would otherwise pollute the polarity comparison). Kept local + minimal.
125
+ const STOPWORDS_LITE = new Set([
126
+ 'the', 'a', 'an', 'to', 'of', 'and', 'or', 'for', 'in', 'on', 'with', 'is',
127
+ 'are', 'be', 'i', 'you', 'user', 'prefer', 'prefers', 'preference', 'like',
128
+ 'likes', 'want', 'wants', 'use', 'uses', 'using', 'should', 'would', 'do',
129
+ 'does', 'my', 'me', 'it', 'this', 'that', 'when', 'always', 'over', 'than',
130
+ ]);
131
+
132
+ /**
133
+ * polarityConflict(a, b) -> boolean. True when a and b share at least one content
134
+ * token that sits on OPPOSITE sides of a negation marker (one before, one after)
135
+ * — i.e. they express opposite preferences over the same subject. Only fires when
136
+ * AT LEAST ONE phrase actually contains a negation (a pure positive-vs-positive
137
+ * pair never conflicts on this signal).
138
+ */
139
+ export function polarityConflict(a, b) {
140
+ const sa = negationSides(a);
141
+ const sb = negationSides(b);
142
+ for (const [tok, side] of sa) {
143
+ if (!sb.has(tok)) continue;
144
+ const other = sb.get(tok);
145
+ // A flip from one side of a negation to the other = opposite polarity.
146
+ if (side !== 0 && other !== 0 && side !== other) return true;
147
+ }
148
+ return false;
149
+ }
150
+
151
+ /**
152
+ * A slug counts as a CORRECT match of a gold subject iff it lexical-semantically
153
+ * matches (reused Gate C matcher) AND does NOT polarity-conflict with it. The
154
+ * polarity guard is the precision-gate hardening; recall-side Gate C keeps its
155
+ * polarity-blind matcher unchanged.
156
+ */
157
+ function correctMatch(slug, goldSubject, threshold) {
158
+ if (semanticPrecisionRecall([slug], [goldSubject], threshold).perPredCorrect[0] !== 1) return false;
159
+ return !polarityConflict(slug, goldSubject);
160
+ }
161
+
162
+ /**
163
+ * Build the PRE-REGISTERED gate config. The 0.8 bar (and the semantic threshold)
164
+ * flow into the prereg hash via the rig's input spread, so any post-hoc edit to
165
+ * the bar is tamper-evident: re-registering with a changed bar yields a different
166
+ * runId hash. We do NOT relax the bar to pass — that was the explicit lesson.
167
+ *
168
+ * @param {object} [opts]
169
+ * @param {number} [opts.bar] precision bar (default 0.8)
170
+ * @param {number} [opts.semanticThreshold] Jaccard threshold (default 0.5)
171
+ * @param {number} [opts.seed] prereg seed (default 1)
172
+ * @returns {object} frozen prereg with .slugPrecisionBar, .slugSemanticThreshold,
173
+ * .hash and .runId. hashPreReg over this object is stable + tamper-evident.
174
+ */
175
+ export function buildSlugQualityPreReg(opts = {}) {
176
+ const bar = Number.isFinite(opts.bar) ? opts.bar : SLUG_PRECISION_BAR;
177
+ const semanticThreshold = Number.isFinite(opts.semanticThreshold)
178
+ ? opts.semanticThreshold : SLUG_SEMANTIC_THRESHOLD;
179
+ const seed = Number.isFinite(opts.seed) ? opts.seed : 1;
180
+
181
+ // Reuse the Gate B rig for the base prereg (seeds + Bonferroni + base hash).
182
+ const base = buildPreReg({
183
+ primaryEndpoint: 'slug-quality-precision-gate',
184
+ slugPrecisionBar: bar,
185
+ slugSemanticThreshold: semanticThreshold,
186
+ seed,
187
+ runId: opts.runId,
188
+ });
189
+
190
+ // buildPreReg only hashes its FIXED field list, so `slugPrecisionBar` /
191
+ // `slugSemanticThreshold` are present on the object but do NOT yet affect
192
+ // base.hash — a silent loosening of the bar would slip through. We make the bar
193
+ // GENUINELY tamper-evident with a COMPOSITE hash over (base prereg hash + the
194
+ // slug-specific knobs). Editing the bar now changes the composite hash and the
195
+ // runId, exactly as the founder's-call requires (the 0.8 cannot be quietly
196
+ // moved to pass). hashPreReg is still reused for the base leg.
197
+ const baseHash = hashPreReg(base);
198
+ const composite = crypto.createHash('sha256')
199
+ .update(`${baseHash}|slugPrecisionBar:${bar}|slugSemanticThreshold:${semanticThreshold}`)
200
+ .digest('hex');
201
+
202
+ return Object.freeze({
203
+ ...base,
204
+ slugPrecisionBar: bar,
205
+ slugSemanticThreshold: semanticThreshold,
206
+ baseHash,
207
+ hash: composite,
208
+ runId: opts.runId || `slugq-${composite.slice(0, 12)}`,
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Assemble the HELD-OUT TEST gold from a corpus's probe/test window. Returns two
214
+ * disjoint slug sets in the same normalized space:
215
+ * - preferenceSubjects : actionable preferences the user expressed (gold +).
216
+ * - nonPreferenceSubjects : things the user really said that are NOT
217
+ * preferences (real-but-not-a-preference targets).
218
+ *
219
+ * Accepts the makeHeldOutFixture corpus shape (probes[].goldSubjects /
220
+ * probes[].nonPreferenceSubjects) OR an explicit { testPreferences,
221
+ * testNonPreferences } pair. The TEST window — never the train target — is the
222
+ * scoring source (anti-circularity).
223
+ */
224
+ export function testWindowGold(corpus = {}) {
225
+ const pref = new Set();
226
+ const nonPref = new Set();
227
+
228
+ if (Array.isArray(corpus.testPreferences)) {
229
+ for (const p of corpus.testPreferences) pref.add(toSubject(p));
230
+ }
231
+ if (Array.isArray(corpus.testNonPreferences)) {
232
+ for (const p of corpus.testNonPreferences) nonPref.add(toSubject(p));
233
+ }
234
+ const probes = Array.isArray(corpus.probes) ? corpus.probes
235
+ : (Array.isArray(corpus.test) ? corpus.test : []);
236
+ for (const probe of probes) {
237
+ for (const g of (probe.goldSubjects || [])) pref.add(toSubject(g));
238
+ for (const g of (probe.nonPreferenceSubjects || probe.nonPrefSubjects || [])) {
239
+ nonPref.add(toSubject(g));
240
+ }
241
+ }
242
+ // A subject can't be both a preference and a non-preference. Preference wins
243
+ // (it is the stronger, actionable claim); drop the overlap from nonPref so the
244
+ // labels are mutually exclusive.
245
+ for (const s of pref) nonPref.delete(s);
246
+ return { preferenceSubjects: [...pref], nonPreferenceSubjects: [...nonPref] };
247
+ }
248
+
249
+ /**
250
+ * labelSlugs(slugs, corpus, opts) -> per-slug quality labels + counts.
251
+ *
252
+ * Each surfaced slug is labeled three-way against the HELD-OUT TEST window:
253
+ * - matches a TEST preference (semantic) -> 'correct-actionable-preference'
254
+ * - else matches a TEST non-preference utterance -> 'real-but-not-a-preference'
255
+ * - else -> 'wrong'
256
+ *
257
+ * Matching reuses the SAME lexical-semantic matcher Gate C uses
258
+ * (semanticPrecisionRecall), so "captured-but-paraphrased" counts and a verbatim
259
+ * coincidence is not required. The preference set is checked FIRST so a slug that
260
+ * is genuinely a preference is never mislabeled as merely-real.
261
+ *
262
+ * @param {string[]} slugs the SURFACED slugs (already at the brief floor)
263
+ * @param {object} corpus carries the TEST window (see testWindowGold)
264
+ * @param {object} [opts] @param {number} [opts.semanticThreshold]
265
+ * @param {string[]} [opts.trainIds] / @param {string[]} [opts.testIds] for the
266
+ * held-out disjointness assertion (throws on leakage).
267
+ * @returns {{ labels: Array<{slug,label,matchedPreference,matchedNonPreference}>,
268
+ * counts: {correct,realNotPref,wrong,total} }}
269
+ */
270
+ export function labelSlugs(slugs = [], corpus = {}, opts = {}) {
271
+ // HELD-OUT ENFORCEMENT (assertion, not comment): if the caller declares both
272
+ // train and test ids, no id may appear on both sides. A leaky split means the
273
+ // gold may include the very target a brief injected — the ea15479 void. Refuse.
274
+ const trainIds = Array.isArray(opts.trainIds) ? opts.trainIds : (corpus.trainIds || []);
275
+ const testIds = Array.isArray(opts.testIds) ? opts.testIds : (corpus.testIds || []);
276
+ if (trainIds.length && testIds.length) {
277
+ const train = new Set(trainIds.map(String));
278
+ for (const id of testIds) {
279
+ if (train.has(String(id))) {
280
+ throw new Error(
281
+ 'slug-quality held-out violation: a TEST id also appears in TRAIN '
282
+ + '(the gold would include the injected train target — the ea15479 circularity). '
283
+ + 'Refusing to label slugs against a leaky split.',
284
+ );
285
+ }
286
+ }
287
+ }
288
+
289
+ const threshold = Number.isFinite(opts.semanticThreshold)
290
+ ? opts.semanticThreshold : SLUG_SEMANTIC_THRESHOLD;
291
+ const { preferenceSubjects, nonPreferenceSubjects } = testWindowGold(corpus);
292
+
293
+ const labels = [];
294
+ let correct = 0; let realNotPref = 0; let wrong = 0;
295
+ for (const raw of slugs) {
296
+ const slug = toSubject(raw);
297
+ // Preference FIRST (the stronger, actionable claim). A correct match must be
298
+ // semantic AND polarity-agreeing — the OPPOSITE preference is NOT correct.
299
+ const matchedPref = firstMatch(slug, preferenceSubjects, threshold);
300
+ if (matchedPref) {
301
+ correct += 1;
302
+ labels.push({
303
+ slug, raw, label: SLUG_LABELS.CORRECT,
304
+ matchedPreference: matchedPref,
305
+ matchedNonPreference: null,
306
+ });
307
+ continue;
308
+ }
309
+ const matchedNonPref = firstMatch(slug, nonPreferenceSubjects, threshold);
310
+ if (matchedNonPref) {
311
+ realNotPref += 1;
312
+ labels.push({
313
+ slug, raw, label: SLUG_LABELS.REAL_NOT_PREF,
314
+ matchedPreference: null,
315
+ matchedNonPreference: matchedNonPref,
316
+ });
317
+ continue;
318
+ }
319
+ wrong += 1;
320
+ labels.push({
321
+ slug, raw, label: SLUG_LABELS.WRONG, matchedPreference: null, matchedNonPreference: null,
322
+ });
323
+ }
324
+
325
+ return {
326
+ labels,
327
+ counts: { correct, realNotPref, wrong, total: slugs.length },
328
+ };
329
+ }
330
+
331
+ /**
332
+ * First TEST-window gold subject a slug CORRECTLY matches (semantic AND
333
+ * polarity-agreeing), for provenance. The OPPOSITE preference is NOT a match.
334
+ */
335
+ function firstMatch(slug, goldSubjects, threshold) {
336
+ for (const g of goldSubjects) {
337
+ if (correctMatch(slug, g, threshold)) return g;
338
+ }
339
+ return null;
340
+ }
341
+
342
+ /**
343
+ * eligibleSlugsForInjection(slugs, corpus, opts) -> the RUNTIME gate.
344
+ *
345
+ * Given the current derived/surfaced slugs, decide which (if any) may auto-inject.
346
+ * The gate is precision-over-the-surfaced-set: a single 'wrong' slug among few can
347
+ * sink precision below 0.8 and block auto-injection — that is the intended
348
+ * safe-by-design behavior (cite-or-drop, high bar to auto-act). We return BOTH the
349
+ * set-level verdict (`cleared`) AND the per-slug labels, so a caller may choose to
350
+ * inject only the 'correct' subset, or fall back to show-and-confirm below the bar.
351
+ *
352
+ * PRECISION counts only 'correct-actionable-preference' as a true positive.
353
+ * 'real-but-not-a-preference' is NOT a preference and so is a precision MISS (it
354
+ * would put a non-preference into the profile) — counted against precision, never
355
+ * eligible to inject. 'wrong' is likewise a miss.
356
+ *
357
+ * RECALL is reported honestly against the TEST preference gold (how many of the
358
+ * user's actual TEST-window preferences the surfaced set recovered). Low recall is
359
+ * acceptable; the gate optimizes precision.
360
+ *
361
+ * @param {string[]} slugs current derived/surfaced slugs
362
+ * @param {object} corpus HELD-OUT TEST window (see testWindowGold)
363
+ * @param {object} [opts]
364
+ * @param {number} [opts.bar] precision bar (default 0.8, pre-registered)
365
+ * @param {number} [opts.semanticThreshold] Jaccard threshold (default 0.5)
366
+ * @param {number} [opts.bootstrapSeed] CI seed (default 42)
367
+ * @param {string[]} [opts.trainIds] / [opts.testIds] held-out leakage check
368
+ * @returns {{
369
+ * bar, preReg:{hash,runId,slugPrecisionBar},
370
+ * cleared:boolean,
371
+ * precision:{point,lo,hi}, recall:{point,lo,hi},
372
+ * eligible:string[], ineligible:string[],
373
+ * labels:Array, counts:{correct,realNotPref,wrong,total},
374
+ * gold:{nPreferences,nNonPreferences}
375
+ * }}
376
+ */
377
+ export function eligibleSlugsForInjection(slugs = [], corpus = {}, opts = {}) {
378
+ const preReg = buildSlugQualityPreReg({
379
+ bar: opts.bar,
380
+ semanticThreshold: opts.semanticThreshold,
381
+ seed: opts.seed,
382
+ runId: opts.runId,
383
+ });
384
+ const bar = preReg.slugPrecisionBar;
385
+ const threshold = preReg.slugSemanticThreshold;
386
+ const seed = Number.isFinite(opts.bootstrapSeed) ? opts.bootstrapSeed : 42;
387
+
388
+ const { labels, counts } = labelSlugs(slugs, corpus, {
389
+ semanticThreshold: threshold,
390
+ trainIds: opts.trainIds,
391
+ testIds: opts.testIds,
392
+ });
393
+
394
+ // PRECISION per-unit vector: 1 iff the slug is a correct actionable preference.
395
+ // (real-but-not-a-preference and wrong both count as 0 — a non-preference in the
396
+ // profile is exactly the harm the gate exists to prevent.)
397
+ const perPredCorrect = labels.map((l) => (l.label === SLUG_LABELS.CORRECT ? 1 : 0));
398
+ const precision = bootstrapCI(perPredCorrect, { seed });
399
+ // point precision is the honest fraction; bootstrapCI.point equals it.
400
+ const precisionPoint = counts.total > 0 ? counts.correct / counts.total : 0;
401
+
402
+ // RECALL against the TEST preference gold. A gold preference is "recovered"
403
+ // only if SOME surfaced slug CORRECTLY matches it (semantic AND polarity-
404
+ // agreeing) — the opposite preference does not count as recovery. perGoldHit is
405
+ // a 0/1 vector over the gold prefs (feeds the same CI helper). Reported honestly
406
+ // (the gate optimizes precision; low recall is acceptable).
407
+ const { preferenceSubjects } = testWindowGold(corpus);
408
+ const slugSubjects = slugs.map(toSubject);
409
+ const perGoldHit = preferenceSubjects.map(
410
+ (g) => (slugSubjects.some((s) => correctMatch(s, g, threshold)) ? 1 : 0),
411
+ );
412
+ const recall = bootstrapCI(perGoldHit, { seed: seed + 1 });
413
+
414
+ // SET-LEVEL verdict: clear the bar (>=, not >) to auto-inject. Use the honest
415
+ // point precision for the decision (the CI is reported alongside, not the gate).
416
+ const cleared = counts.total > 0 && precisionPoint >= bar;
417
+
418
+ // The per-slug eligible subset: the 'correct' slugs are the ones that COULD
419
+ // inject. We surface them regardless of the set verdict so a caller may inject
420
+ // the precise subset even when the whole set doesn't clear — but `cleared`
421
+ // remains the auto-act gate.
422
+ const eligible = labels.filter((l) => l.label === SLUG_LABELS.CORRECT).map((l) => l.raw);
423
+ const ineligible = labels.filter((l) => l.label !== SLUG_LABELS.CORRECT).map((l) => l.raw);
424
+
425
+ return {
426
+ bar,
427
+ preReg: { hash: preReg.hash, runId: preReg.runId, slugPrecisionBar: bar },
428
+ cleared,
429
+ precision: { point: precisionPoint, lo: precision.lo, hi: precision.hi },
430
+ recall: { point: recall.point, lo: recall.lo, hi: recall.hi },
431
+ eligible,
432
+ ineligible,
433
+ labels,
434
+ counts,
435
+ gold: {
436
+ nPreferences: preferenceSubjects.length,
437
+ nNonPreferences: testWindowGold(corpus).nonPreferenceSubjects.length,
438
+ },
439
+ };
440
+ }
441
+
442
+ /**
443
+ * negativeControlClears(slugs, invertedCorpus, opts) -> boolean.
444
+ *
445
+ * The precision-guard control, in the wrong-target-control idiom: slugs scored
446
+ * against an UNRELATED user's TEST window must NOT clear the bar. A gate that
447
+ * lights up on someone else's preferences is matching universals, not the user —
448
+ * a false PASS. Tests assert this returns false for a fabricated/foreign set.
449
+ */
450
+ export function negativeControlClears(slugs = [], invertedCorpus = {}, opts = {}) {
451
+ return eligibleSlugsForInjection(slugs, invertedCorpus, opts).cleared;
452
+ }
453
+
454
+ export default {
455
+ SLUG_PRECISION_BAR,
456
+ SLUG_SEMANTIC_THRESHOLD,
457
+ SLUG_LABELS,
458
+ buildSlugQualityPreReg,
459
+ testWindowGold,
460
+ labelSlugs,
461
+ eligibleSlugsForInjection,
462
+ negativeControlClears,
463
+ hashPreReg,
464
+ };
@@ -0,0 +1,85 @@
1
+ // stylometry-features.js — PURE, dependency-free feature extractors shared by the
2
+ // authorship metric (stylometry.js) and the reference-stats generator
3
+ // (tools/build-stylo-reference.mjs). Kept in its own module so BOTH consumers use the
4
+ // IDENTICAL tokenization, and so the generator never has to import stylometry.js (which
5
+ // imports the generated reference constants — that would be a load-order cycle).
6
+ //
7
+ // Nothing here references the frozen REF_MEAN/REF_SD. These functions only turn text
8
+ // into raw relative-frequency vectors over a given key set. The z-standardization (the
9
+ // step that MUST use frozen constants, never recomputed from the scored text) lives in
10
+ // stylometry.js::authorVector and is the subject of the anti-recompute guard test.
11
+
12
+ // Word tokens: lowercase alphanumeric+apostrophe runs.
13
+ export function tokenizeWords(text) {
14
+ return (String(text).toLowerCase().match(/[a-z0-9']+/gi) || []);
15
+ }
16
+
17
+ // Whitespace-marked character trigrams: collapse runs of whitespace to a single space
18
+ // so word boundaries become part of the trigram signal, then slide a 3-char window.
19
+ export function charTrigrams(text) {
20
+ const s = String(text).toLowerCase().replace(/\s+/g, ' ').trim();
21
+ const out = [];
22
+ for (let i = 0; i + 3 <= s.length; i += 1) out.push(s.slice(i, i + 3));
23
+ return out;
24
+ }
25
+
26
+ // Relative frequency of each function word = count / total words. Returns an array
27
+ // aligned to `funcWords`.
28
+ export function relFreqFunc(text, funcWords) {
29
+ const w = tokenizeWords(text);
30
+ const n = w.length || 1;
31
+ const counts = Object.create(null);
32
+ for (const x of w) counts[x] = (counts[x] || 0) + 1;
33
+ return funcWords.map((fw) => (counts[fw] || 0) / n);
34
+ }
35
+
36
+ // Relative frequency of each trigram key = count / total trigrams. Returns an array
37
+ // aligned to `keys`.
38
+ export function relFreqTrigrams(text, keys) {
39
+ const tris = charTrigrams(text);
40
+ const n = tris.length || 1;
41
+ const counts = Object.create(null);
42
+ for (const t of tris) counts[t] = (counts[t] || 0) + 1;
43
+ return keys.map((k) => (counts[k] || 0) / n);
44
+ }
45
+
46
+ // Relative frequency of each punctuation n-gram = occurrences / total chars. Returns an
47
+ // array aligned to `keys`. Keys may be multi-char (e.g. '...', '?!').
48
+ export function relFreqPunct(text, keys) {
49
+ const s = String(text);
50
+ const denom = s.length || 1;
51
+ return keys.map((k) => {
52
+ let n = 0;
53
+ let idx = 0;
54
+ while ((idx = s.indexOf(k, idx)) !== -1) { n += 1; idx += k.length; }
55
+ return n / denom;
56
+ });
57
+ }
58
+
59
+ // Full trigram count map — used ONLY by the generator to pick the top-K trigram keys
60
+ // from the reference corpus. Never used at scoring time.
61
+ export function trigramCounts(text) {
62
+ const counts = Object.create(null);
63
+ for (const t of charTrigrams(text)) counts[t] = (counts[t] || 0) + 1;
64
+ return counts;
65
+ }
66
+
67
+ // Split a corpus string into `n` roughly equal word-count chunks (for computing
68
+ // per-feature mean/SD ACROSS documents, Burrows's-Delta style).
69
+ export function splitChunks(text, n) {
70
+ const w = tokenizeWordsPreservingRaw(text);
71
+ const per = Math.max(1, Math.floor(w.length / n));
72
+ const chunks = [];
73
+ for (let i = 0; i < n; i += 1) {
74
+ const start = i * per;
75
+ const slice = i === n - 1 ? w.slice(start) : w.slice(start, start + per);
76
+ if (slice.length) chunks.push(slice.join(' '));
77
+ }
78
+ return chunks;
79
+ }
80
+
81
+ // Raw whitespace split (keeps punctuation attached) so chunk text still carries
82
+ // punctuation for the punct features.
83
+ function tokenizeWordsPreservingRaw(text) {
84
+ return String(text).split(/\s+/).filter(Boolean);
85
+ }
@@ -0,0 +1,16 @@
1
+ // AUTO-GENERATED by tools/build-stylo-reference.mjs — DO NOT EDIT BY HAND.
2
+ // Frozen reference statistics that z-standardize the authorship sub-vectors. Derived
3
+ // from a NEUTRAL hand-authored public English baseline, DISJOINT from every Gate-B
4
+ // persona author. Never recomputed from scored text. Regenerate for the confirmatory
5
+ // run: node tools/build-stylo-reference.mjs --corpus <broad-disjoint-corpus>
6
+ /* eslint-disable */
7
+ export const REFERENCE_SOURCE = "neutral-handauthored-baseline-v1 (disjoint from all personas)";
8
+ export const FUNCTION_WORDS = ["the","a","an","this","that","these","those","my","your","his","her","its","our","their","some","any","no","every","each","either","neither","all","both","few","many","much","more","most","several","such","what","which","whose","i","you","he","she","it","we","they","me","him","them","us","who","whom","whoever","anyone","someone","everyone","nobody","anything","something","everything","nothing","myself","yourself","himself","herself","itself","ourselves","themselves","one","ones","mine","yours","hers","ours","theirs","of","in","to","for","with","on","at","by","from","up","about","into","over","after","beneath","under","above","below","between","among","through","during","before","behind","beyond","plus","except","around","near","since","until","against","without","within","along","across","toward","upon","off","out","down","and","but","or","nor","so","yet","because","although","though","while","whereas","unless","whether","if","then","than","as","is","am","are","was","were","be","been","being","have","has","had","do","does","did","will","would","shall","should","can","could","may","might","must","ought","not","very","too","also","just","only","even","still","again","ever","never","always","often","sometimes","here","there","where","when","how","why","now","once","quite","rather","almost","enough","indeed","however","therefore"];
9
+ export const PUNCT_KEYS = [".",",",";",":","!","?","-","—","–","(",")","[","]","\"","'","...","?!","!?","!!","??","--",", ",". ","; "];
10
+ export const TRIGRAM_KEYS = [" th","the","he ","nd ","and"," a "," an","ing"," wh"," of","er ","is "," is","e w","of ","d t"," it"," to"," be","e t","re "," st",", a","at ","en ","it ","one","to ","hat","ng ","st "," ca"," ma","t i","t t","tha"," fo"," ho"," on","are","e s","e, ","es ","le ","n t","s a","s t","t a"," ar"," in"," no"," re",", t","an ","ed ","ent","est","her","ly ","se ","whe"," mo"," pa"," we"," wi"," wo",". t","be ","d a","der","ds ","e a","e b","e i","e o","ere","et ","in ","ind","ion","ll ","ne ","ow ","r t","und","we ","wor","y t"," ha"," le","a s","all","can","e c","e p","f a","hen","how","lea","old","on ","oun","s i","s. ","sto","t, ","th ","thi","tho","tio"," co"," la"," li"," pe"," si"," tr"," wa","ati","att","ce ","con","e h","e m","e r","ear","for","hou","ill","kin","low","nds","ome","ose","ove","rou","s, ","t f","t. ","ter","ton","tte","y a"," do"," fi"," ki"," lo"," ol"," so"," ye",", o","a l","any","ate","ch ","d s","des","e e","e f","end","ers","eve","ge ","han","hin","ice","ide","ise","it,","ith","lde","les","lle","n a","n. ","nin","no ","not","nsi","nt ","ny ","ons","or ","ork","ows","res","s m","s o","s r","sta","t c","t w","ts ","ure","ut ","ven","ver","wha","who","wit"," ac"," ag"," at"," bu"," by"," de"," ea"," fe"," gr"," me"," mu"," pl"," qu"," ri"," ro"," se"," sl"," un",", w",". a",". i",". w","a c","a m","aga","age","ain","ang","ard","aro","ave","by ","car","cro","d b","d l","d o","d, ","de ","din","do ","e d","e. ","ead","ean","eas","ele","ey ","f s","f t","fel"];
11
+ export const REF_MEAN_FUNC = [0.0795491,0.0425523,0,0.0037037,0.012963,0.0037037,0.00362319,0,0,0,0,0,0,0,0,0.00185185,0.00740741,0,0.00185185,0,0,0,0.00185185,0,0.00555556,0.00185185,0.00185185,0.00185185,0,0.00185185,0.00740741,0.00181159,0,0,0,0,0,0.0258857,0.012963,0.0055153,0,0,0.00181159,0.00185185,0.00547504,0,0,0,0,0,0,0,0.00185185,0,0,0,0,0,0,0,0,0,0.00740741,0,0,0,0,0,0,0.0296296,0.009219,0.0221014,0.00555556,0.00555556,0.00555556,0.00185185,0.00555556,0,0,0.00185185,0,0.0037037,0,0,0,0,0,0,0,0.00185185,0,0,0,0.00185185,0,0,0.0055153,0,0,0.00185185,0.00185185,0.00185185,0,0,0.0037037,0.00185185,0,0,0.00185185,0.00185185,0.0351047,0.00185185,0.00185185,0,0.00185185,0.00555556,0,0,0,0.00181159,0,0,0.00185185,0.00185185,0.00555556,0.00555556,0,0.0295894,0,0.00913849,0.00185185,0,0.012963,0,0,0.00185185,0,0,0.00555556,0,0,0.00185185,0,0,0,0.009219,0,0,0,0.0037037,0,0.00185185,0,0,0,0,0.0037037,0.00185185,0.00185185,0.00366345,0,0,0,0.00185185,0,0,0.00555556,0.00185185,0.00555556,0.00925926,0.00181159,0,0,0,0,0,0,0,0,0];
12
+ export const REF_SD_FUNC = [0.0440053,0.0264162,0.0001,0.00828173,0.0142243,0.00828173,0.0120168,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0061419,0.013858,0.0001,0.0061419,0.0001,0.0001,0.0001,0.0061419,0.0001,0.0096225,0.0061419,0.0061419,0.0061419,0.0001,0.0061419,0.013858,0.00600838,0.0001,0.0001,0.0001,0.0001,0.0001,0.0253975,0.0191557,0.00955345,0.0001,0.0001,0.00600838,0.0061419,0.0129887,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0061419,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.013858,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0245676,0.0109088,0.0237814,0.0096225,0.0096225,0.0096225,0.0061419,0.0096225,0.0001,0.0001,0.0061419,0.0001,0.00828173,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0061419,0.0001,0.0001,0.0001,0.0061419,0.0001,0.0001,0.00955345,0.0001,0.0001,0.0061419,0.0061419,0.0061419,0.0001,0.0001,0.00828173,0.0061419,0.0001,0.0001,0.0061419,0.0061419,0.0141744,0.0061419,0.0061419,0.0001,0.0061419,0.0132249,0.0001,0.0001,0.0001,0.00600838,0.0001,0.0001,0.0061419,0.0061419,0.0096225,0.0132249,0.0001,0.020966,0.0001,0.0187945,0.0061419,0.0001,0.0168712,0.0001,0.0001,0.0061419,0.0001,0.0001,0.0096225,0.0001,0.0001,0.0061419,0.0001,0.0001,0.0001,0.0191289,0.0001,0.0001,0.0001,0.00828173,0.0001,0.0061419,0.0001,0.0001,0.0001,0.0001,0.0122838,0.0061419,0.0061419,0.00819231,0.0001,0.0001,0.0001,0.0061419,0.0001,0.0001,0.0096225,0.0061419,0.0132249,0.0142243,0.00600838,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001];
13
+ export const REF_MEAN_TRI = [0.0262081,0.0197752,0.0146636,0.0115133,0.00835129,0.00803939,0.00732562,0.00664172,0.00606836,0.00561603,0.00558707,0.00554066,0.00552094,0.00547383,0.00561603,0.00539653,0.00481424,0.00492965,0.00456318,0.00459663,0.00416959,0.0041447,0.00417769,0.00420407,0.00405605,0.00412779,0.0041609,0.00423076,0.00385096,0.00378708,0.0037749,0.00360926,0.00247826,0.00343663,0.00340436,0.00360291,0.00242611,0.00303332,0.00329417,0.00305784,0.00315133,0.00309155,0.00311913,0.00310203,0.00306396,0.0031014,0.00314394,0.00313747,0.00270622,0.00280604,0.00239966,0.00275446,0.00278363,0.00295621,0.00278929,0.00275477,0.00269392,0.00272885,0.00284828,0.00246671,0.00271408,0.00249953,0.00238731,0.0024527,0.00243153,0.00236626,0.0024487,0.00246899,0.00238354,0.00237798,0.00242567,0.00242069,0.00244214,0.00244875,0.00245042,0.00238705,0.00243167,0.00243351,0.00252937,0.00238049,0.00244905,0.00246368,0.00241778,0.0024729,0.002351,0.00210108,0.00236626,0.00248027,0.00208468,0.00206919,0.00205711,0.00209973,0.00221216,0.00210887,0.00213963,0.00207907,0.00203758,0.00205546,0.00206781,0.00204544,0.00217957,0.0020741,0.00208354,0.00207131,0.00209342,0.00208017,0.00208575,0.00212053,0.00206517,0.00203327,0.00142341,0.0013864,0.00174495,0.00179818,0.00171403,0.00180406,0.00174467,0.00168028,0.00177547,0.00173062,0.00170734,0.00172911,0.00138968,0.00137104,0.00169966,0.0017718,0.00172374,0.00173951,0.0018642,0.00170752,0.00177068,0.00174655,0.00174916,0.0017948,0.00169859,0.0016997,0.00137502,0.00178033,0.0017992,0.00174032,0.00177547,0.00171722,0.00137989,0.00135388,0.00151109,0.0014362,0.00137068,0.00139493,0.00137898,0.0013948,0.00139847,0.00141357,0.00139372,0.00137584,0.00141283,0.00136114,0.00137149,0.00106195,0.00135572,0.00142169,0.00133885,0.00139629,0.00141613,0.00142169,0.0013834,0.00137395,0.00139933,0.00137544,0.0013513,0.00137068,0.00137,0.00141513,0.00139372,0.00135171,0.00138926,0.00137058,0.00136921,0.0014278,0.00102124,0.00141357,0.00137093,0.00144025,0.00135274,0.00132938,0.00137646,0.00144169,0.00141946,0.00106332,0.00135862,0.00147284,0.00136697,0.00136551,0.00139991,0.00137989,0.00138875,0.00138528,0.00133085,0.0013736,0.0013513,0.00105634,0.00102638,0.00103411,0.00104062,0.00102336,0.0010332,0.00102638,0.00110666,0.0010001,0.00101975,0.00102775,0.00104287,0.00103983,0.00102382,0.00072457,0.00105051,0.00104481,0.00100147,0.00101794,0.00109013,0.00104332,0.00102233,0.0010165,0.00105485,0.00102638,0.00104902,0.00102638,0.00104318,0.00101987,0.0010081,0.00104611,0.00102336,0.00103478,0.00103806,0.00101975,0.00104211,0.00103073,0.00101838,0.00105046,0.00104062,0.000680998,0.00105195,0.00103759,0.00102233,0.00103262,0.00102775,0.00104211,0.000684604,0.00105046,0.00106265,0.00110666];
14
+ export const REF_SD_TRI = [0.00895104,0.00900031,0.00851377,0.00593693,0.00241578,0.00500421,0.00345127,0.00276521,0.00704054,0.00465794,0.00564577,0.00383774,0.0038419,0.00531826,0.00465794,0.00562228,0.00474384,0.00462869,0.00280884,0.00441612,0.00409947,0.00336316,0.0017046,0.00342941,0.00514773,0.00295658,0.00370557,0.00468011,0.00362062,0.00469265,0.00416407,0.00410278,0.00364846,0.00329229,0.00328679,0.00406721,0.00205821,0.003331,0.00534869,0.00374798,0.00396759,0.00414992,0.00347914,0.00348068,0.00291142,0.00298574,0.00246607,0.00298131,0.00447818,0.00256421,0.00264854,0.00255534,0.00316297,0.0054157,0.00262453,0.00384367,0.00469174,0.00475669,0.00278822,0.00403313,0.00378129,0.00323093,0.00311064,0.00358161,0.00206243,0.00308014,0.00207656,0.00326886,0.00307596,0.00309519,0.00262671,0.00352913,0.00316073,0.00318579,0.00269444,0.00385127,0.00314259,0.00318193,0.00427975,0.00308827,0.00269362,0.00359892,0.00263369,0.0031869,0.00348487,0.00315884,0.00308014,0.00267407,0.00265415,0.00207086,0.00205803,0.0032092,0.00399446,0.00268487,0.00334536,0.00316669,0.002601,0.0026423,0.00206954,0.00357692,0.00290796,0.00353941,0.002691,0.00269935,0.00266833,0.00319257,0.00209304,0.00212715,0.00314138,0.00310213,0.00201852,0.00196108,0.00267522,0.00213162,0.00203031,0.00213824,0.0026943,0.00258558,0.00365225,0.00315603,0.00202124,0.00261202,0.00261397,0.00193971,0.00262972,0.00269929,0.00265171,0.00268431,0.00398974,0.00258782,0.00210061,0.00363033,0.0026536,0.00212737,0.00260268,0.00347163,0.00194506,0.00211253,0.00366721,0.0026415,0.00365225,0.00203297,0.00195254,0.00252958,0.00395141,0.00267133,0.00351676,0.00262027,0.00258791,0.00311899,0.00197864,0.00200536,0.00261804,0.00194712,0.00199846,0.00192556,0.00194079,0.00184616,0.00191802,0.00201738,0.00189359,0.00260179,0.00262751,0.00201738,0.00309758,0.0025973,0.00258373,0.00258471,0.00191145,0.00351676,0.0019386,0.00267596,0.00197152,0.0019125,0.0019651,0.00257905,0.0025516,0.00265819,0.00176981,0.00200536,0.00193941,0.00267053,0.00191356,0.00339627,0.00194712,0.00204262,0.00201351,0.00184838,0.0019223,0.00283523,0.00193409,0.00252964,0.00198065,0.00195254,0.0025481,0.00195983,0.00247928,0.00255093,0.00191145,0.00251293,0.0017782,0.00179231,0.00249349,0.0017727,0.00246913,0.0017782,0.00191927,0.00173233,0.00176711,0.00178054,0.00180771,0.00244995,0.00177405,0.00162638,0.00181963,0.00181157,0.00173475,0.00176347,0.00189193,0.00180895,0.00177136,0.0017609,0.00251105,0.0017782,0.00250376,0.0017782,0.00247679,0.00176746,0.00174611,0.00181202,0.0017727,0.00179264,0.00179878,0.00176711,0.00180548,0.00178587,0.00176475,0.00250555,0.00249349,0.00152373,0.00182209,0.00179745,0.00177136,0.00178965,0.00178054,0.00180548,0.00153101,0.00181949,0.00184094,0.00191927];
15
+ export const REF_MEAN_PUNCT = [0.00992986,0.0130429,0.000344353,0.000746792,0,0.000672043,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.0130429,0.00959519,0.000344353];
16
+ export const REF_SD_PUNCT = [0.00458435,0.00551939,0.00114209,0.00167133,0.0001,0.00222891,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.0001,0.00551939,0.0048496,0.00114209];