@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
package/src/types.ts ADDED
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Plain TypeScript types for @clossys/writer's contract: one
3
+ * addressable, reviewable piece of copy (`CopyEntry`) and the record that
4
+ * groups a consumer's whole set of them (`CopyRecord`). Pure data — no
5
+ * validation logic lives here (see `schema.ts`) and no I/O (see
6
+ * `registry.ts`).
7
+ *
8
+ * This is the FROZEN CONTRACT this package and its sibling scanner/gate
9
+ * half (built in parallel, against the exact same shape) were both
10
+ * compiled against. Do not add, remove, rename, or reshape a field here
11
+ * without updating both halves in lockstep — a silent drift here breaks
12
+ * the other half's compile, not this package's own tests.
13
+ *
14
+ * No runtime schema library. That follows this repository's own
15
+ * precedent, restated once more because it matters more here than
16
+ * anywhere else: `@example/catalog`, `@example/policy`,
17
+ * `@example/ui/tokens`, and `@clossys/writer/voice` all ship zero
18
+ * runtime dependencies; only `@example/ui` carries any, and only
19
+ * because it wraps React primitives it genuinely cannot hand-roll. This
20
+ * package's entire job is dependency-free data shape validation — the
21
+ * same job `@example/strategy`'s `validation.ts` and
22
+ * `@clossys/writer/voice`'s `schema.ts` already do, in plain type
23
+ * guards, for their own shapes. `schema.ts` follows that pattern closely
24
+ * rather than pulling in a schema library (and its own major-version
25
+ * churn, a real cost for a *public* package's consumers) for what one
26
+ * file of type guards already covers.
27
+ *
28
+ * This file ships no example content of anyone's real copy. Every field
29
+ * here is either required-and-generic or a structural placeholder —
30
+ * filling in an actual product's actual sentences is a consumer's job.
31
+ * See "The single most important constraint" in the README: this package
32
+ * is to `@clossys/writer/voice` what `@example/ui` is to
33
+ * `@example/ui/tokens` — a vocabulary layer over a contract layer,
34
+ * never a source of real words. If a doc comment, a test fixture, or a
35
+ * README example in this package ever reads like something a product
36
+ * would actually show a user, that is a bug in this package, not a
37
+ * feature of it.
38
+ */
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // CopyEntryId — the stable address of one entry
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /**
45
+ * A stable address for one `CopyEntry`, unique within its `CopyRecord`.
46
+ * Dot-separated, lowercase, kebab-case within each segment — e.g.
47
+ * `"pagination.no-results"`, `"onboarding.step-2.title"`. At least one
48
+ * dot is required: a bare, unnamespaced id like `"title"` is exactly the
49
+ * kind of id that collides the moment a second feature also needs a
50
+ * `"title"` entry, which is the whole reason this package asks for a
51
+ * namespaced, dot-separated address rather than an arbitrary free-text
52
+ * string. See `schema.ts`'s `COPY_ENTRY_ID_RE` for the exact pattern this
53
+ * shape is validated against.
54
+ *
55
+ * A plain `string` alias, not a branded/nominal type: this package has no
56
+ * runtime mechanism to keep a caller from constructing one by hand
57
+ * outside `validateCopyRecordShape`/`parseCopyRecord`, so pretending
58
+ * otherwise at the type level would be a false promise. The alias exists
59
+ * purely to make every signature below say what kind of string it means.
60
+ */
61
+ export type CopyEntryId = string;
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // CopyRef and resolvable registry metadata
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * A locale tag supplied by the consumer (normally a BCP 47 tag such as
69
+ * `en` or `fr-CA`). It remains a plain string because locale support is a
70
+ * consumer policy: copy must not bundle a second internationalisation stack.
71
+ */
72
+ export type CopyLocale = string;
73
+
74
+ /** Values permitted when a `CopyRef` fills a registered `{placeholder}`. */
75
+ export type CopyValue = string | number | boolean;
76
+
77
+ /**
78
+ * The stable, serialisable way another package asks copy for audience-facing
79
+ * text. Surface documents carry this value instead of a prose literal.
80
+ *
81
+ * `locale` is a request, not a fallback algorithm. The resolver owns the
82
+ * fallback policy and fails closed when no compatible registry is available.
83
+ */
84
+ export interface CopyRef {
85
+ id: CopyEntryId;
86
+ locale?: CopyLocale;
87
+ values?: Readonly<Record<string, CopyValue>>;
88
+ }
89
+
90
+ /** Lifecycle state of a registry entry. Only approved entries can resolve. */
91
+ export type CopyEntryStatus = "draft" | "approved" | "retired";
92
+
93
+ /**
94
+ * Consumer-supplied provenance for a resolvable copy registry. `reference`
95
+ * is deliberately opaque: it may be a revision-control object, CMS revision,
96
+ * or editorial record, without Foundry importing that system.
97
+ */
98
+ export interface CopySource {
99
+ kind: "consumer" | "generated" | "imported";
100
+ reference: string;
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // CopyEntry — one addressable, reviewable piece of copy
105
+ // ---------------------------------------------------------------------------
106
+
107
+ /**
108
+ * One piece of copy a consumer's product shows a user, made addressable,
109
+ * reviewable, and checkable. This package never populates `text` with
110
+ * anything real — every `CopyEntry` a consumer's own repository registers
111
+ * is that repository's own words, the same way a `VoiceRecord`'s
112
+ * `glossary` and `claims` are never this package's words either (see
113
+ * `@clossys/writer/voice`'s README, "The single most important
114
+ * constraint").
115
+ */
116
+ export interface CopyEntry {
117
+ /** Stable address. Unique within the `CopyRecord` it belongs to — see `CopyEntryId`. */
118
+ id: CopyEntryId;
119
+ /** The copy itself — a consumer's own words, never this package's. */
120
+ text: string;
121
+ /**
122
+ * Where this copy appears — a screen name, a component, a route, a
123
+ * notification type. Required, and deliberately not optional: an entry
124
+ * with no stated location cannot be found by a human reviewer skimming
125
+ * a report, which makes it unreviewable in practice even though it is
126
+ * structurally present. "Unlocatable copy is not reviewable copy" is
127
+ * the one-line version of why this field exists at all.
128
+ */
129
+ context: string;
130
+ /**
131
+ * Named interpolations `text` must contain, e.g. `["start", "end",
132
+ * "total"]` for copy like `"Showing {start}–{end} of {total}
133
+ * results"`. Each name in this list is checked against `text` using the
134
+ * literal `{name}` convention — the same brace-delimited placeholder
135
+ * syntax already familiar from ICU MessageFormat and most i18n
136
+ * libraries — by `schema.ts`'s shape validation: a name declared here
137
+ * but not found as `{name}` in `text` is treated as a real bug (the
138
+ * copy was edited and the interpolation was dropped, or the
139
+ * placeholder list drifted from the actual sentence), not decoration.
140
+ * See `schema.ts` for exactly how this is checked, and this package's
141
+ * README for why this is presence-detection over an already-authored
142
+ * string, not a formatting engine: `Intl.PluralRules`,
143
+ * `Intl.ListFormat`, `Intl.RelativeTimeFormat`, and
144
+ * `Intl.NumberFormat.prototype.formatRange` already exist natively in
145
+ * Node 20+ for the actual formatting work, and this package does not
146
+ * re-wrap any of them.
147
+ *
148
+ * Optional — an entry with no interpolations at all (most copy) simply
149
+ * omits this field; `parseCopyRecord` defaults it to `[]`.
150
+ */
151
+ placeholders?: string[];
152
+ /**
153
+ * A plain string into a consumer's own facts registry — never a typed
154
+ * import, never a runtime dependency on `@example/strategy`.
155
+ * This mirrors exactly how `@clossys/writer/voice`'s `Claim.factRef`
156
+ * refers to a `strategy` package's facts without importing it: the
157
+ * coupling is an opaque string convention, not a code import, so this
158
+ * package works today, before `strategy` (or a consumer's own facts
159
+ * registry) exists at all, and keeps working unchanged after it does.
160
+ * Resolving a `factRef` against a real facts registry is a later gate's
161
+ * job, one with visibility into both this package's entries and that
162
+ * registry — deliberately not this package's, and not
163
+ * `@clossys/writer/voice`'s either. See the README, "The `factRef`
164
+ * seam", for the fuller argument (borrowed near-verbatim from that
165
+ * package's own README section of the same name, because it is the
166
+ * same seam, one layer over).
167
+ */
168
+ factRef?: string;
169
+ }
170
+
171
+ /**
172
+ * Content-derived provenance for one target-locale translation. Present only
173
+ * on a target-locale `CopyRegistryEntry` — a source-locale entry has nothing
174
+ * to be "translated from" and never carries this field. See
175
+ * `computeCopyFingerprint` below and `locale-coverage.ts`'s doc comment for
176
+ * why this is a fingerprint of the source text, never a human-maintained
177
+ * revision number: a hand-bumped counter requires someone to remember to
178
+ * bump it every time source copy changes, and nothing enforces that
179
+ * discipline, so it silently drifts out of sync. A content hash cannot drift
180
+ * the same way — it changes if and only if the text it was computed from
181
+ * changed.
182
+ */
183
+ export interface CopyTranslationProvenance {
184
+ /**
185
+ * A fingerprint of the SOURCE entry's `text`, computed with
186
+ * `fingerprintAlgorithm`, at the moment this translation was produced.
187
+ * Compared against a fresh `computeCopyFingerprint(sourceEntry.text)` by
188
+ * `checkLocaleCoverage` to detect staleness — see that file's doc
189
+ * comment for the full comparison.
190
+ */
191
+ sourceFingerprint: string;
192
+ /**
193
+ * The algorithm `sourceFingerprint` was computed with, e.g. `"sha256"` —
194
+ * matches `COPY_FINGERPRINT_ALGORITHM`, the constant `computeCopyFingerprint`
195
+ * (`fingerprint.ts`) is documented to use. Declared explicitly, per entry,
196
+ * so a future algorithm change is a detectable mismatch rather than a
197
+ * silent comparison between two fingerprints computed two different ways.
198
+ */
199
+ fingerprintAlgorithm: string;
200
+ /** ISO-8601 timestamp of when this translation was produced. */
201
+ translatedAt: string;
202
+ /**
203
+ * Opaque translator/vendor-job identifier — never a personal name. This
204
+ * package ships no real identity data, per its own "no real product
205
+ * sentences" constraint (see this file's top-of-file doc comment).
206
+ */
207
+ translatedBy?: string;
208
+ /** ISO-8601 timestamp of when this translation was last reviewed, if it has been. */
209
+ reviewedAt?: string;
210
+ /** Opaque reviewer identifier, mirroring `translatedBy` — never a personal name. */
211
+ reviewedBy?: string;
212
+ }
213
+
214
+ /** A `CopyEntry` that is eligible to participate in a rendered registry. */
215
+ export interface CopyRegistryEntry extends CopyEntry {
216
+ status: CopyEntryStatus;
217
+ /**
218
+ * Content-derived translation provenance, present only on a target-locale
219
+ * entry that has recorded it. Optional: absent on every source-locale
220
+ * entry, and absent on any target-locale entry authored before this field
221
+ * existed or by a host that has not adopted it yet — an existing registry
222
+ * does not become invalid overnight. See `locale-coverage.ts` for how
223
+ * presence/absence of this field changes what `checkLocaleCoverage` can
224
+ * report: an entry with `translation` gets a real stale/not-stale
225
+ * verdict; an entry without it gets `"locale-coverage:provenance-missing"`
226
+ * instead, which is a DIFFERENT outcome from "checked and not stale" —
227
+ * never collapsed into one signal.
228
+ */
229
+ translation?: CopyTranslationProvenance;
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // CopyRecord — one consumer's whole registered set of entries
234
+ // ---------------------------------------------------------------------------
235
+
236
+ /**
237
+ * One consumer's complete, registered set of copy entries. This is the
238
+ * "brand.css" of this package — foundry ships the schema this conforms
239
+ * to and the machinery to check it, never a real instance of it. See the
240
+ * README, "The single most important constraint".
241
+ */
242
+ export interface CopyRecord {
243
+ id: string;
244
+ entries: CopyEntry[];
245
+ }
246
+
247
+ /**
248
+ * A versioned, locale-specific `CopyRecord` that can safely resolve
249
+ * `CopyRef`s for an audience-facing surface. The older `CopyRecord` remains
250
+ * the deliberately minimal scanner/checker input; resolution requires this
251
+ * stronger contract so a manifest never pretends unversioned text is traced.
252
+ */
253
+ export interface CopyRegistry extends CopyRecord {
254
+ locale: CopyLocale;
255
+ revision: string;
256
+ source: CopySource;
257
+ entries: CopyRegistryEntry[];
258
+ }
259
+
260
+ /** A successful, fully traced resolution of one copy reference. */
261
+ export interface CopyResolution {
262
+ ref: CopyRef;
263
+ text: string;
264
+ recordId: string;
265
+ revision: string;
266
+ locale: CopyLocale;
267
+ source: CopySource;
268
+ entryId: CopyEntryId;
269
+ }
270
+
271
+ /** Resolver shape for callers that need rendered audience text. */
272
+ export type CopyResolver = (ref: CopyRef) => CopyResolution | undefined;
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // CopyFinding — shared shape, mirroring @clossys/writer/voice's own `VoiceFinding`
276
+ // ---------------------------------------------------------------------------
277
+
278
+ /**
279
+ * One thing `schema.ts`'s shape validation found wrong with a candidate
280
+ * `CopyRecord` or `CopyEntry`. Deliberately the same shape as
281
+ * `@clossys/writer/voice`'s `VoiceFinding` (itself deliberately the same
282
+ * shape as `@example/policy`'s `Finding`) — `rule` / `severity` /
283
+ * `message` / optional `path` — so a caller already handling one kind of
284
+ * finding in this repository's ecosystem does not need a second mental
285
+ * model for this package's. Defined fresh here, not imported: this
286
+ * file's own zero-runtime-dependency requirement applies to
287
+ * `@clossys/writer/voice` too — `types.ts` and `schema.ts` do not import
288
+ * from it. (`checker.ts` does depend on `@clossys/writer/voice`, for
289
+ * `checkCopy` itself — see that file's own doc comment for why that
290
+ * dependency is confined there.)
291
+ */
292
+ export interface CopyFinding {
293
+ /** Stable identifier for the rule that produced this finding, e.g. `"id-shape"`, `"placeholder-missing-from-text"`. */
294
+ rule: string;
295
+ /** Always `"error"` from `schema.ts` — a malformed `CopyRecord` is never merely a warning. */
296
+ severity: "error" | "warning";
297
+ /** Human-readable description of the problem. */
298
+ message: string;
299
+ /** The specific field, entry index, or placeholder name this finding is about, when there is a single clear one. */
300
+ path?: string;
301
+ }