@clossys/writer 0.3.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/LICENSE +21 -0
  3. package/README.md +593 -0
  4. package/dist/addressability.d.ts +245 -0
  5. package/dist/addressability.d.ts.map +1 -0
  6. package/dist/addressability.js +567 -0
  7. package/dist/addressability.js.map +1 -0
  8. package/dist/checker.d.ts +159 -0
  9. package/dist/checker.d.ts.map +1 -0
  10. package/dist/checker.js +177 -0
  11. package/dist/checker.js.map +1 -0
  12. package/dist/cli.d.ts +149 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +1049 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/copy-gate.d.ts +107 -0
  17. package/dist/copy-gate.d.ts.map +1 -0
  18. package/dist/copy-gate.js +132 -0
  19. package/dist/copy-gate.js.map +1 -0
  20. package/dist/fingerprint.d.ts +61 -0
  21. package/dist/fingerprint.d.ts.map +1 -0
  22. package/dist/fingerprint.js +64 -0
  23. package/dist/fingerprint.js.map +1 -0
  24. package/dist/index.d.ts +47 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +63 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/locale-coverage.d.ts +227 -0
  29. package/dist/locale-coverage.d.ts.map +1 -0
  30. package/dist/locale-coverage.js +414 -0
  31. package/dist/locale-coverage.js.map +1 -0
  32. package/dist/passage.d.ts +294 -0
  33. package/dist/passage.d.ts.map +1 -0
  34. package/dist/passage.js +463 -0
  35. package/dist/passage.js.map +1 -0
  36. package/dist/path-exclusions.d.ts +114 -0
  37. package/dist/path-exclusions.d.ts.map +1 -0
  38. package/dist/path-exclusions.js +213 -0
  39. package/dist/path-exclusions.js.map +1 -0
  40. package/dist/registry.d.ts +61 -0
  41. package/dist/registry.d.ts.map +1 -0
  42. package/dist/registry.js +64 -0
  43. package/dist/registry.js.map +1 -0
  44. package/dist/resolve.d.ts +33 -0
  45. package/dist/resolve.d.ts.map +1 -0
  46. package/dist/resolve.js +133 -0
  47. package/dist/resolve.js.map +1 -0
  48. package/dist/scan.d.ts +457 -0
  49. package/dist/scan.d.ts.map +1 -0
  50. package/dist/scan.js +1698 -0
  51. package/dist/scan.js.map +1 -0
  52. package/dist/schema.d.ts +81 -0
  53. package/dist/schema.d.ts.map +1 -0
  54. package/dist/schema.js +482 -0
  55. package/dist/schema.js.map +1 -0
  56. package/dist/types.d.ts +268 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/dist/types.js +40 -0
  59. package/dist/types.js.map +1 -0
  60. package/dist/voice/checker.d.ts +186 -0
  61. package/dist/voice/checker.d.ts.map +1 -0
  62. package/dist/voice/checker.js +483 -0
  63. package/dist/voice/checker.js.map +1 -0
  64. package/dist/voice/derivation-coverage.d.ts +168 -0
  65. package/dist/voice/derivation-coverage.d.ts.map +1 -0
  66. package/dist/voice/derivation-coverage.js +181 -0
  67. package/dist/voice/derivation-coverage.js.map +1 -0
  68. package/dist/voice/fields.d.ts +107 -0
  69. package/dist/voice/fields.d.ts.map +1 -0
  70. package/dist/voice/fields.js +144 -0
  71. package/dist/voice/fields.js.map +1 -0
  72. package/dist/voice/index.d.ts +19 -0
  73. package/dist/voice/index.d.ts.map +1 -0
  74. package/dist/voice/index.js +24 -0
  75. package/dist/voice/index.js.map +1 -0
  76. package/dist/voice/internal/parse-template.d.ts +27 -0
  77. package/dist/voice/internal/parse-template.d.ts.map +1 -0
  78. package/dist/voice/internal/parse-template.js +111 -0
  79. package/dist/voice/internal/parse-template.js.map +1 -0
  80. package/dist/voice/internal/pattern-safety.d.ts +111 -0
  81. package/dist/voice/internal/pattern-safety.d.ts.map +1 -0
  82. package/dist/voice/internal/pattern-safety.js +278 -0
  83. package/dist/voice/internal/pattern-safety.js.map +1 -0
  84. package/dist/voice/schema.d.ts +65 -0
  85. package/dist/voice/schema.d.ts.map +1 -0
  86. package/dist/voice/schema.js +535 -0
  87. package/dist/voice/schema.js.map +1 -0
  88. package/dist/voice/types.d.ts +282 -0
  89. package/dist/voice/types.d.ts.map +1 -0
  90. package/dist/voice/types.js +37 -0
  91. package/dist/voice/types.js.map +1 -0
  92. package/package.json +72 -0
  93. package/src/addressability.ts +664 -0
  94. package/src/checker.ts +293 -0
  95. package/src/cli.ts +1166 -0
  96. package/src/copy-gate.ts +197 -0
  97. package/src/fingerprint.ts +66 -0
  98. package/src/index.ts +163 -0
  99. package/src/locale-coverage.ts +512 -0
  100. package/src/passage.ts +644 -0
  101. package/src/path-exclusions.ts +265 -0
  102. package/src/registry.ts +103 -0
  103. package/src/resolve.ts +172 -0
  104. package/src/scan.ts +1959 -0
  105. package/src/schema.ts +528 -0
  106. package/src/types.ts +301 -0
  107. package/src/voice/checker.ts +590 -0
  108. package/src/voice/derivation-coverage.ts +217 -0
  109. package/src/voice/fields.ts +160 -0
  110. package/src/voice/index.ts +55 -0
  111. package/src/voice/internal/parse-template.ts +114 -0
  112. package/src/voice/internal/pattern-safety.ts +336 -0
  113. package/src/voice/schema.ts +585 -0
  114. package/src/voice/types.ts +329 -0
  115. package/templates/voice-record.template.jsonc +135 -0
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Plain TypeScript types for @clossys/writer/voice's four entities: voice
3
+ * rules, a glossary entry, a claim, and the `VoiceRecord` that binds them
4
+ * together for one consumer. Pure data — no validation logic lives here
5
+ * (see `schema.ts`) and no I/O.
6
+ *
7
+ * No runtime schema library. That follows this repository's own
8
+ * precedent: `@example/catalog`, `@example/policy`, and
9
+ * `@example/ui/tokens` all ship zero runtime dependencies; only
10
+ * `@example/ui` carries any, and only because it wraps React
11
+ * primitives it genuinely cannot hand-roll. `@clossys/writer/voice`'s
12
+ * entire job is dependency-free data shape validation — the same job
13
+ * `@example/policy`'s `validate.ts` already does, in plain type
14
+ * guards, for a smaller shape. `schema.ts` follows that file's pattern
15
+ * closely rather than pulling in a schema library (and its own major-
16
+ * version churn, a real cost for a *public* package's consumers) for what
17
+ * one file of type guards already covers.
18
+ *
19
+ * This file ships no example content of a real voice. Every field here is
20
+ * either required-and-generic or a structural placeholder — filling in an
21
+ * actual person rule, glossary, or claims register is a consumer's job,
22
+ * the same split `@example/ui/tokens` draws between `tokens.css`
23
+ * (neutral machinery) and a consumer's own `brand.css` (real values). See
24
+ * "The single most important constraint" in the README.
25
+ */
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Voice rules — tone, person, tense, formality
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /** Descriptive only. Never mechanically checked — see README. */
32
+ export type FormalityLevel = "casual" | "neutral" | "formal";
33
+
34
+ /**
35
+ * The valid `FormalityLevel` values, in declaration order — exported as a
36
+ * list, mirroring `@example/policy`'s `DIGEST_ALGORITHMS`, so
37
+ * `schema.ts` never hardcodes them as a second, separately-maintained
38
+ * literal check.
39
+ */
40
+ export const FORMALITY_LEVELS: readonly FormalityLevel[] = ["casual", "neutral", "formal"];
41
+
42
+ /**
43
+ * A "person" rule, e.g. "always address the reader as you, never refer to
44
+ * the product/company as I". `description` is for humans — it is never
45
+ * parsed. `forbiddenPronouns` is the ONLY part of this rule the checker
46
+ * actually evaluates: a plain list of pronouns that, if they appear in
47
+ * copy, contradict the rule. Matched case-insensitively, whole-word only —
48
+ * except a single-letter, uppercase entry (e.g. `"I"`), which `checker.ts`
49
+ * matches case-sensitively so it cannot collide with an unrelated
50
+ * lowercase letter (a roman numeral, a loop variable in quoted code).
51
+ *
52
+ * This is deliberately a word list, not a `"first" | "second" | "third"`
53
+ * enum the checker tries to infer grammar from. A word list is auditable —
54
+ * a reader can see exactly what will be flagged — and it is the same
55
+ * mechanism the glossary check already uses, rather than a second,
56
+ * differently-shaped piece of pseudo-grammar logic.
57
+ */
58
+ export interface PersonRule {
59
+ description: string;
60
+ forbiddenPronouns: string[];
61
+ }
62
+
63
+ /**
64
+ * A "tense" rule. Same shape and same honesty as `PersonRule`:
65
+ * `forbiddenMarkers` is a plain word list (e.g. `["will", "shall"]` to
66
+ * steer away from future-tense promises), not an attempt at real
67
+ * grammatical tense parsing — see the README's "what this checker does not
68
+ * attempt" section for why that line is drawn here.
69
+ */
70
+ export interface TenseRule {
71
+ description: string;
72
+ forbiddenMarkers: string[];
73
+ }
74
+
75
+ /**
76
+ * The full rules block of a `VoiceRecord`. `person` and `tense` are
77
+ * mechanically checkable (via their word lists, when populated).
78
+ * `formality` and `tone` are metadata for a human writer/reviewer only —
79
+ * the checker never reads them. See README, "Be honest about what is and
80
+ * is not mechanically checkable."
81
+ */
82
+ export interface VoiceRules {
83
+ person: PersonRule;
84
+ tense: TenseRule;
85
+ formality: FormalityLevel;
86
+ /** Free-text tone descriptors, e.g. `["warm", "direct", "no jargon"]`. */
87
+ tone: string[];
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Glossary — forbidden / preferred terms
92
+ // ---------------------------------------------------------------------------
93
+
94
+ export type GlossaryStatus = "forbidden" | "preferred";
95
+
96
+ /** The valid `GlossaryStatus` values, in declaration order. See `FORMALITY_LEVELS`'s doc comment for why this is a list, not a repeated literal check. */
97
+ export const GLOSSARY_STATUSES: readonly GlossaryStatus[] = ["forbidden", "preferred"];
98
+
99
+ /**
100
+ * One glossary entry. Only `status: "forbidden"` entries are ever actively
101
+ * scanned for by the checker — see README for why `"preferred"` entries are
102
+ * documentation-only (detecting "did you use the sanctioned synonym" would
103
+ * require real synonym/paraphrase detection, which this package does not
104
+ * attempt and will not claim to).
105
+ */
106
+ export interface GlossaryEntry {
107
+ term: string;
108
+ status: GlossaryStatus;
109
+ /** Required — a forbidden or preferred term with no stated reason is not auditable. */
110
+ reason: string;
111
+ /** Suggested replacement, shown in a finding's message when `status` is `"forbidden"`. */
112
+ alternative?: string;
113
+ /** Default `false` — most house-style terms are meant to be caught regardless of capitalization. */
114
+ caseSensitive: boolean;
115
+ /** See `VoiceChannel`'s doc comment. Omitted (the default) means this entry applies regardless of channel. */
116
+ channel?: VoiceChannel;
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Channel scoping
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /**
124
+ * A consumer-supplied channel tag (e.g. `"linkedin"`, `"x"`, `"hn"`) a rule
125
+ * can be scoped to, so different channels can carry different banned sets —
126
+ * the same seam `CopyLocale` (`../types.ts`) draws for locale, and the same
127
+ * plain-opaque-string pattern `Claim.factRef`/`CopyEntry.factRef` draw for
128
+ * `strategy`'s facts registry. This package does NOT define what a channel
129
+ * IS, does not enumerate valid channels, and never interprets one beyond an
130
+ * exact string-equality comparison against `VoiceCheckOptions.channel` (see
131
+ * `checker.ts`) — that taxonomy is entirely consumer policy. Validated for
132
+ * SHAPE only (a non-empty, non-whitespace-only string) — see `schema.ts`'s
133
+ * `validateChannelShape`.
134
+ */
135
+ export type VoiceChannel = string;
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Severity — three tiers, and what each one means for CI
139
+ // ---------------------------------------------------------------------------
140
+
141
+ /**
142
+ * Three severity tiers, replacing the previous two-value `"error" | "warning"`
143
+ * union (still both present, unchanged in meaning — this is a strictly
144
+ * additive change; see `VOICE_SEVERITIES`' own note on backward
145
+ * compatibility). Every tier is reported identically in `VoiceFinding[]`;
146
+ * what differs is what a CALLER should do with each one, which this package
147
+ * documents explicitly rather than leaving implicit (see the README,
148
+ * "Severity tiers and the exit-code mapping"):
149
+ *
150
+ * - `"error"` — FAILS CI. This package's own documented idiom
151
+ * (`report.findings.some(f => f.severity === "error")`) treats this,
152
+ * and only this, tier as build-breaking. Unchanged from before this
153
+ * addition: every finding this package's OWN dimensions (glossary,
154
+ * person, claims) already produced at `"error"` still does.
155
+ * - `"warning"` — fails only a NARROWER, editorial gate — a stricter,
156
+ * consumer-owned check (e.g. "block merge to a marketing branch", "flag
157
+ * for a copy reviewer") that is not this package's CI idiom and that
158
+ * this package does not implement. The tense dimension already produces
159
+ * `"warning"` findings; nothing about their behavior changes here — this
160
+ * is a documentation clarification of a name that was already in use,
161
+ * not a new runtime behavior for existing findings.
162
+ * - `"advisory"` — purely informational. Never fails anything, including
163
+ * the narrower editorial gate above. Exists so a genuinely optional
164
+ * style nudge (worth a human's attention, never worth blocking on) has
165
+ * a home that isn't a `"warning"` a consumer's own tooling might
166
+ * mistakenly treat as gate-worthy.
167
+ *
168
+ * Only `PatternRule.severity` (below) requires an author to pick one of
169
+ * these explicitly today — every other dimension's severity remains
170
+ * hardcoded by `checker.ts`, exactly as before.
171
+ */
172
+ export type VoiceSeverity = "error" | "warning" | "advisory";
173
+
174
+ /** The valid `VoiceSeverity` values, in documented-precedence order (most to least CI-blocking). See `FORMALITY_LEVELS`'s doc comment for why this is a list, not a repeated literal check. */
175
+ export const VOICE_SEVERITIES: readonly VoiceSeverity[] = ["error", "warning", "advisory"];
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Claims register
179
+ // ---------------------------------------------------------------------------
180
+
181
+ /**
182
+ * One claim a consumer's copy might make, e.g. "fastest sync in its
183
+ * class". `factRef` is the seam into a separate, not-yet-existent
184
+ * `strategy` package's `facts` registry — see README, "The `factRef`
185
+ * seam", for why it is a plain opaque string and never a typed import.
186
+ *
187
+ * `matchPhrases` are the literal, verbatim phrases the checker searches
188
+ * copy for to decide this claim is being made. When empty, the checker
189
+ * falls back to searching for `text` itself. Matching is literal and
190
+ * case-insensitive — a paraphrase of the claim that never uses one of
191
+ * these phrases is not detected. See README's limits section.
192
+ */
193
+ export interface Claim {
194
+ id: string;
195
+ /** Human-readable statement of the claim, for the register itself and as the default match phrase. */
196
+ text: string;
197
+ matchPhrases: string[];
198
+ /**
199
+ * Opaque plain-string reference into a consumer's own facts registry.
200
+ * Never validated by this package — see README, "The `factRef` seam".
201
+ */
202
+ factRef?: string;
203
+ /** Default `true`: most claims in a register are there because they need backing. */
204
+ requiresSupport: boolean;
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Pattern rules — regex alongside literal glossary terms
209
+ // ---------------------------------------------------------------------------
210
+
211
+ /**
212
+ * A regex pattern, serialized as its two `RegExp` constructor arguments —
213
+ * NEVER a real `RegExp` instance — because a `VoiceRecord` is checked-in
214
+ * JSON data, not code: `JSON.stringify`/`JSON.parse` round-trips a `RegExp`
215
+ * as `{}`, silently losing both the source and the flags. This package
216
+ * stores the two strings a `new RegExp(source, flags)` call actually needs,
217
+ * and constructs the real object only at validation/check time — see
218
+ * `internal/pattern-safety.ts`.
219
+ */
220
+ export interface VoicePattern {
221
+ /**
222
+ * The regex source, e.g. `"\\u2014"` for a hard em-dash ban, or
223
+ * `"\\b(deep dive|dive deep)\\b"` for alternation, or `"\\bit'?s\\b"` for
224
+ * an optional apostrophe. Never includes the enclosing `/.../` delimiters
225
+ * — this is `RegExp`'s own `source` argument. Restricted at validation
226
+ * time — see `internal/pattern-safety.ts`'s top doc comment for the full,
227
+ * documented regex-safety gate this package applies before ever
228
+ * constructing a real `RegExp` from this string.
229
+ */
230
+ source: string;
231
+ /**
232
+ * Regex flags, restricted to a safe, documented subset (`i`, `u`, `s` —
233
+ * see `internal/pattern-safety.ts`). Omit for no flags. Never include `g`
234
+ * or `y`: this package always does its own match-counting pass and never
235
+ * accepts a caller-supplied global/sticky flag — see that file's doc
236
+ * comment for why.
237
+ */
238
+ flags?: string;
239
+ }
240
+
241
+ /**
242
+ * A regex-based voice rule — the alternation, optional-apostrophe, and
243
+ * punctuation-ban gaps `GlossaryEntry`'s literal term matching cannot
244
+ * express (see the README, "Pattern rules vs. the glossary"). A hard
245
+ * character ban (a U+2014 em dash, say) is just a pattern with no
246
+ * alternation at all — `PatternRule` subsumes that case rather than this
247
+ * package growing a second, distinct "punctuation rule" type.
248
+ *
249
+ * SAFETY. `pattern` is caller-supplied regex, which can hang a scanner via
250
+ * catastrophic backtracking if accepted unbounded. This package's position:
251
+ * bound the pattern AT REGISTRATION TIME, never at run time — there is no
252
+ * runtime timeout anywhere in this package. See
253
+ * `internal/pattern-safety.ts`'s top doc comment for the full, honest
254
+ * accounting of exactly what is rejected and what known gap remains.
255
+ * `schema.ts`'s `validateVoiceRecordShape` and `checker.ts`'s `checkCopy`
256
+ * BOTH run this gate — the former at registration, the latter as
257
+ * defense-in-depth, since `checkCopy` does not otherwise trust its
258
+ * `VoiceRecord` argument (see that file's own doc comment). Either way, a
259
+ * pattern that fails this gate is reported as a real, unmissable,
260
+ * non-waivable `"pattern:invalid-rule"` finding — never a silent skip. See
261
+ * the `types.ts` header comment's core rule: a check that cannot run must
262
+ * fail, not pass.
263
+ */
264
+ export interface PatternRule {
265
+ id: string;
266
+ description: string;
267
+ pattern: VoicePattern;
268
+ severity: VoiceSeverity;
269
+ /** Required — an unauditable pattern rule is exactly the mistake `GlossaryEntry.reason` already guards against. */
270
+ reason: string;
271
+ /** Suggested replacement, shown in a finding's message. */
272
+ alternative?: string;
273
+ /** See `VoiceChannel`'s doc comment. Omitted (the default) means this rule applies regardless of channel. */
274
+ channel?: VoiceChannel;
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // VoiceRecord — one consumer's bound values
279
+ // ---------------------------------------------------------------------------
280
+
281
+ /**
282
+ * One consumer's complete, bound voice: an identifier, its rules, its
283
+ * glossary, its claims register, and (optionally) its regex pattern rules.
284
+ * This is the "brand.css" of this package — foundry ships the schema this
285
+ * conforms to, never a real instance of it. See README, "The single most
286
+ * important constraint".
287
+ *
288
+ * `patterns` is OPTIONAL AT THE TYPE LEVEL — unlike `glossary`/`claims`,
289
+ * which are required fields that `validateVoiceRecordShape` merely treats
290
+ * leniently (`undefined` defaults to `[]`) — specifically so every existing
291
+ * `VoiceRecord` object literal anywhere (this package's own tests, a
292
+ * consumer's already-written code) keeps compiling unchanged. Runtime
293
+ * behavior is identical to `glossary`/`claims` either way: omitted means
294
+ * `[]`, once a value has passed through `parseVoiceRecord`/`buildVoiceRecord`
295
+ * (`schema.ts`). See `checker.ts` for why `checkCopy` itself still reads it
296
+ * as `record.patterns ?? []`, since it does not require a `VoiceRecord` to
297
+ * have been built via `parseVoiceRecord` first.
298
+ */
299
+ export interface VoiceRecord {
300
+ id: string;
301
+ rules: VoiceRules;
302
+ glossary: GlossaryEntry[];
303
+ claims: Claim[];
304
+ patterns?: PatternRule[];
305
+ }
306
+
307
+ // ---------------------------------------------------------------------------
308
+ // Findings — shared shape, mirroring @example/policy's own `Finding`
309
+ // ---------------------------------------------------------------------------
310
+
311
+ /**
312
+ * One thing a validator or the checker found wrong (or, at `"warning"`,
313
+ * worth a look). Deliberately the same shape as `@example/policy`'s
314
+ * `Finding` — `rule` / `severity` / `message` / optional `path` — so a
315
+ * caller already handling one kind of finding in this repo's ecosystem
316
+ * does not need a second mental model for this package's. Defined fresh
317
+ * here, not imported: this package has zero runtime dependency on
318
+ * `@example/policy`, on purpose (see README, "Requirements").
319
+ */
320
+ export interface VoiceFinding {
321
+ /** Stable identifier for the rule that produced this finding, e.g. `"glossary:forbidden-term"`. */
322
+ rule: string;
323
+ /** See `VoiceSeverity`'s own doc comment for what each of the three tiers means for CI and for a narrower editorial gate. */
324
+ severity: VoiceSeverity;
325
+ /** Human-readable description of the problem. */
326
+ message: string;
327
+ /** The specific term/pronoun/marker/claim/pattern id this finding is about, when there is a single clear one. */
328
+ path?: string;
329
+ }
@@ -0,0 +1,135 @@
1
+ // ════════════════════════════════════════════════════════════════════════
2
+ // @clossys/writer/voice · voice-record.template.jsonc
3
+ // ────────────────────────────────────────────────────────────────────────
4
+ // BINDING — the second layer, the exact peer of `@example/ui/tokens`'
5
+ // `brand-template.css`. This is a template, not real content — copy it into
6
+ // your own project and fill in every placeholder:
7
+ //
8
+ // cp node_modules/@clossys/writer/voice/voice-record.template.jsonc \
9
+ // src/voice/acme-app.voice.jsonc
10
+ //
11
+ // then load it (any JSON-with-comments reader, or strip comments yourself)
12
+ // and pass the result through `parseVoiceRecord` before using it — that
13
+ // call is what turns "JSON that happens to have the right keys" into an
14
+ // actual, validated `VoiceRecord`:
15
+ //
16
+ // import { parseVoiceRecord, checkCopy } from "@clossys/writer/voice";
17
+ // const acmeVoice = parseVoiceRecord(JSON.parse(stripJsonComments(raw)));
18
+ // const report = checkCopy(acmeVoice, someCopy);
19
+ // if (!report.bound) {
20
+ // // this record is still (partly) this template, unedited — see below.
21
+ // }
22
+ //
23
+ // WHAT'S HERE, AND WHAT ISN'T
24
+ //
25
+ // Every slot below is one this package considers BINDABLE — a rule VALUE a
26
+ // real voice plausibly wants to state for itself: which pronouns are
27
+ // forbidden, which tense markers, which terms and why, which claims and
28
+ // whether each needs a fact. Every field this package's schema instead
29
+ // considers a rule KIND — that a voice has exactly a `person` rule and a
30
+ // `tense` rule and nothing else, that a glossary entry's `status` is one of
31
+ // `"forbidden"`/`"preferred"` and no third option, that a claim has exactly
32
+ // `id`/`text`/`matchPhrases`/`factRef`/`requiresSupport` and no more fields
33
+ // — is FIXED, shipped once in `src/types.ts`, and deliberately does not
34
+ // appear here as a fill-in-the-blank, the same way `brand-template.css`
35
+ // never lists `--spacing-*` or `--ui-z-*`. See `src/fields.ts`'s header
36
+ // comment for the full accounting of where that line falls and why it
37
+ // falls between two FILES here rather than within this one file the way it
38
+ // runs within `tokens.css` itself.
39
+ //
40
+ // `glossary`, `claims`, and `patterns` are shown below with exactly ONE
41
+ // example entry each, to demonstrate the shape — add as many of your own as
42
+ // your voice actually needs, and delete the example once you have real
43
+ // entries. The per-entry FIELD NAMES (`term`/`status`/`reason`/
44
+ // `alternative`/`caseSensitive`/`channel` for a glossary entry;
45
+ // `id`/`text`/`matchPhrases`/`factRef`/`requiresSupport` for a claim;
46
+ // `id`/`description`/`pattern`/`severity`/`reason`/`alternative`/`channel`
47
+ // for a pattern rule) are fixed — see `src/types.ts` — only the VALUES you
48
+ // put in them are yours. Unlike glossary/claims, `patterns` itself is
49
+ // entirely OPTIONAL — omit the whole array for a voice with no regex rules.
50
+ //
51
+ // THE PLACEHOLDER, AND WHY IT MUST STAY LOUD
52
+ //
53
+ // Every bindable slot below is filled with the literal string
54
+ // `REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER` (exported as `TEMPLATE_PLACEHOLDER`
55
+ // from `src/fields.ts`) rather than left blank or `null`. `checkCopy` scans
56
+ // every string field of the `VoiceRecord` you pass it for this exact value
57
+ // and reports `bound: false`, plus an unmissable `"voice:unbound-placeholder"`
58
+ // ERROR finding for each slot still carrying it, if even one remains — see
59
+ // `checker.ts`'s `findPlaceholderPaths` and the README's "The unbound
60
+ // signal" section for the full reasoning. Leave NO placeholder behind
61
+ // before treating a copy of this file as a real, bound voice.
62
+ // ════════════════════════════════════════════════════════════════════════
63
+ {
64
+ "id": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER",
65
+
66
+ "rules": {
67
+ // ─── PERSON (required) ────────────────────────────────────────────
68
+ "person": {
69
+ "description": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER",
70
+ "forbiddenPronouns": ["REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER"]
71
+ },
72
+
73
+ // ─── TENSE (required) ─────────────────────────────────────────────
74
+ "tense": {
75
+ "description": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER",
76
+ "forbiddenMarkers": ["REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER"]
77
+ },
78
+
79
+ // ─── FORMALITY (required — one of "casual" | "neutral" | "formal",
80
+ // the fixed FormalityLevel enum; the CHOICE of which is yours) ──
81
+ "formality": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER",
82
+
83
+ // ─── TONE (required — free-text descriptors; never mechanically
84
+ // checked, see the README) ─────────────────────────────────────
85
+ "tone": ["REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER"]
86
+ },
87
+
88
+ // ─── GLOSSARY (required — one example entry; add/remove your own) ────
89
+ // `status` is one of the two fixed GlossaryStatus values, not itself a
90
+ // blank to fill in — this example entry picks "forbidden"; a real entry
91
+ // of your own may pick either.
92
+ "glossary": [
93
+ {
94
+ "term": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER",
95
+ "status": "forbidden",
96
+ "reason": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER",
97
+ "alternative": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER",
98
+ "caseSensitive": false
99
+ }
100
+ ],
101
+
102
+ // ─── CLAIMS (required — one example entry; add/remove your own) ──────
103
+ // `factRef` is the opaque seam into a future `strategy` package's facts
104
+ // registry — see the README, "The factRef seam". Leave it out entirely
105
+ // (as here) for a claim that has no fact backing it yet.
106
+ "claims": [
107
+ {
108
+ "id": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER",
109
+ "text": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER",
110
+ "matchPhrases": [],
111
+ "requiresSupport": true
112
+ }
113
+ ],
114
+
115
+ // ─── PATTERNS (OPTIONAL — one example entry; add/remove your own) ────
116
+ // Unlike glossary/claims above, this whole array may be omitted entirely
117
+ // for a voice with no regex rules — see src/types.ts's VoiceRecord doc
118
+ // comment. `pattern.source`/`pattern.flags` are the two RegExp
119
+ // constructor arguments, never a real RegExp (this is checked-in JSON).
120
+ // `severity` is one of the three fixed VoiceSeverity values ("error" |
121
+ // "warning" | "advisory") — see the README, "Severity tiers and the
122
+ // exit-code mapping". `flags`, `alternative`, and `channel` are all
123
+ // optional and left out of this example — see src/types.ts's PatternRule.
124
+ "patterns": [
125
+ {
126
+ "id": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER",
127
+ "description": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER",
128
+ "pattern": {
129
+ "source": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER"
130
+ },
131
+ "severity": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER",
132
+ "reason": "REPLACE_ME__VOICE_TEMPLATE_PLACEHOLDER"
133
+ }
134
+ ]
135
+ }