@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.
- package/CHANGELOG.md +196 -0
- package/LICENSE +21 -0
- package/README.md +593 -0
- package/dist/addressability.d.ts +245 -0
- package/dist/addressability.d.ts.map +1 -0
- package/dist/addressability.js +567 -0
- package/dist/addressability.js.map +1 -0
- package/dist/checker.d.ts +159 -0
- package/dist/checker.d.ts.map +1 -0
- package/dist/checker.js +177 -0
- package/dist/checker.js.map +1 -0
- package/dist/cli.d.ts +149 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1049 -0
- package/dist/cli.js.map +1 -0
- package/dist/copy-gate.d.ts +107 -0
- package/dist/copy-gate.d.ts.map +1 -0
- package/dist/copy-gate.js +132 -0
- package/dist/copy-gate.js.map +1 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +64 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/locale-coverage.d.ts +227 -0
- package/dist/locale-coverage.d.ts.map +1 -0
- package/dist/locale-coverage.js +414 -0
- package/dist/locale-coverage.js.map +1 -0
- package/dist/passage.d.ts +294 -0
- package/dist/passage.d.ts.map +1 -0
- package/dist/passage.js +463 -0
- package/dist/passage.js.map +1 -0
- package/dist/path-exclusions.d.ts +114 -0
- package/dist/path-exclusions.d.ts.map +1 -0
- package/dist/path-exclusions.js +213 -0
- package/dist/path-exclusions.js.map +1 -0
- package/dist/registry.d.ts +61 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +64 -0
- package/dist/registry.js.map +1 -0
- package/dist/resolve.d.ts +33 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +133 -0
- package/dist/resolve.js.map +1 -0
- package/dist/scan.d.ts +457 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +1698 -0
- package/dist/scan.js.map +1 -0
- package/dist/schema.d.ts +81 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +482 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +268 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +40 -0
- package/dist/types.js.map +1 -0
- package/dist/voice/checker.d.ts +186 -0
- package/dist/voice/checker.d.ts.map +1 -0
- package/dist/voice/checker.js +483 -0
- package/dist/voice/checker.js.map +1 -0
- package/dist/voice/derivation-coverage.d.ts +168 -0
- package/dist/voice/derivation-coverage.d.ts.map +1 -0
- package/dist/voice/derivation-coverage.js +181 -0
- package/dist/voice/derivation-coverage.js.map +1 -0
- package/dist/voice/fields.d.ts +107 -0
- package/dist/voice/fields.d.ts.map +1 -0
- package/dist/voice/fields.js +144 -0
- package/dist/voice/fields.js.map +1 -0
- package/dist/voice/index.d.ts +19 -0
- package/dist/voice/index.d.ts.map +1 -0
- package/dist/voice/index.js +24 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/internal/parse-template.d.ts +27 -0
- package/dist/voice/internal/parse-template.d.ts.map +1 -0
- package/dist/voice/internal/parse-template.js +111 -0
- package/dist/voice/internal/parse-template.js.map +1 -0
- package/dist/voice/internal/pattern-safety.d.ts +111 -0
- package/dist/voice/internal/pattern-safety.d.ts.map +1 -0
- package/dist/voice/internal/pattern-safety.js +278 -0
- package/dist/voice/internal/pattern-safety.js.map +1 -0
- package/dist/voice/schema.d.ts +65 -0
- package/dist/voice/schema.d.ts.map +1 -0
- package/dist/voice/schema.js +535 -0
- package/dist/voice/schema.js.map +1 -0
- package/dist/voice/types.d.ts +282 -0
- package/dist/voice/types.d.ts.map +1 -0
- package/dist/voice/types.js +37 -0
- package/dist/voice/types.js.map +1 -0
- package/package.json +72 -0
- package/src/addressability.ts +664 -0
- package/src/checker.ts +293 -0
- package/src/cli.ts +1166 -0
- package/src/copy-gate.ts +197 -0
- package/src/fingerprint.ts +66 -0
- package/src/index.ts +163 -0
- package/src/locale-coverage.ts +512 -0
- package/src/passage.ts +644 -0
- package/src/path-exclusions.ts +265 -0
- package/src/registry.ts +103 -0
- package/src/resolve.ts +172 -0
- package/src/scan.ts +1959 -0
- package/src/schema.ts +528 -0
- package/src/types.ts +301 -0
- package/src/voice/checker.ts +590 -0
- package/src/voice/derivation-coverage.ts +217 -0
- package/src/voice/fields.ts +160 -0
- package/src/voice/index.ts +55 -0
- package/src/voice/internal/parse-template.ts +114 -0
- package/src/voice/internal/pattern-safety.ts +336 -0
- package/src/voice/schema.ts +585 -0
- package/src/voice/types.ts +329 -0
- 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
|
+
}
|