@tduyng/pi-timbre 0.0.0-stage → 0.0.2
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 +178 -0
- package/README.md +156 -2
- package/package.json +45 -4
- package/src/README.md +385 -0
- package/src/code.ts +154 -0
- package/src/config.ts +21 -0
- package/src/guidance/catalog.md +17 -0
- package/src/guidance/review.md +438 -0
- package/src/guide.ts +26 -0
- package/src/index.ts +695 -0
- package/src/memory.ts +238 -0
- package/src/patterns.ts +255 -0
- package/src/register.ts +35 -0
- package/src/report.ts +205 -0
- package/src/rules.ts +37 -0
- package/src/scan.ts +328 -0
- package/src/segments.ts +81 -0
- package/src/stats.ts +112 -0
package/src/memory.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { createHash } from 'node:crypto'
|
|
5
|
+
import { Validator } from 'ata-validator'
|
|
6
|
+
import { t } from 'ata-validator/t'
|
|
7
|
+
|
|
8
|
+
import type { JsonValue } from '@earendil-works/pi-ai'
|
|
9
|
+
const isBoolean = new Validator(t.boolean()).isValidObject
|
|
10
|
+
const isStringArray = new Validator(t.array(t.string())).isValidObject
|
|
11
|
+
const isString = new Validator(t.string()).isValidObject
|
|
12
|
+
|
|
13
|
+
type JsonObject = { [key: string]: JsonValue }
|
|
14
|
+
|
|
15
|
+
export type Domain = 'prose'
|
|
16
|
+
|
|
17
|
+
export type Sample = {
|
|
18
|
+
at: number
|
|
19
|
+
prevented: boolean
|
|
20
|
+
domain: Domain
|
|
21
|
+
chars: number
|
|
22
|
+
hits: number
|
|
23
|
+
ids: string[]
|
|
24
|
+
cacheRead?: number
|
|
25
|
+
cacheWrite?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type Settings = {
|
|
29
|
+
prevent?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type Notice = {
|
|
33
|
+
at: number
|
|
34
|
+
session: string
|
|
35
|
+
path: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type Memory = {
|
|
39
|
+
version: 1
|
|
40
|
+
samples: Sample[]
|
|
41
|
+
notices?: Notice[]
|
|
42
|
+
settings?: Settings
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const EMPTY: Memory = { version: 1, samples: [], notices: [] }
|
|
46
|
+
const MAX_SAMPLES = 500
|
|
47
|
+
const MAX_NOTICES = 500
|
|
48
|
+
const MAX_MEMORY_BYTES = 2 * 1024 * 1024
|
|
49
|
+
|
|
50
|
+
export function memoryPath(projectRoot: string): string {
|
|
51
|
+
const digest = createHash('sha256').update(projectRoot).digest('hex').slice(0, 16)
|
|
52
|
+
const base = process.env['XDG_STATE_HOME'] || join(homedir(), '.local', 'state')
|
|
53
|
+
return join(base, 'timbre', `${digest}.json`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function load(path: string): Memory {
|
|
57
|
+
try {
|
|
58
|
+
if (statSync(path).size > MAX_MEMORY_BYTES) return { ...EMPTY }
|
|
59
|
+
const raw = readFileSync(path, 'utf-8')
|
|
60
|
+
return parseMemory(JSON.parse(raw) as JsonValue)
|
|
61
|
+
} catch {
|
|
62
|
+
return { ...EMPTY }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const UnknownRecordSchema = t.object({}, { additionalProperties: true })
|
|
67
|
+
const isUnknownRecordSchema = new Validator(UnknownRecordSchema).isValidObject
|
|
68
|
+
const FiniteNonnegativeSchema = t.number({ minimum: 0 })
|
|
69
|
+
const isFiniteNonnegativeSchema = new Validator(FiniteNonnegativeSchema).isValidObject
|
|
70
|
+
const DomainSchema = t.literal('prose')
|
|
71
|
+
const isDomainSchema = new Validator(DomainSchema).isValidObject
|
|
72
|
+
|
|
73
|
+
function isRecord(value: JsonValue | undefined): value is JsonObject {
|
|
74
|
+
return isUnknownRecordSchema(value)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function finiteNonnegative(value: JsonValue | undefined): value is number {
|
|
78
|
+
return isFiniteNonnegativeSchema(value) && Number.isFinite(value)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseSample(value: JsonValue): Sample | undefined {
|
|
82
|
+
if (!isRecord(value)) return undefined
|
|
83
|
+
const at = value['at']
|
|
84
|
+
const prevented = value['prevented']
|
|
85
|
+
const domain = value['domain'] ?? 'prose'
|
|
86
|
+
const chars = value['chars']
|
|
87
|
+
const hits = value['hits']
|
|
88
|
+
if (!finiteNonnegative(at) || !isBoolean(prevented) || !finiteNonnegative(chars) || !finiteNonnegative(hits)) {
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
if (!isDomainSchema(domain)) return undefined
|
|
92
|
+
const ids = isStringArray(value['ids']) ? value['ids'].slice(0, 100) : []
|
|
93
|
+
const sample: Sample = { at, prevented, domain, chars, hits, ids }
|
|
94
|
+
const cacheRead = value['cacheRead']
|
|
95
|
+
const cacheWrite = value['cacheWrite']
|
|
96
|
+
if (finiteNonnegative(cacheRead)) sample.cacheRead = cacheRead
|
|
97
|
+
if (finiteNonnegative(cacheWrite)) sample.cacheWrite = cacheWrite
|
|
98
|
+
return sample
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseNotice(value: JsonValue): Notice | undefined {
|
|
102
|
+
if (!isRecord(value)) return undefined
|
|
103
|
+
const at = value['at']
|
|
104
|
+
const session = value['session']
|
|
105
|
+
const path = value['path']
|
|
106
|
+
if (!finiteNonnegative(at)) return undefined
|
|
107
|
+
if (!isString(session) || !isString(path)) return undefined
|
|
108
|
+
return { at, session, path }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function parseMemory(parsed: JsonValue | undefined): Memory {
|
|
112
|
+
if (!isRecord(parsed) || parsed['version'] !== 1) return { ...EMPTY }
|
|
113
|
+
const rows = Array.isArray(parsed['samples']) ? parsed['samples'].slice(-MAX_SAMPLES) : []
|
|
114
|
+
const noted = Array.isArray(parsed['notices']) ? parsed['notices'].slice(-MAX_NOTICES) : []
|
|
115
|
+
return {
|
|
116
|
+
version: 1,
|
|
117
|
+
samples: evict(rows.map(parseSample).filter((sample): sample is Sample => sample !== undefined)),
|
|
118
|
+
notices: noted.map(parseNotice).filter((notice): notice is Notice => notice !== undefined),
|
|
119
|
+
settings: sanitizeSettings(parsed['settings'])
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sanitizeSettings(raw: JsonValue | undefined): Settings | undefined {
|
|
124
|
+
if (!isRecord(raw)) return undefined
|
|
125
|
+
const settings: Settings = {}
|
|
126
|
+
const prevent = raw['prevent']
|
|
127
|
+
if (prevent === true || prevent === false) settings.prevent = prevent
|
|
128
|
+
return Object.keys(settings).length > 0 ? settings : undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function save(path: string, memory: Memory): void {
|
|
132
|
+
try {
|
|
133
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
134
|
+
const tmp = `${path}.${process.pid}.tmp`
|
|
135
|
+
writeFileSync(tmp, JSON.stringify(memory), 'utf-8')
|
|
136
|
+
renameSync(tmp, path)
|
|
137
|
+
} catch {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function merge(a: Memory, b: Memory): Memory {
|
|
141
|
+
const seen = new Set(a.samples.map(key))
|
|
142
|
+
const samples = [...a.samples, ...b.samples.filter((s) => !seen.has(key(s)))].toSorted((x, y) => x.at - y.at)
|
|
143
|
+
const mineNotices = a.notices ?? []
|
|
144
|
+
const theirsNotices = b.notices ?? []
|
|
145
|
+
const seenNotices = new Set(mineNotices.map(noticeKey))
|
|
146
|
+
const notices = [...mineNotices, ...theirsNotices.filter((x) => !seenNotices.has(noticeKey(x)))]
|
|
147
|
+
return {
|
|
148
|
+
version: 1,
|
|
149
|
+
samples: evict(samples),
|
|
150
|
+
notices: evictNotices(notices),
|
|
151
|
+
settings: b.settings ?? a.settings
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function key(s: Sample): string {
|
|
156
|
+
return `${s.at}:${s.prevented}:${s.domain}:${s.chars}:${s.hits}`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function record(memory: Memory, ids: string[], sample: Omit<Sample, 'ids'>): Memory {
|
|
160
|
+
return { ...memory, samples: evict([...memory.samples, { ...sample, ids }]) }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function noticeKey(n: Notice): string {
|
|
164
|
+
return `${n.session}:${n.path}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function appendNotice(memory: Memory, notice: Notice): Memory {
|
|
168
|
+
return { ...memory, notices: evictNotices([...(memory.notices ?? []), notice]) }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function noticed(memory: Memory, session: string, path: string): boolean {
|
|
172
|
+
return (memory.notices ?? []).some((n) => n.session === session && n.path === path)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function evictNotices(notices: Notice[]): Notice[] {
|
|
176
|
+
return notices.length <= MAX_NOTICES ? notices : notices.slice(-MAX_NOTICES)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function evict(samples: Sample[]): Sample[] {
|
|
180
|
+
if (samples.length <= MAX_SAMPLES) return samples
|
|
181
|
+
const on = samples.filter((s) => s.prevented)
|
|
182
|
+
const off = samples.filter((s) => !s.prevented)
|
|
183
|
+
const [small, large] = on.length <= off.length ? [on, off] : [off, on]
|
|
184
|
+
|
|
185
|
+
const smallKeep = Math.min(small.length, Math.floor(MAX_SAMPLES / 2))
|
|
186
|
+
const largeKeep = MAX_SAMPLES - smallKeep
|
|
187
|
+
return [...small.slice(-smallKeep), ...large.slice(-largeKeep)].toSorted((a, b) => a.at - b.at)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function tallyIds(samples: Sample[]): Map<string, number> {
|
|
191
|
+
const counts = new Map<string, number>()
|
|
192
|
+
for (const s of samples) for (const id of s.ids) counts.set(id, (counts.get(id) ?? 0) + 1)
|
|
193
|
+
return counts
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export type Effect = {
|
|
197
|
+
withPrevention: { samples: number; density: number }
|
|
198
|
+
withoutPrevention: { samples: number; density: number }
|
|
199
|
+
delta: number | null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function effect(memory: Memory, domain: Domain = 'prose'): Effect {
|
|
203
|
+
const rows = memory.samples.filter((s) => s.domain === domain)
|
|
204
|
+
const on = rows.filter((s) => s.prevented)
|
|
205
|
+
const off = rows.filter((s) => !s.prevented)
|
|
206
|
+
const onD = density(on)
|
|
207
|
+
const offD = density(off)
|
|
208
|
+
return {
|
|
209
|
+
withPrevention: { samples: on.length, density: onD },
|
|
210
|
+
withoutPrevention: { samples: off.length, density: offD },
|
|
211
|
+
delta:
|
|
212
|
+
off.length === 0 || on.length === 0
|
|
213
|
+
? null
|
|
214
|
+
: offD === 0
|
|
215
|
+
? onD === 0
|
|
216
|
+
? 0
|
|
217
|
+
: -100
|
|
218
|
+
: ((offD - onD) / offD) * 100
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function density(xs: Sample[]): number {
|
|
223
|
+
const chars = xs.reduce((a, s) => a + s.chars, 0)
|
|
224
|
+
const hits = xs.reduce((a, s) => a + s.hits, 0)
|
|
225
|
+
return chars === 0 ? 0 : (hits / chars) * 1000
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export type CacheSummary = { turns: number; avgCacheRead: number; avgCacheWrite: number }
|
|
229
|
+
|
|
230
|
+
export function cacheSummary(memory: Memory, prevented: boolean): CacheSummary | null {
|
|
231
|
+
const turns = memory.samples.filter(
|
|
232
|
+
(s) => s.domain === 'prose' && s.prevented === prevented && s.cacheRead !== undefined
|
|
233
|
+
)
|
|
234
|
+
if (turns.length === 0) return null
|
|
235
|
+
const reads = turns.reduce((a, s) => a + (s.cacheRead ?? 0), 0)
|
|
236
|
+
const writes = turns.reduce((a, s) => a + (s.cacheWrite ?? 0), 0)
|
|
237
|
+
return { turns: turns.length, avgCacheRead: reads / turns.length, avgCacheWrite: writes / turns.length }
|
|
238
|
+
}
|
package/src/patterns.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
export type Tier = 'forensic' | 1 | 2
|
|
2
|
+
export type Scope = 'prose' | 'url'
|
|
3
|
+
|
|
4
|
+
export type Pattern = {
|
|
5
|
+
id: string
|
|
6
|
+
tier: Tier
|
|
7
|
+
re: RegExp
|
|
8
|
+
hint: string
|
|
9
|
+
scope?: Scope
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const PATTERNS: Pattern[] = [
|
|
13
|
+
{
|
|
14
|
+
id: 'tool-leakage',
|
|
15
|
+
tier: 'forensic',
|
|
16
|
+
scope: 'url',
|
|
17
|
+
re: /citeturn\d|contentReference\[oaicite:|oai_citation|attributableIndex|utm_source=(chatgpt\.com|openai|copilot\.com|perplexity)|referrer=grok\.com/i,
|
|
18
|
+
hint: 'Strip the citation token or tracking parameter.'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'invisible-unicode',
|
|
22
|
+
tier: 'forensic',
|
|
23
|
+
scope: 'url',
|
|
24
|
+
re: /\u200B|\u200C|\u200D|\u2060|\uFEFF|\u00AD/,
|
|
25
|
+
hint: 'Delete the zero-width or soft-hyphen character.'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'placeholder',
|
|
29
|
+
tier: 'forensic',
|
|
30
|
+
re: /\[(your name|insert [a-z ]+|company name|todo)\]|\b20\d\d-XX-XX\b/i,
|
|
31
|
+
hint: 'Fill the placeholder or cut the line.'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'reasoning-artifact',
|
|
35
|
+
tier: 'forensic',
|
|
36
|
+
re: /^\s*(first, i'?ll\b|let me think\b|breaking this down\b)/i,
|
|
37
|
+
hint: 'Delete the scaffolding, keep the conclusion.'
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
id: 'em-dash',
|
|
42
|
+
tier: 1,
|
|
43
|
+
re: /—|–/,
|
|
44
|
+
hint: 'Comma, period, colon, or parentheses.'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'semicolon',
|
|
48
|
+
tier: 1,
|
|
49
|
+
re: /(?<!&#\d{1,6}|&#x[0-9a-f]{1,5}|&[a-z][a-z0-9]{1,30});/i,
|
|
50
|
+
hint: 'Split into two sentences.'
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'curly-quote',
|
|
54
|
+
tier: 1,
|
|
55
|
+
re: /[‘’“”…]/,
|
|
56
|
+
hint: 'Straight quotes, three periods.'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'emoji',
|
|
60
|
+
tier: 1,
|
|
61
|
+
re: /[\u{1F300}-\u{1FAFF}\u{1F900}-\u{1F9FF}]/u,
|
|
62
|
+
hint: 'Remove the decoration.'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'bold-label-bullet',
|
|
66
|
+
tier: 1,
|
|
67
|
+
re: /^\s*[-*+]\s+\*\*[^*]{1,40}:\*\*/,
|
|
68
|
+
hint: 'Write it as prose, or drop the bold label.'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'list-label-period',
|
|
72
|
+
tier: 1,
|
|
73
|
+
re: /^\s*[-*+]\s+\*\*[^*]{1,40}\.\*\*\s+\S/,
|
|
74
|
+
hint: 'Use a colon, not a period, on a list label. Or write it as prose.'
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
id: 'ai-vocabulary',
|
|
79
|
+
tier: 1,
|
|
80
|
+
re: /\b(tapestry|nestled|boasts|bustling|myriad|leverage|leveraging|delve|delving|garner|garnering|holistic|multifaceted|synergy|treasure trove|utilize|utilizing|facilitate|facilitating|elucidate|endeavor|encompass|encompassing|paradigm|catalyze|catalyzing|juxtapose|realm|plethora|galvanize|galvanizing|epitomize|epitomizing|unravel|unraveling|catapult|catapulting|conceptualize|supercharge|supercharging|spearhead|spearheading|commence|commencing|top[- ]notch)\b/i,
|
|
81
|
+
hint: 'Use the plain word. "utilize" is always "use".'
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
id: 'negative-parallelism',
|
|
86
|
+
tier: 1,
|
|
87
|
+
re: /\bit(?:'?s| is) not (just|merely)\b|\bnot only\b.{0,60}\bbut also\b|\b(?:isn'?t|aren'?t|wasn'?t|weren'?t)\b[^.\n]{2,60},\s*(?:it|that)'s\b/i,
|
|
88
|
+
hint: 'State the positive claim alone.'
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'throat-clearing',
|
|
92
|
+
tier: 1,
|
|
93
|
+
re: /\bhere(?:'?s| is) the thing\b|\bin today'?s\b|\bat its core\b|\bit(?:'?s| is) worth noting\b|\bit(?:'?s| is) important to (note|remember|highlight|mention)\b|\bthe real question is\b|\bwhen it comes to\b|\bat the end of the day\b|\bneedless to say\b|\bit turns out\b|\bthe truth is\b|\blet me be clear\b|\bin a nutshell\b|\brest assured\b|\bit goes without saying\b/i,
|
|
94
|
+
hint: 'Delete it and state the point.'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'signposting',
|
|
98
|
+
tier: 1,
|
|
99
|
+
re: /\blet'?s (dive|explore|break (this|it) down)|\bdelv(e|ing) into\b|\bwithout further ado\b/i,
|
|
100
|
+
hint: 'Delete the announcement, keep the content.'
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'mannered-flourish',
|
|
104
|
+
tier: 1,
|
|
105
|
+
re: /\bearn(?:s|ed)? (?:its|their) keep\b|\bpull(?:s|ed)? (?:its|their) (?:own )?weight\b|\b(?:does|doing|did) the heavy lifting\b|\bmoves? the needle\b/i,
|
|
106
|
+
hint: 'Say the literal thing: what it does, or why it matters.'
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'chat-artifact',
|
|
110
|
+
tier: 1,
|
|
111
|
+
re: /\bI hope this helps\b|\bwould you like me to\b|\bgreat question\b|\byou'?re absolutely right\b|\bfeel free to\b|\bdon'?t hesitate to\b|\bI love that you asked\b|\bhappy to help\b|\bi'?d be happy to\b|\blet me know if\b|\bcertainly!\b|\bof course!\b|\bthat'?s a great point\b|\byou'?re asking about\b|\bto answer your question\b/i,
|
|
112
|
+
hint: 'Delete the assistant chatter, answer directly.'
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
id: 'claudism',
|
|
117
|
+
tier: 1,
|
|
118
|
+
re: /\bworth sitting with\b|\bkeep arriving at\b|\bthe question (that kept|that keeps|i keep) coming (up|back to)\b|\bhere'?s where it gets interesting\b|\bdouble-click on\b|\bthroughline\b|\bthrough-line\b|\bthe whole (game|ballgame)\b|\bthat'?s the entire point\b|\bthe only thing that (matters|mattered|happened|changed)\b|\bworth (talking about|drawing|making|examining|considering|exploring)\b|\b(which |that )?brings (me|us) back to\b|\bwe'?ve seen this movie before\b|\bthe thing that (got me|hooked me|got to me)\b|\bdispatches from\b|\bi can'?t stop thinking about\b|\bthat'?s the tell\b|\bthe tell is\b|\bshed light on\b|\bpave(s|d)? the way\b|\bit cannot be overstated\b|\bthis is where\b.{0,40}\bcomes in\b/i,
|
|
119
|
+
hint: 'Pseudo-reflective framing. State the thing directly.'
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
{
|
|
123
|
+
id: 'false-agency',
|
|
124
|
+
tier: 1,
|
|
125
|
+
re: /\b(?:numbers|results|data|figures) speak for (?:themselves|itself)\b|\btells? a (?:clear )?story\b|\bpaints? a (?:clear |vivid )?picture\b|\bspeaks volumes\b/i,
|
|
126
|
+
hint: 'State the finding yourself. Data does not narrate.'
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'colon-reveal',
|
|
130
|
+
tier: 1,
|
|
131
|
+
re: /\b(?:the )?(?:answer|secret|trick|truth|reality|lesson) (?:is|was|remains)\s*:|\b(?:the )?(?:bottom line|key takeaway|takeaway)\s*:/i,
|
|
132
|
+
hint: 'State the point. The setup colon manufactures a reveal.'
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
{
|
|
136
|
+
id: 'future-narrative',
|
|
137
|
+
tier: 1,
|
|
138
|
+
re: /\bthe (?:next|coming|upcoming)\s+(?:era|decade|chapter|frontier|market cycle|paradigm|iteration|wave|phase|epoch)\b|\bis (?:set|destined|poised)\s+to\s+(?:be|become|define|reshape|redefine|transform)\b/i,
|
|
139
|
+
hint: 'Pick the falsifiable version, or cut it. The template shape is not a prediction.'
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'confidence-calibration',
|
|
143
|
+
tier: 1,
|
|
144
|
+
re: /\b(?:interestingly|surprisingly|notably|fascinatingly|remarkably|unsurprisingly|tellingly)\b/i,
|
|
145
|
+
hint: 'Let the fact speak for itself. Cut the calibration word.'
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'listicle-trench',
|
|
149
|
+
tier: 1,
|
|
150
|
+
re: /\bthe (?:first|second|third) (?:wall|step|pillar|phase|wave|stage|reason|lesson|takeaway) is\b/i,
|
|
151
|
+
hint: 'A hidden numbered list. Make it a list, or weave the points without numbering.'
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
id: 'fronted-participle',
|
|
156
|
+
tier: 2,
|
|
157
|
+
re: /(?:^|(?<=[.!?])\s+)(?:(?!(?:According|Regarding|Concerning|Depending|Seeing|Nothing|Something|Anything|Everything|Morning|Evening|Building|String|During|Ceiling|Meeting|Spring|Ring|Wing|King)\b)[A-Z][a-z]*ing\s+(?:the|a|an|its|their|his|her|our|your|this|that|these|those|on|in|into|with|for|from|at|by|to)|Having\s+[a-z]+(?:ed|ing|en)|By\s+[a-z]+ing)\b[^,.;:!?\n]{0,60}?,\s+(?:the|a|an|this|that|these|those|its|their|our|your|his|her|we|you|they|it|he|she|i)\b/,
|
|
158
|
+
hint: 'Name the actor first. "The team ran the suite" beats "Running the suite, the team".'
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'participial-padding',
|
|
162
|
+
tier: 2,
|
|
163
|
+
re: /,\s+(?:highlighting|underscoring|showcasing|fostering|reflecting|demonstrating|signaling|signalling|ensuring|emphasizing|emphasising|paving the way)\b/i,
|
|
164
|
+
hint: 'The -ing tail adds significance instead of information. Cut it, or make it a sentence.'
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
export const TIER_2_WORDS = [
|
|
169
|
+
'robust',
|
|
170
|
+
'comprehensive',
|
|
171
|
+
'innovative',
|
|
172
|
+
'streamline',
|
|
173
|
+
'empower',
|
|
174
|
+
'enhance',
|
|
175
|
+
'optimize',
|
|
176
|
+
'scalable',
|
|
177
|
+
'profound',
|
|
178
|
+
'cultivate',
|
|
179
|
+
'bolster',
|
|
180
|
+
'cornerstone',
|
|
181
|
+
'groundbreaking',
|
|
182
|
+
'transformative',
|
|
183
|
+
'unprecedented',
|
|
184
|
+
'compelling',
|
|
185
|
+
'versatile',
|
|
186
|
+
'vibrant',
|
|
187
|
+
'hone',
|
|
188
|
+
'tailor',
|
|
189
|
+
'revolutionize',
|
|
190
|
+
'amplify',
|
|
191
|
+
'discern',
|
|
192
|
+
'cutting-edge',
|
|
193
|
+
'pivotal',
|
|
194
|
+
'seamless',
|
|
195
|
+
'harness',
|
|
196
|
+
'unlock',
|
|
197
|
+
'unleash',
|
|
198
|
+
'elevate',
|
|
199
|
+
'foster',
|
|
200
|
+
'meticulous',
|
|
201
|
+
'invaluable',
|
|
202
|
+
'unwavering',
|
|
203
|
+
'bespoke',
|
|
204
|
+
'interplay',
|
|
205
|
+
'intricate',
|
|
206
|
+
'resonate',
|
|
207
|
+
'illuminate',
|
|
208
|
+
'captivate',
|
|
209
|
+
'nuanced',
|
|
210
|
+
'paramount',
|
|
211
|
+
'burgeoning',
|
|
212
|
+
'ubiquitous',
|
|
213
|
+
'aforementioned',
|
|
214
|
+
'pertaining to',
|
|
215
|
+
'noteworthy',
|
|
216
|
+
'delineate',
|
|
217
|
+
'underpin',
|
|
218
|
+
'unveil',
|
|
219
|
+
'double-edged sword',
|
|
220
|
+
'sheds light',
|
|
221
|
+
'strikes a balance',
|
|
222
|
+
'substrate',
|
|
223
|
+
'flywheel',
|
|
224
|
+
'north star',
|
|
225
|
+
'nexus',
|
|
226
|
+
'endgame',
|
|
227
|
+
'gold-plating',
|
|
228
|
+
'vantage point',
|
|
229
|
+
'locus',
|
|
230
|
+
'quietly',
|
|
231
|
+
'genuinely',
|
|
232
|
+
'inherently'
|
|
233
|
+
] as const
|
|
234
|
+
|
|
235
|
+
export const TIER_3_PHRASES = [
|
|
236
|
+
'the integration of',
|
|
237
|
+
'decentralized compute',
|
|
238
|
+
'community-driven',
|
|
239
|
+
'long-term sustainability',
|
|
240
|
+
'seamless integration',
|
|
241
|
+
'cutting-edge solution',
|
|
242
|
+
'robust solution',
|
|
243
|
+
'future-proof',
|
|
244
|
+
'best-in-class',
|
|
245
|
+
'world-class',
|
|
246
|
+
'turn-key',
|
|
247
|
+
'scalable solution',
|
|
248
|
+
'streamlined workflow',
|
|
249
|
+
'powerful tool',
|
|
250
|
+
'blazing fast',
|
|
251
|
+
'state-of-the-art',
|
|
252
|
+
'next-generation'
|
|
253
|
+
] as const
|
|
254
|
+
|
|
255
|
+
export const byId = new Map(PATTERNS.map((p) => [p.id, p]))
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type Register = 'reference' | 'changelog' | 'narrative' | 'chat' | 'unknown'
|
|
2
|
+
|
|
3
|
+
export type Profile = {
|
|
4
|
+
register: Register
|
|
5
|
+
terseIsCorrect: boolean
|
|
6
|
+
voiceAllowed: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const PROFILES = {
|
|
10
|
+
reference: { register: 'reference', terseIsCorrect: true, voiceAllowed: false },
|
|
11
|
+
changelog: { register: 'changelog', terseIsCorrect: true, voiceAllowed: false },
|
|
12
|
+
narrative: { register: 'narrative', terseIsCorrect: false, voiceAllowed: true },
|
|
13
|
+
chat: { register: 'chat', terseIsCorrect: true, voiceAllowed: true },
|
|
14
|
+
unknown: { register: 'unknown', terseIsCorrect: false, voiceAllowed: false }
|
|
15
|
+
} satisfies Record<Register, Profile>
|
|
16
|
+
|
|
17
|
+
const SEG = String.raw`(?:^|/)`
|
|
18
|
+
|
|
19
|
+
const RULES: Array<[RegExp, Register]> = [
|
|
20
|
+
[new RegExp(`${SEG}(blog|posts?|essays?|articles?|drafts?)/`, 'i'), 'narrative'],
|
|
21
|
+
[new RegExp(`${SEG}(CHANGELOG|HISTORY|RELEASE[-_]?NOTES)`, 'i'), 'changelog'],
|
|
22
|
+
[/COMMIT_EDITMSG|\.git\//, 'changelog'],
|
|
23
|
+
[new RegExp(`${SEG}(adr|rfc|spec|specs|reference|api)/`, 'i'), 'reference'],
|
|
24
|
+
[/(README|CONTRIBUTING|AGENTS|CLAUDE)\.mdx?$/i, 'reference'],
|
|
25
|
+
[new RegExp(`${SEG}docs?/`, 'i'), 'reference']
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
export function classify(path: string): Profile {
|
|
29
|
+
for (const [re, register] of RULES) {
|
|
30
|
+
if (re.test(path)) return PROFILES[register]
|
|
31
|
+
}
|
|
32
|
+
return PROFILES.unknown
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const chatProfile = PROFILES.chat
|