@clossys/writer 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +587 -2
- package/dist/approval-bypass.d.ts +120 -0
- package/dist/approval-bypass.d.ts.map +1 -0
- package/dist/approval-bypass.js +2318 -0
- package/dist/approval-bypass.js.map +1 -0
- package/dist/approval.d.ts +62 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +421 -0
- package/dist/approval.js.map +1 -0
- package/dist/approve-cli.d.ts +82 -0
- package/dist/approve-cli.d.ts.map +1 -0
- package/dist/approve-cli.js +430 -0
- package/dist/approve-cli.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +48 -13
- package/dist/cli.js.map +1 -1
- package/dist/field-options.d.ts +59 -0
- package/dist/field-options.d.ts.map +1 -0
- package/dist/field-options.js +109 -0
- package/dist/field-options.js.map +1 -0
- package/dist/front-door.check.d.ts +2 -0
- package/dist/front-door.check.d.ts.map +1 -0
- package/dist/front-door.check.js +8 -0
- package/dist/front-door.check.js.map +1 -0
- package/dist/front-door.d.ts +82 -0
- package/dist/front-door.d.ts.map +1 -0
- package/dist/front-door.js +271 -0
- package/dist/front-door.js.map +1 -0
- package/dist/generated/advisor-plan-contract.generated.d.ts +3 -0
- package/dist/generated/advisor-plan-contract.generated.d.ts.map +1 -0
- package/dist/generated/advisor-plan-contract.generated.js +430 -0
- package/dist/generated/advisor-plan-contract.generated.js.map +1 -0
- package/dist/generated/contract-schema.generated.d.ts +97 -0
- package/dist/generated/contract-schema.generated.d.ts.map +1 -0
- package/dist/generated/contract-schema.generated.js +496 -0
- package/dist/generated/contract-schema.generated.js.map +1 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -1
- package/dist/messaging-kit.d.ts +113 -0
- package/dist/messaging-kit.d.ts.map +1 -0
- package/dist/messaging-kit.js +121 -0
- package/dist/messaging-kit.js.map +1 -0
- package/dist/passage.d.ts +2 -1
- package/dist/passage.d.ts.map +1 -1
- package/dist/passage.js +2 -1
- package/dist/passage.js.map +1 -1
- package/dist/plan-authority.d.ts +73 -0
- package/dist/plan-authority.d.ts.map +1 -0
- package/dist/plan-authority.js +126 -0
- package/dist/plan-authority.js.map +1 -0
- package/dist/plan-contract.d.ts +16 -0
- package/dist/plan-contract.d.ts.map +1 -0
- package/dist/plan-contract.js +33 -0
- package/dist/plan-contract.js.map +1 -0
- package/dist/plan-digest.d.ts +23 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plan-digest.js +82 -0
- package/dist/plan-digest.js.map +1 -0
- package/dist/plan-rules.d.ts +54 -0
- package/dist/plan-rules.d.ts.map +1 -0
- package/dist/plan-rules.js +145 -0
- package/dist/plan-rules.js.map +1 -0
- package/dist/resolve.d.ts +40 -6
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +144 -18
- package/dist/resolve.js.map +1 -1
- package/dist/scan.d.ts +32 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +67 -6
- package/dist/scan.js.map +1 -1
- package/dist/schema.d.ts +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +209 -5
- package/dist/schema.js.map +1 -1
- package/dist/site-identity.d.ts +88 -0
- package/dist/site-identity.d.ts.map +1 -0
- package/dist/site-identity.js +82 -0
- package/dist/site-identity.js.map +1 -0
- package/dist/types.d.ts +65 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -7
- package/dist/types.js.map +1 -1
- package/package.json +7 -6
- package/skill/SKILL.md +2 -2
- package/src/approval-bypass.ts +2263 -0
- package/src/approval.ts +491 -0
- package/src/approve-cli.ts +474 -0
- package/src/cli.ts +46 -13
- package/src/field-options.ts +134 -0
- package/src/front-door.check.ts +10 -0
- package/src/front-door.ts +312 -0
- package/src/generated/advisor-plan-contract.generated.ts +430 -0
- package/src/generated/contract-schema.generated.ts +520 -0
- package/src/index.ts +97 -1
- package/src/messaging-kit.ts +195 -0
- package/src/passage.ts +2 -1
- package/src/plan-authority.ts +160 -0
- package/src/plan-contract.ts +45 -0
- package/src/plan-digest.ts +81 -0
- package/src/plan-rules.ts +179 -0
- package/src/resolve.ts +185 -19
- package/src/scan.ts +97 -6
- package/src/schema.ts +223 -5
- package/src/site-identity.ts +125 -0
- package/src/types.ts +72 -7
- package/templates/front-door.en.json +426 -0
package/README.md
CHANGED
|
@@ -66,6 +66,8 @@ remains the only required first-day assessment.
|
|
|
66
66
|
`VoiceChannel`.
|
|
67
67
|
- `@clossys/writer/voice-record.template.jsonc` is an annotated,
|
|
68
68
|
unbound template to copy into a consumer repository and fill in.
|
|
69
|
+
- `@clossys/writer/front-door.en.json` is the shipped English front-door
|
|
70
|
+
catalog (`FRONT_DOOR_COPY_EN`) as a `CopyRegistry` JSON file.
|
|
69
71
|
|
|
70
72
|
```ts
|
|
71
73
|
import {
|
|
@@ -107,7 +109,8 @@ registered `CopyRecord`. It exits 0 when clean, 1 when it finds traceability
|
|
|
107
109
|
issues, and 2 when it cannot run.
|
|
108
110
|
|
|
109
111
|
This package does not resolve a claim's `factRef`, infer tone or grammar, or
|
|
110
|
-
ship actual product language
|
|
112
|
+
ship actual product language, except for the 63 English front-door defaults in
|
|
113
|
+
"Front-door copy" below. Those decisions remain with the consumer and the
|
|
111
114
|
product's facts system.
|
|
112
115
|
|
|
113
116
|
## Resolving copy for a surface
|
|
@@ -136,6 +139,51 @@ unknown ID, locale mismatch, draft/retired entry, missing placeholder value,
|
|
|
136
139
|
or unexpected value. Locale fallback belongs to a consumer-owned registry
|
|
137
140
|
selector, rather than an implicit package policy.
|
|
138
141
|
|
|
142
|
+
Both `resolveCopyRef` and `createCopyResolver` take an optional third
|
|
143
|
+
argument, `CopyResolveOptions`:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { createCopyResolver, type CopyRegistry, type CopyResolveOptions } from "@clossys/writer";
|
|
147
|
+
|
|
148
|
+
declare const registry: CopyRegistry;
|
|
149
|
+
const options: CopyResolveOptions = { target: "preview", acceptDelegateInProduction: false, now: new Date() };
|
|
150
|
+
const resolve = createCopyResolver(registry, options);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- `target` (`CopyResolveTarget`, `"preview" | "production"`, default
|
|
154
|
+
`"production"`) — which audience this resolution is for. An owner-approved
|
|
155
|
+
entry resolves on either target. A delegate-approved entry resolves freely
|
|
156
|
+
on `"preview"` but is refused on `"production"`
|
|
157
|
+
(`"delegate-approval-refused"`) unless the caller opts in, with
|
|
158
|
+
`acceptDelegateInProduction: true` or with `approvalPlan`.
|
|
159
|
+
- `acceptDelegateInProduction` (default `false`) — only meaningful when
|
|
160
|
+
`target` is `"production"`; set it to `true` to accept a delegate's
|
|
161
|
+
sign-off as sufficient to publish, not just to preview.
|
|
162
|
+
- `approvalPlan` (`Uint8Array`, optional) — the bytes of an Advisor plan
|
|
163
|
+
record, read by the caller. Consulted only for a delegate-approved entry on
|
|
164
|
+
`"production"` when `acceptDelegateInProduction` is not `true`; see
|
|
165
|
+
"Production authority from an approved plan" below. Passing it together with
|
|
166
|
+
`acceptDelegateInProduction: true`, or passing anything that is not a
|
|
167
|
+
`Uint8Array`, is `"invalid-options"`.
|
|
168
|
+
- `now` (default `new Date()`, evaluated per call) — the clock staleness and
|
|
169
|
+
expiry are measured against; a test fixes it to make an assertion
|
|
170
|
+
deterministic.
|
|
171
|
+
|
|
172
|
+
Three new refusal reasons follow directly from an entry's approval record
|
|
173
|
+
(see "Delegated approval" below for what the record itself contains):
|
|
174
|
+
`"approval-stale"` (the recorded fingerprint no longer matches the entry's
|
|
175
|
+
current text), `"approval-expired"` (a delegate record's `expiresAt` has
|
|
176
|
+
passed), and `"delegate-approval-refused"` (a delegate record on
|
|
177
|
+
`"production"` without `acceptDelegateInProduction: true` or an authorizing
|
|
178
|
+
`approvalPlan`; when a plan was given, the issue's `planRefusal`
|
|
179
|
+
(`CopyResolvePlanRefusal`) says why it did not authorize the entry). A malformed
|
|
180
|
+
`CopyResolveOptions` itself is `"invalid-options"`, checked and refused
|
|
181
|
+
before anything else so a broken options object never silently falls back
|
|
182
|
+
to defaults. An `approved` entry that carries no `approval` record at all
|
|
183
|
+
resolves exactly as it always has, on either target, with no `approval` key
|
|
184
|
+
on the returned `CopyResolution` — this is unchanged, on-by-default
|
|
185
|
+
behavior, not an opt-in.
|
|
186
|
+
|
|
139
187
|
The default `writer-check` command also runs two registry-only gates against
|
|
140
188
|
the same `CopyRegistry` file a surface renders from:
|
|
141
189
|
|
|
@@ -150,6 +198,513 @@ the same `CopyRegistry` file a surface renders from:
|
|
|
150
198
|
gate (built-in defaults for `display-heading`, `eyebrow`, and `button` when
|
|
151
199
|
`maxWords` is omitted).
|
|
152
200
|
|
|
201
|
+
## Site identity copy (`site.name` / `site.tagline`)
|
|
202
|
+
|
|
203
|
+
A site's name and tagline are a reserved copy kind: two entries in the same
|
|
204
|
+
`CopyRegistry` a surface renders from, under the ids `SITE_NAME_COPY_ID`
|
|
205
|
+
(`"site.name"`) and `SITE_TAGLINE_COPY_ID` (`"site.tagline"`, both listed in
|
|
206
|
+
`SITE_IDENTITY_COPY_IDS`). `resolveSiteIdentity` resolves both through
|
|
207
|
+
`resolveCopyRef`, so the approval policy in "Resolving copy for a surface"
|
|
208
|
+
applies to each without a second code path: the entry must be `approved`, a
|
|
209
|
+
recorded approval must still match the entry's current text, a delegate
|
|
210
|
+
approval must not have expired, and a delegate approval is refused on
|
|
211
|
+
`"production"` unless `acceptDelegateInProduction` is set.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { resolveSiteIdentity, type CopyRegistry } from "@clossys/writer";
|
|
215
|
+
|
|
216
|
+
declare const registry: CopyRegistry;
|
|
217
|
+
const result = resolveSiteIdentity(registry, { target: "production", locale: "en" });
|
|
218
|
+
|
|
219
|
+
if (!result.complete) {
|
|
220
|
+
for (const issue of result.issues) console.error(issue.field, issue.reason, issue.message);
|
|
221
|
+
} else if (result.identity) {
|
|
222
|
+
const { name, tagline } = result.identity;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The second argument is `CopyResolveOptions` (`target`, `acceptDelegateInProduction`,
|
|
227
|
+
`now`) plus an optional `locale`, which is passed to the resolver as the
|
|
228
|
+
requested locale of each entry. A malformed options object, or a blank or
|
|
229
|
+
non-string `locale`, is reported as `"invalid-options"` rather than thrown.
|
|
230
|
+
|
|
231
|
+
The result is all-or-nothing. Both entries are always attempted, and every
|
|
232
|
+
problem is returned in one pass, each `SiteIdentityIssue` tagged with the
|
|
233
|
+
`field` (`"name"` or `"tagline"`) it belongs to. `identity` (the two strings)
|
|
234
|
+
and `resolutions` (each field's `CopyResolution`, with its registry, revision,
|
|
235
|
+
locale, source and approval provenance) are present only when both resolved;
|
|
236
|
+
when either fails neither is returned, so a caller cannot end up with half an
|
|
237
|
+
identity. An issue's `reason` is any `CopyResolveIssueReason`, or one of two
|
|
238
|
+
reasons specific to this kind:
|
|
239
|
+
|
|
240
|
+
- `"site-identity-placeholder"` — the entry declares placeholders, or its text
|
|
241
|
+
contains braces the resolver would rewrite. A name and a tagline are literal
|
|
242
|
+
text, not templates.
|
|
243
|
+
- `"site-identity-blank"` — the resolved text is empty after trimming.
|
|
244
|
+
|
|
245
|
+
`resolveSiteIdentity` does not modify the registry and returns the same result
|
|
246
|
+
for the same registry and options (pass `now` to fix the clock). Consuming it
|
|
247
|
+
is a separate change: a publisher page-metadata builder is not part of this
|
|
248
|
+
package, and nothing here reads a brand-facts record.
|
|
249
|
+
|
|
250
|
+
## Messaging kit (`messaging.pitch.*` / `messaging.boilerplate.*`)
|
|
251
|
+
|
|
252
|
+
The short, medium and long "about us" texts a consumer reuses in press,
|
|
253
|
+
footers and email are a reserved copy kind too: six entries in the same
|
|
254
|
+
`CopyRegistry`, under the ids in `MESSAGING_KIT_COPY_IDS`, in this order:
|
|
255
|
+
|
|
256
|
+
- `messaging.pitch.one-liner`, `messaging.pitch.elevator`,
|
|
257
|
+
`messaging.pitch.paragraph`
|
|
258
|
+
- `messaging.boilerplate.short`, `messaging.boilerplate.medium`,
|
|
259
|
+
`messaging.boilerplate.long`
|
|
260
|
+
|
|
261
|
+
`resolveMessagingKit` resolves each through `resolveCopyRef`, so the approval
|
|
262
|
+
policy in "Resolving copy for a surface" applies unchanged and there is no
|
|
263
|
+
second text store or approval path. No copy ships in this package; the words
|
|
264
|
+
are the consumer's own.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import { resolveMessagingKit, type CopyRegistry } from "@clossys/writer";
|
|
268
|
+
|
|
269
|
+
declare const registry: CopyRegistry;
|
|
270
|
+
const result = resolveMessagingKit(registry, { target: "production", locale: "en" });
|
|
271
|
+
|
|
272
|
+
if (!result.complete) {
|
|
273
|
+
for (const issue of result.issues) console.error(issue.field, issue.reason, issue.message);
|
|
274
|
+
} else if (result.kit) {
|
|
275
|
+
const { pitch, boilerplate } = result.kit;
|
|
276
|
+
console.log(pitch.oneLiner, boilerplate.long);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The options are the same as for `resolveSiteIdentity`: `CopyResolveOptions`
|
|
281
|
+
plus an optional `locale`; malformed options are reported as
|
|
282
|
+
`"invalid-options"`, not thrown. The result is all-or-nothing: all six entries
|
|
283
|
+
are attempted, every problem is returned in id order with its `field`
|
|
284
|
+
(`"pitch.oneLiner"` through `"boilerplate.long"`), and `kit` and `resolutions`
|
|
285
|
+
exist only when there are no issues. An issue's `reason` is any
|
|
286
|
+
`CopyResolveIssueReason` (each refusal keeps the resolver's own reason), or
|
|
287
|
+
one of three specific to this kind:
|
|
288
|
+
|
|
289
|
+
- `"messaging-placeholder"` — the entry declares placeholders, or its text
|
|
290
|
+
contains braces the resolver would rewrite.
|
|
291
|
+
- `"messaging-blank"` — the resolved text is empty after trimming.
|
|
292
|
+
- `"messaging-ladder-order"` — `countCopyWords` does not strictly increase
|
|
293
|
+
within a ladder (one-liner, elevator, paragraph; then short, medium, long).
|
|
294
|
+
It is reported on the later field. The two ladders are not compared with
|
|
295
|
+
each other, and there are no other word targets.
|
|
296
|
+
|
|
297
|
+
The placeholder-value reasons of `resolveCopyRef` (`"missing-placeholder-value"`
|
|
298
|
+
and `"unexpected-placeholder-value"`) never surface from this function: a kit
|
|
299
|
+
entry is literal text, so every one of them is reported as
|
|
300
|
+
`"messaging-placeholder"`, once per field.
|
|
301
|
+
|
|
302
|
+
This function judges approval and shape, not wording. For the words of a pitch
|
|
303
|
+
or boilerplate, run `checkCopy` over each resolved text, and run
|
|
304
|
+
`auditClaimsRegister` over the claims register that backs it.
|
|
305
|
+
|
|
306
|
+
An FAQ and a tagline are out of scope (`site.tagline` already exists).
|
|
307
|
+
|
|
308
|
+
## Front-door copy (`front-door.*`)
|
|
309
|
+
|
|
310
|
+
The words on a sign-in page and the pages around it are a reserved copy kind
|
|
311
|
+
too, so each page does not carry its own. An id is
|
|
312
|
+
`front-door.<state>.<slot>`, where the slot is one of `title`, `description`,
|
|
313
|
+
`label`, `primary`, `secondary`, `notice` or `alt`. This version ships 63
|
|
314
|
+
ids, listed in `FRONT_DOOR_COPY_IDS` (typed as `FrontDoorKey`), as
|
|
315
|
+
`FRONT_DOOR_COPY_EN`, a `CopyRegistry` with the id `front-door`, locale `en`
|
|
316
|
+
and revision `1`; the same registry ships as data in
|
|
317
|
+
`@clossys/writer/front-door.en.json`. They are US English and cover these
|
|
318
|
+
states: sign-in, password, code, reset, activation, request-access,
|
|
319
|
+
forgot-password, identifier-not-found, the field notices (identifier-required,
|
|
320
|
+
password-required, code-required, network), the status notices (unavailable,
|
|
321
|
+
rate-limited, locked, expired, signed-out), the weak-password and
|
|
322
|
+
name-required notices, and the pages error, not-found,
|
|
323
|
+
not-authorized, access-pending and service-unavailable, plus an internal-note
|
|
324
|
+
label:
|
|
325
|
+
|
|
326
|
+
| Id | Text |
|
|
327
|
+
| --- | --- |
|
|
328
|
+
| `front-door.sign-in.title` | Sign in |
|
|
329
|
+
| `front-door.sign-in.description` | Continue to {surface}. |
|
|
330
|
+
| `front-door.sign-in.label` | Email |
|
|
331
|
+
| `front-door.sign-in.primary` | Continue |
|
|
332
|
+
| `front-door.password.title` | Enter your password |
|
|
333
|
+
| `front-door.password.description` | Signing in as {identifier}. |
|
|
334
|
+
| `front-door.password.label` | Password |
|
|
335
|
+
| `front-door.password.primary` | Sign in |
|
|
336
|
+
| `front-door.password.secondary` | Use a different email |
|
|
337
|
+
| `front-door.identifier-not-found.notice` | We couldn’t find an account for that email. Check it and try again. |
|
|
338
|
+
| `front-door.forgot-password.label` | Forgot password? |
|
|
339
|
+
| `front-door.password.notice` | That password isn’t right. Try again or reset it. |
|
|
340
|
+
| `front-door.sign-in.alt` | Sign in to {brand} |
|
|
341
|
+
| `front-door.code.title` | Check your email |
|
|
342
|
+
| `front-door.code.description` | Enter the code sent to {identifier}. |
|
|
343
|
+
| `front-door.code.label` | Code |
|
|
344
|
+
| `front-door.code.primary` | Verify |
|
|
345
|
+
| `front-door.code.secondary` | Send a new code |
|
|
346
|
+
| `front-door.code.notice` | That code isn’t right or has expired. Try again or send a new code. |
|
|
347
|
+
| `front-door.unavailable.notice` | Sign-in isn’t available right now. Try again in a few minutes. |
|
|
348
|
+
| `front-door.rate-limited.notice` | Too many attempts. Wait a few minutes, then try again. |
|
|
349
|
+
| `front-door.locked.notice` | This account is temporarily locked after too many attempts. Try again later. |
|
|
350
|
+
| `front-door.expired.notice` | Your session expired. Sign in again to continue. |
|
|
351
|
+
| `front-door.signed-out.notice` | You’re signed out. |
|
|
352
|
+
| `front-door.error.title` | This page didn’t load |
|
|
353
|
+
| `front-door.error.description` | Something went wrong. Error: {digest}. |
|
|
354
|
+
| `front-door.error.primary` | Try again |
|
|
355
|
+
| `front-door.not-found.title` | Page not found |
|
|
356
|
+
| `front-door.not-found.description` | This page doesn’t exist or has moved. |
|
|
357
|
+
| `front-door.not-found.primary` | Go to {surface} |
|
|
358
|
+
| `front-door.not-authorized.title` | You don’t have access |
|
|
359
|
+
| `front-door.not-authorized.description` | {identifier} doesn’t have access to {surface}. |
|
|
360
|
+
| `front-door.not-authorized.primary` | Switch account |
|
|
361
|
+
| `front-door.not-authorized.secondary` | Sign out |
|
|
362
|
+
| `front-door.access-pending.title` | Access pending |
|
|
363
|
+
| `front-door.access-pending.description` | Your access to {surface} awaits approval. Check back later. |
|
|
364
|
+
| `front-door.access-pending.primary` | Sign out |
|
|
365
|
+
| `front-door.service-unavailable.title` | {surface} isn’t available |
|
|
366
|
+
| `front-door.service-unavailable.description` | This is temporary. Try again soon. |
|
|
367
|
+
| `front-door.service-unavailable.primary` | Try again |
|
|
368
|
+
| `front-door.request-access.description` | Don’t have an account? |
|
|
369
|
+
| `front-door.request-access.label` | {requestAccessLabel} |
|
|
370
|
+
| `front-door.reset.title` | Reset your password |
|
|
371
|
+
| `front-door.reset.description` | Enter your email and we’ll send you a code. |
|
|
372
|
+
| `front-door.reset.label` | New password |
|
|
373
|
+
| `front-door.reset.primary` | Send code |
|
|
374
|
+
| `front-door.reset.secondary` | Back to sign in |
|
|
375
|
+
| `front-door.reset.notice` | Password updated. Sign in with your new password. |
|
|
376
|
+
| `front-door.activation.title` | Set up your account |
|
|
377
|
+
| `front-door.activation.description` | You’re invited to {surface}. Choose a password to finish. |
|
|
378
|
+
| `front-door.activation.primary` | Activate account |
|
|
379
|
+
| `front-door.activation.notice` | This invitation has expired or was already used. Ask for a new invitation. |
|
|
380
|
+
| `front-door.internal-note.label` | Internal |
|
|
381
|
+
| `front-door.identifier-required.notice` | Enter your email. |
|
|
382
|
+
| `front-door.password-required.notice` | Enter your password. |
|
|
383
|
+
| `front-door.code-required.notice` | Enter the code. |
|
|
384
|
+
| `front-door.network.notice` | Couldn’t reach {surface}. Check your connection and try again. |
|
|
385
|
+
| `front-door.password-weak.notice` | That password isn’t strong enough. Use a longer one you haven’t used anywhere else. |
|
|
386
|
+
| `front-door.reset-code.primary` | Reset password |
|
|
387
|
+
| `front-door.activation.label` | Password |
|
|
388
|
+
| `front-door.activation-first-name.label` | First name |
|
|
389
|
+
| `front-door.activation-last-name.label` | Last name |
|
|
390
|
+
| `front-door.name-required.notice` | Enter your first and last name. |
|
|
391
|
+
|
|
392
|
+
A `{token}` in a text is a noun from the closed set `FRONT_DOOR_NOUNS`:
|
|
393
|
+
`brand`, `surface`, `identifier`, `digest` and `requestAccessLabel` (typed as
|
|
394
|
+
`FrontDoorNoun`; a caller's values are `FrontDoorNouns`). The defaults are
|
|
395
|
+
marked `approved` and carry no approval record: that is the package's own
|
|
396
|
+
statement about shipped defaults, not a consumer's sign-off.
|
|
397
|
+
|
|
398
|
+
`resolveFrontDoorCopy(key, nouns)` resolves one entry through
|
|
399
|
+
`resolveCopyRef` and never throws. It hands the resolver only the nouns that
|
|
400
|
+
entry declares, so a known noun the entry does not use is dropped. The result
|
|
401
|
+
is `{ complete, text?, resolution?, issues }`; `text` and `resolution` exist
|
|
402
|
+
only when `issues` is empty. An issue's `reason` is one of:
|
|
403
|
+
|
|
404
|
+
- `"unknown-copy-id"` — the id is not in `FRONT_DOOR_COPY_IDS`.
|
|
405
|
+
- `"missing-noun"` — a noun the entry declares is absent or blank.
|
|
406
|
+
- `"unknown-noun"` — a noun name outside `FRONT_DOOR_NOUNS`.
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
import { resolveFrontDoorCopy } from "@clossys/writer";
|
|
410
|
+
|
|
411
|
+
const heading = resolveFrontDoorCopy("front-door.password.title", {});
|
|
412
|
+
const line = resolveFrontDoorCopy("front-door.password.description", {
|
|
413
|
+
identifier: "ana@example.test",
|
|
414
|
+
brand: "Acme",
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (line.complete) {
|
|
418
|
+
console.log(heading.text, line.text); // "Enter your password" "Signing in as ana@example.test."
|
|
419
|
+
} else {
|
|
420
|
+
for (const issue of line.issues) console.error(issue.reason, issue.noun, issue.message);
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
A site overrides one entry by registering the same id in its own registry and
|
|
425
|
+
resolving it with `resolveCopyRef`; the defaults are the fallback, not a lock.
|
|
426
|
+
Other states and a JSON file of the catalog are not in this version.
|
|
427
|
+
|
|
428
|
+
## Delegated approval — who approved this copy, and is it still that text?
|
|
429
|
+
|
|
430
|
+
`status: "approved"` says an entry may render. It does not say who decided
|
|
431
|
+
that, when, or whether the sentence has since changed underneath the
|
|
432
|
+
decision. `CopyRegistryEntry.approval` (`CopyApproval`) answers all three.
|
|
433
|
+
`writer-check approve` writes the record, so nobody has to hand-edit it into
|
|
434
|
+
a registry or into code; `writer-check approval-state` reports approval state
|
|
435
|
+
set in source instead. `textFingerprint` is a content hash rather than a
|
|
436
|
+
human-maintained counter for the same reason `fingerprint.ts` gives for
|
|
437
|
+
translation provenance.
|
|
438
|
+
|
|
439
|
+
```json
|
|
440
|
+
{
|
|
441
|
+
"id": "site.home.title",
|
|
442
|
+
"text": "Home title copy.",
|
|
443
|
+
"context": "home hero",
|
|
444
|
+
"status": "approved",
|
|
445
|
+
"approval": {
|
|
446
|
+
"approvedBy": "delegate",
|
|
447
|
+
"approvedAt": "2026-09-27T00:00:00.000Z",
|
|
448
|
+
"textFingerprint": "bb46f6c7794911342f760f6e9e46f1f6ca31d0d306c9b301a1069f4b6ff6966c",
|
|
449
|
+
"fingerprintAlgorithm": "sha256",
|
|
450
|
+
"delegate": { "id": "delegate-a", "scope": ["site.home"] },
|
|
451
|
+
"pendingOwnerReview": true,
|
|
452
|
+
"expiresAt": "2026-10-27T00:00:00.000Z"
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
(`textFingerprint` above is `computeCopyFingerprint("Home title copy.")` —
|
|
458
|
+
a 64-character `sha256` hex digest of that entry's own `text`, computed by
|
|
459
|
+
`writer-check approve`.)
|
|
460
|
+
|
|
461
|
+
- **`approvedBy`** (`CopyApprover`, `"owner" | "delegate"`) — who recorded
|
|
462
|
+
this approval.
|
|
463
|
+
- **`textFingerprint`**/**`fingerprintAlgorithm`** — pin the record to the
|
|
464
|
+
EXACT text that was approved. The moment `text` changes, a fresh
|
|
465
|
+
`computeCopyFingerprint(entry.text)` no longer matches, and the record is
|
|
466
|
+
`"approval-stale"` — an error, not a warning — until it is approved again.
|
|
467
|
+
This is the same content-derived-over-hand-maintained argument
|
|
468
|
+
`fingerprint.ts` already makes for translation staleness, applied to
|
|
469
|
+
approval staleness instead.
|
|
470
|
+
- **`delegate`** (`CopyDelegateScope`, `{ id, scope }`) — required when
|
|
471
|
+
`approvedBy` is `"delegate"`, forbidden when it is `"owner"`. `id` is an
|
|
472
|
+
opaque identifier, never a personal name. `scope` is one or more
|
|
473
|
+
dot-separated entry-id namespaces the delegate may approve: an entry is in
|
|
474
|
+
scope when its id equals a scope item or starts with `item + "."` —
|
|
475
|
+
`["site.home"]` covers `site.home` and `site.home.title`, never
|
|
476
|
+
`site.homepage.title`. `isEntryInDelegateScope(entryId, scope)` is the
|
|
477
|
+
exported predicate.
|
|
478
|
+
- **`pendingOwnerReview`** — required (and only meaningful) on a delegate
|
|
479
|
+
record. A delegate's approval is enough for the entry to resolve — see
|
|
480
|
+
`target`/`acceptDelegateInProduction` above — but it is a warning
|
|
481
|
+
(`"approval-pending-owner-review"`), not an error, until an owner durably
|
|
482
|
+
confirms it. Approving the SAME entry again with `--by owner` REPLACES the
|
|
483
|
+
delegate's record with the owner's own — that replacement, not a separate
|
|
484
|
+
"review" action, is how a pending delegate approval is durably confirmed.
|
|
485
|
+
- **`expiresAt`** — delegate records only, an ISO 8601 UTC timestamp
|
|
486
|
+
strictly after `approvedAt`. Once `now` reaches it, the record is
|
|
487
|
+
`"approval-expired"` — an error, mutually exclusive with `"approval-stale"`
|
|
488
|
+
(staleness, the more fundamental problem, takes priority when a record is
|
|
489
|
+
somehow both).
|
|
490
|
+
|
|
491
|
+
### Production authority from an approved plan
|
|
492
|
+
|
|
493
|
+
`acceptDelegateInProduction: true` is a bare flag: it records no reason. The
|
|
494
|
+
other way to let a delegate-approved entry resolve on `"production"` is
|
|
495
|
+
`approvalPlan`, the bytes of an Advisor plan record whose own approval covers
|
|
496
|
+
the delegation. The plan declares it in an optional top-level member,
|
|
497
|
+
`delegatedCopyApproval` (`{ "target": "production", "scopes": ["site.home"] }`),
|
|
498
|
+
defined by the plan contract and its
|
|
499
|
+
[digest definition](https://github.com/clossys/foundry/blob/main/docs/contracts/advisor-plan-digest.md);
|
|
500
|
+
the [contract](https://github.com/clossys/foundry/blob/main/docs/contracts/advisor-plan.json)
|
|
501
|
+
and the digest page live in the public repository, not shipped in this
|
|
502
|
+
package. `scopes` is optional; when present it is a non-empty list of copy
|
|
503
|
+
entry-id namespaces (`site.home`, not `site.home.*`). Writer reads no file:
|
|
504
|
+
the caller passes the bytes.
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
import { createCopyResolver, planDelegateCopyAuthority, type CopyRegistry } from "@clossys/writer";
|
|
508
|
+
|
|
509
|
+
declare const registry: CopyRegistry;
|
|
510
|
+
declare const planBytes: Uint8Array; // the plan file's bytes, read by the caller
|
|
511
|
+
const authority = planDelegateCopyAuthority(planBytes);
|
|
512
|
+
if (!authority.authorized) throw new Error(`plan does not authorize delegate copy: ${authority.refusal}`);
|
|
513
|
+
const resolved = createCopyResolver(registry, { target: "production", approvalPlan: planBytes })({ id: "home.title" });
|
|
514
|
+
const digest = resolved?.approval?.approvedBy === "delegate" ? resolved.approval.authorizingPlanDigest : undefined;
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
A delegate-approved entry resolves on `"production"` under `approvalPlan` when
|
|
518
|
+
Writer itself has established each of these from the bytes it was given:
|
|
519
|
+
|
|
520
|
+
- the bytes are strict JSON: valid UTF-8, no byte-order mark, and no key
|
|
521
|
+
repeated at any depth;
|
|
522
|
+
- the plan they encode passes the plan contract's schema and its code rules
|
|
523
|
+
R1 to R12 and, for `delegatedCopyApproval`, the schema's own checks (a
|
|
524
|
+
non-empty `scopes` list of well-formed namespaces, a known `target`, no
|
|
525
|
+
unknown key). Repeating a scope item is accepted and only redundant;
|
|
526
|
+
- the plan declares `delegatedCopyApproval`;
|
|
527
|
+
- the plan's latest decision by time has chosen `"approved"` and names a
|
|
528
|
+
`subjectDigest` equal to the canonical digest Writer computed from those
|
|
529
|
+
same bytes. Decisions that tie at the same instant must all approve and
|
|
530
|
+
name one digest; a decision time that does not parse authorizes nothing.
|
|
531
|
+
The digest covers `delegatedCopyApproval` and its `scopes`, so adding,
|
|
532
|
+
removing or editing either after an approval leaves the plan unapproved
|
|
533
|
+
for this purpose until a new approval names the new digest;
|
|
534
|
+
- the entry's id is inside the declared `scopes`, when scopes are declared.
|
|
535
|
+
An entry is inside a scope when its id equals a scope item or starts with
|
|
536
|
+
`item + "."`, as for a delegate's own scope: `site.home` covers
|
|
537
|
+
`site.home` and `site.home.title`, not `site.homepage.title`. Without
|
|
538
|
+
`scopes` the declaration covers every entry. The entry's own
|
|
539
|
+
`approval.delegate.scope` is enforced as before, so both lists must hold.
|
|
540
|
+
|
|
541
|
+
`planDelegateCopyAuthority(bytes)` is the pure function that does this. It
|
|
542
|
+
returns a `PlanDelegateCopyAuthority`: `{ authorized: true, planDigest,
|
|
543
|
+
scopes? }` (`scopes` present only when the plan declares them) or
|
|
544
|
+
`{ authorized: false, refusal, violations? }`, where `refusal` is a
|
|
545
|
+
`PlanDelegateCopyRefusal` and `violations` (only with `"plan-invalid"`) lists
|
|
546
|
+
each failed rule and position, never a value. Nothing in the result quotes the
|
|
547
|
+
plan. The refusal codes:
|
|
548
|
+
|
|
549
|
+
- `"plan-not-bytes"` — the argument is not a `Uint8Array` (a string, a parsed
|
|
550
|
+
object, a `DataView` and a `Uint16Array` are all refused).
|
|
551
|
+
- `"plan-unreadable"` — the bytes are not strict JSON (invalid UTF-8, a
|
|
552
|
+
byte-order mark, a syntax error, or a repeated key).
|
|
553
|
+
- `"plan-invalid"` — the plan fails the contract's schema or code rules, which
|
|
554
|
+
includes a `delegatedCopyApproval` that is malformed, has an empty `scopes`
|
|
555
|
+
list or an unknown `target`, and a decision time that does not parse.
|
|
556
|
+
- `"delegated-copy-approval-absent"` — the plan does not declare
|
|
557
|
+
`delegatedCopyApproval`.
|
|
558
|
+
- `"no-decisions"` — the plan has no decisions.
|
|
559
|
+
- `"decision-time-unparseable"` — a decision's time does not parse (a valid
|
|
560
|
+
plan cannot reach this; it is kept as a second check).
|
|
561
|
+
- `"latest-decision-not-approved"` — a decision at the latest time has not
|
|
562
|
+
chosen `"approved"`, so a later deferral or rejection withdraws the grant.
|
|
563
|
+
- `"approval-without-subject-digest"` — an approving decision at the latest
|
|
564
|
+
time names no well-formed `subjectDigest`, so it binds no bytes.
|
|
565
|
+
- `"latest-decisions-disagree"` — decisions tied at the latest time name
|
|
566
|
+
different digests.
|
|
567
|
+
- `"subject-digest-mismatch"` — the latest approval names a digest other than
|
|
568
|
+
this plan's own: the plan changed after it, or the approval belongs to
|
|
569
|
+
another plan.
|
|
570
|
+
|
|
571
|
+
`resolveCopyRef` adds one more code of its own, in `CopyResolvePlanRefusal`
|
|
572
|
+
(`PlanDelegateCopyRefusal` plus this value): `"entry-outside-plan-scopes"` —
|
|
573
|
+
the plan authorizes delegate copy but its `scopes` do not include the entry.
|
|
574
|
+
|
|
575
|
+
**What the resolution reports.** `CopyResolution.approval` is a
|
|
576
|
+
`CopyResolutionApproval`: `{ approvedBy: "owner", pendingOwnerReview: false }`
|
|
577
|
+
or `{ approvedBy: "delegate", pendingOwnerReview, authorizingPlanDigest? }`.
|
|
578
|
+
`authorizingPlanDigest` is the plan's canonical digest and is present only when
|
|
579
|
+
an `approvalPlan` authorized the entry on `"production"`. It is absent when
|
|
580
|
+
`acceptDelegateInProduction` authorized it and on `"preview"`, so a
|
|
581
|
+
plan-authorized resolution can be told from a flag-authorized one by whether
|
|
582
|
+
the key is a string.
|
|
583
|
+
|
|
584
|
+
**Precedence.** An entry's own `"approval-stale"` and `"approval-expired"`
|
|
585
|
+
outrank every plan outcome. `acceptDelegateInProduction: true` together with
|
|
586
|
+
`approvalPlan` is `"invalid-options"`, so two authorities never compete. On
|
|
587
|
+
`"preview"` the plan is accepted and ignored, and no digest is reported; the
|
|
588
|
+
plan is likewise unread for an owner-approved entry or one with no approval
|
|
589
|
+
record. It is evaluated on each call and not cached.
|
|
590
|
+
|
|
591
|
+
**What this does not prove.**
|
|
592
|
+
|
|
593
|
+
- Who wrote the decision. Nothing in the file shows that the latest decision is
|
|
594
|
+
genuine; that depends on where the plan file is committed and who could
|
|
595
|
+
commit it. Anyone able to write the file can append an approval naming the
|
|
596
|
+
digest.
|
|
597
|
+
- Where the bytes came from. The plan is caller-supplied bytes, and Writer
|
|
598
|
+
verifies no provenance. A caller that controls the call could equally pass
|
|
599
|
+
`acceptDelegateInProduction: true`; the plan route adds a recorded,
|
|
600
|
+
digest-bound reason, not a boundary against the caller.
|
|
601
|
+
- Any link between the registry and the plan. The plan names no registry,
|
|
602
|
+
revision or entry fingerprint, so it covers delegate approvals in scope that
|
|
603
|
+
were recorded before or after the plan's approval. Entry-level staleness and
|
|
604
|
+
expiry still apply.
|
|
605
|
+
- Expiry. The plan's approval has none: it holds until the file changes or a
|
|
606
|
+
later decision withdraws it.
|
|
607
|
+
- More than one subject. One latest decision binds one subject, so a decision
|
|
608
|
+
naming an apply-bundle digest grants nothing here.
|
|
609
|
+
- A narrow grant. A `delegatedCopyApproval` without `scopes` covers every
|
|
610
|
+
entry; the grant is visible only as the missing member.
|
|
611
|
+
|
|
612
|
+
### `writer-check approve` — write or revoke a record
|
|
613
|
+
|
|
614
|
+
```bash
|
|
615
|
+
writer-check approve <registry-file> <entry-id>... --by owner
|
|
616
|
+
writer-check approve <registry-file> <entry-id>... --by delegate --delegate <id> --scope <ns>[,<ns>...] [--expires <ISO>]
|
|
617
|
+
writer-check approve <registry-file> <entry-id>... --revoke
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
Every field the command writes is either computed (`approvedAt` from the
|
|
621
|
+
clock, `textFingerprint` from the entry's current `text`) or copied from an
|
|
622
|
+
argument the command validated. Approving sets `status: "approved"`; a
|
|
623
|
+
delegate record is written with `pendingOwnerReview: true`. Multiple entry
|
|
624
|
+
ids are **all-or-nothing**: if any named id is unknown, retired, or (for a
|
|
625
|
+
delegate approval) outside `--scope`, every problem is printed and nothing
|
|
626
|
+
is written, including for the ids that would otherwise have succeeded. The
|
|
627
|
+
command leaves every other field, the key order and `revision` as they
|
|
628
|
+
were, and writes 2-space JSON to a temp file in the same directory before
|
|
629
|
+
renaming it into place. `--revoke` returns every named entry to `"draft"`
|
|
630
|
+
and removes its `approval` record.
|
|
631
|
+
|
|
632
|
+
Exit codes: `0` written (every named entry approved or revoked, the file on
|
|
633
|
+
disk now reflects it), `1` refused (an unknown, retired, or out-of-scope
|
|
634
|
+
entry id was named; nothing written), `2` could not run (bad arguments, or
|
|
635
|
+
the registry file is missing/unreadable/not valid JSON/fails schema
|
|
636
|
+
validation; nothing written).
|
|
637
|
+
|
|
638
|
+
### `writer-check approval-state` — is the registry current, and does source respect it?
|
|
639
|
+
|
|
640
|
+
```bash
|
|
641
|
+
writer-check approval-state <registry-file> <scan-dir> [--extensions <ext>[,<ext>...]] [--format json] [--now <ISO>]
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Reports approval state from two independent angles in one run, because
|
|
645
|
+
either can fail without the other noticing:
|
|
646
|
+
|
|
647
|
+
- **Registry findings** (`assessCopyApprovals`, above): `"approval-stale"`
|
|
648
|
+
and `"approval-expired"` are errors; a missing record
|
|
649
|
+
(`"approval-record-missing"`) or a delegate record still
|
|
650
|
+
`pendingOwnerReview` (`"approval-pending-owner-review"`) is a warning —
|
|
651
|
+
legitimately approved for now, just not yet durably so.
|
|
652
|
+
- **Source findings** (`scanApprovalBypass`/`checkApprovalBypass`,
|
|
653
|
+
`approval-bypass.ts`): does consumer code in `scan-dir` route copy through
|
|
654
|
+
the resolver, or around it? `"approval-set-in-code"` — the code itself
|
|
655
|
+
declares approval (`status: "approved"`, an `approvedBy`/
|
|
656
|
+
`pendingOwnerReview` property or member assignment) — and
|
|
657
|
+
`"copy-read-without-resolver"` — the code imports the registry file and
|
|
658
|
+
reads its content some way other than passing it whole to
|
|
659
|
+
`createCopyResolver`/`resolveCopyRef`/`validateCopyRegistryShape`, or
|
|
660
|
+
calling `parseCopyRegistry` only when its return value is passed to
|
|
661
|
+
`createCopyResolver` or `resolveCopyRef` — are both errors. An unresolvable registry
|
|
662
|
+
load (a dynamic `import()`/`require()`, an unbound `require`, a
|
|
663
|
+
re-export) or a string that merely names the registry file is reported as
|
|
664
|
+
`unchecked`, not a finding: an incomplete picture, never assumed clean.
|
|
665
|
+
|
|
666
|
+
**The coupled-file limit, stated plainly**: only files that import
|
|
667
|
+
`@clossys/writer` (any subpath) or import the registry file itself are
|
|
668
|
+
examined. A file that copies registry data without importing either —
|
|
669
|
+
pasted JSON, data fetched at runtime, a registry handed in from an
|
|
670
|
+
uncoupled module — is not examined by this gate. Other known gaps: specifier
|
|
671
|
+
matching is exact (an extension-less or aliased specifier is not
|
|
672
|
+
recognised) and the registry-file match is by basename, which is broad
|
|
673
|
+
enough that a distinctive registry filename (not `index.json`) is worth
|
|
674
|
+
choosing; there is no scope tracking, so a local variable that happens to
|
|
675
|
+
share a flagged binding's name is flagged too; a type assertion or `satisfies`
|
|
676
|
+
between the registry binding and the closing `)` of an allowed resolver call
|
|
677
|
+
is refused (for example `createCopyResolver(registry as CopyRegistry)` is not
|
|
678
|
+
a pass — the resolver accepts `unknown`, so no cast is needed); and only a directly written `"approved"` string
|
|
679
|
+
literal is caught — a value built at runtime is not.
|
|
680
|
+
|
|
681
|
+
**Verdict precedence**: the same "a violation outranks an incomplete
|
|
682
|
+
picture" ternary `checkAddressability` and `checkPassageComposition` both
|
|
683
|
+
use. `"violated"` whenever at least one error-severity finding exists
|
|
684
|
+
(registry or source), regardless of what is unchecked; `"indeterminate"`
|
|
685
|
+
only with zero error findings and something left unchecked, unparseable, or
|
|
686
|
+
zero files scanned; `"satisfied"` otherwise. Warnings are reported but do
|
|
687
|
+
not change the verdict.
|
|
688
|
+
|
|
689
|
+
Exit codes: `1` any error-severity finding (a registry error or any
|
|
690
|
+
approval-bypass finding); else `2` any unchecked registry load/reference,
|
|
691
|
+
any file that failed to parse, or zero files scanned; else `0`. Warnings
|
|
692
|
+
never change the exit code.
|
|
693
|
+
|
|
694
|
+
`--format json` prints exactly one object to stdout:
|
|
695
|
+
|
|
696
|
+
```
|
|
697
|
+
{
|
|
698
|
+
verdict: "violated" | "indeterminate" | "satisfied",
|
|
699
|
+
findings: Array<
|
|
700
|
+
| { source: "registry", rule, severity, entryId, message }
|
|
701
|
+
| { source: "source", rule, severity, file, line, message }
|
|
702
|
+
>,
|
|
703
|
+
unchecked: Array<{ file, line, kind, detail }>,
|
|
704
|
+
counts: { errors, warnings, unchecked, filesScanned, coupledFiles }
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
153
708
|
## Where this package sits on i18n
|
|
154
709
|
|
|
155
710
|
"i18n" is two different things, and this package deliberately does only one
|
|
@@ -680,12 +1235,28 @@ The root entry point exports the copy registry and traceability surface:
|
|
|
680
1235
|
`createCopyResolver`, `resolveCopyRef`, `checkCopyRecord`, `CopyRecord`,
|
|
681
1236
|
`CopyRegistry`, `CopyEntry`, `CopyRegistryEntry`, `CopyRef`, `CopyResolution`,
|
|
682
1237
|
`CopyResolver`, `CopySource`, `CopyLocale`, `CopyValue`, `CopyEntryStatus`,
|
|
683
|
-
`CopyResolveIssue`, `CopyResolveIssueReason`, `
|
|
1238
|
+
`CopyResolveIssue`, `CopyResolveIssueReason`, `CopyResolveOptions`,
|
|
1239
|
+
`CopyResolveResult`, `CopyResolveTarget`,
|
|
684
1240
|
`CopyEntryId`, `CopyFinding`, `CopyRegistryReadIssue`,
|
|
685
1241
|
`CopyRegistryReadIssueReason`, `CopyRegistryReadResult`,
|
|
686
1242
|
`CopyEntryCheckResult`, `CopyEntrySkip`, `CopyRecordCheckOptions`,
|
|
687
1243
|
`CopyRecordCheckReport`, `CopyRecordFinding`, and
|
|
688
1244
|
`CopyRecordWaivedFinding`.
|
|
1245
|
+
- Site identity (see above): `resolveSiteIdentity`, `SITE_NAME_COPY_ID`,
|
|
1246
|
+
`SITE_TAGLINE_COPY_ID`, `SITE_IDENTITY_COPY_IDS`, `SiteIdentityField`,
|
|
1247
|
+
`SiteIdentityIssue`, `SiteIdentityIssueReason`, `SiteIdentityOptions`, and
|
|
1248
|
+
`SiteIdentityResolution`.
|
|
1249
|
+
- Messaging kit (see above): `resolveMessagingKit`, `MESSAGING_KIT_COPY_IDS`,
|
|
1250
|
+
`MESSAGING_PITCH_ONE_LINER_COPY_ID`, `MESSAGING_PITCH_ELEVATOR_COPY_ID`,
|
|
1251
|
+
`MESSAGING_PITCH_PARAGRAPH_COPY_ID`, `MESSAGING_BOILERPLATE_SHORT_COPY_ID`,
|
|
1252
|
+
`MESSAGING_BOILERPLATE_MEDIUM_COPY_ID`, `MESSAGING_BOILERPLATE_LONG_COPY_ID`,
|
|
1253
|
+
`MessagingKit`, `MessagingKitField`, `MessagingKitIssue`,
|
|
1254
|
+
`MessagingKitIssueReason`, `MessagingKitOptions`, `MessagingKitResolution`,
|
|
1255
|
+
and `MessagingKitResolutions`.
|
|
1256
|
+
- Front-door copy (see above): `resolveFrontDoorCopy`, `isFrontDoorCopyId`,
|
|
1257
|
+
`FRONT_DOOR_COPY_EN`, `FRONT_DOOR_COPY_IDS`, `FRONT_DOOR_NOUNS`,
|
|
1258
|
+
`FrontDoorKey`, `FrontDoorNoun`, `FrontDoorNouns`, `FrontDoorCopyIssue`,
|
|
1259
|
+
`FrontDoorCopyIssueReason`, and `FrontDoorCopyResolution`.
|
|
689
1260
|
- Translation provenance and fingerprinting (see "Where this package sits
|
|
690
1261
|
on i18n" above): `CopyTranslationProvenance`, `computeCopyFingerprint`,
|
|
691
1262
|
and `COPY_FINGERPRINT_ALGORITHM`.
|
|
@@ -726,6 +1297,20 @@ The root entry point exports the copy registry and traceability surface:
|
|
|
726
1297
|
`PassageRegistryReadResult`, `PassageTermReference`,
|
|
727
1298
|
`PassageUnclassifiedItem`, `PassageVerdict`, `PassageViolation`, and
|
|
728
1299
|
`PassageViolationRule`.
|
|
1300
|
+
- Delegated approval (see above): `CopyApproval`, `CopyApprover`,
|
|
1301
|
+
`CopyDelegateScope`, `assessCopyApprovals`, `isEntryInDelegateScope`,
|
|
1302
|
+
`CopyApprovalFinding`, `CopyApprovalFindingRule`, `checkApprovalBypass`,
|
|
1303
|
+
`extractApprovalBypass`, `scanApprovalBypass`, `ApprovalBypassExtractResult`,
|
|
1304
|
+
`ApprovalBypassFinding`, `ApprovalBypassGateResult`, `ApprovalBypassRule`,
|
|
1305
|
+
`ApprovalBypassScanOptions`, `ApprovalBypassScanResult`,
|
|
1306
|
+
`ApprovalBypassUncheckedItem`, and `ApprovalBypassVerdict`. The commands
|
|
1307
|
+
that write and report on this state, `writer-check approve` and
|
|
1308
|
+
`writer-check approval-state`, are CLI-only and not exported from
|
|
1309
|
+
`index.ts`.
|
|
1310
|
+
- Production authority from an approved plan (see above):
|
|
1311
|
+
`planDelegateCopyAuthority`, `PlanDelegateCopyAuthority`,
|
|
1312
|
+
`PlanDelegateCopyRefusal`, `CopyResolutionApproval`, and
|
|
1313
|
+
`CopyResolvePlanRefusal`.
|
|
729
1314
|
|
|
730
1315
|
The voice names described under Public entry points are re-exported from the
|
|
731
1316
|
root and from `@clossys/writer/voice`, including the rule-vocabulary
|