@clossys/writer 0.3.9 → 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 +647 -6
- package/dist/addressability.d.ts +11 -1
- package/dist/addressability.d.ts.map +1 -1
- package/dist/addressability.js +169 -3
- package/dist/addressability.js.map +1 -1
- 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 +264 -22
- 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/fold-wallpaper-gate.d.ts +7 -0
- package/dist/fold-wallpaper-gate.d.ts.map +1 -0
- package/dist/fold-wallpaper-gate.js +29 -0
- package/dist/fold-wallpaper-gate.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 +18 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/dist/live-copy-gate.d.ts +16 -0
- package/dist/live-copy-gate.d.ts.map +1 -0
- package/dist/live-copy-gate.js +135 -0
- package/dist/live-copy-gate.js.map +1 -0
- 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/render-registry-parity.d.ts +20 -0
- package/dist/render-registry-parity.d.ts.map +1 -0
- package/dist/render-registry-parity.js +45 -0
- package/dist/render-registry-parity.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 +231 -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/treatment-word-budget.d.ts +31 -0
- package/dist/treatment-word-budget.d.ts.map +1 -0
- package/dist/treatment-word-budget.js +80 -0
- package/dist/treatment-word-budget.js.map +1 -0
- package/dist/types.d.ts +76 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -7
- package/dist/types.js.map +1 -1
- package/fit-signals.json +11 -0
- package/package.json +201 -11
- package/skill/SKILL.md +49 -0
- package/src/addressability.ts +186 -4
- package/src/approval-bypass.ts +2263 -0
- package/src/approval.ts +491 -0
- package/src/approve-cli.ts +474 -0
- package/src/cli.ts +274 -22
- package/src/field-options.ts +134 -0
- package/src/fold-wallpaper-gate.ts +29 -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 +107 -1
- package/src/live-copy-gate.ts +150 -0
- 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/render-registry-parity.ts +65 -0
- package/src/resolve.ts +185 -19
- package/src/scan.ts +97 -6
- package/src/schema.ts +247 -5
- package/src/site-identity.ts +125 -0
- package/src/treatment-word-budget.ts +100 -0
- package/src/types.ts +83 -7
- package/templates/front-door.en.json +426 -0
- package/CHANGELOG.md +0 -330
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The front-door copy kind: the words on a sign-in page and the pages around
|
|
3
|
+
* it, as one shipped catalog of reserved ids with English defaults and a
|
|
4
|
+
* noun resolver.
|
|
5
|
+
*
|
|
6
|
+
* Sign-in and boundary pages each used to carry their own copy. This module
|
|
7
|
+
* is a reserved copy kind like the messaging kit, not a second text store or
|
|
8
|
+
* a second approval path: `resolveFrontDoorCopy` asks `resolveCopyRef`, so
|
|
9
|
+
* everything that resolver refuses is refused here for the same reason.
|
|
10
|
+
*
|
|
11
|
+
* Invariants:
|
|
12
|
+
*
|
|
13
|
+
* - F1: an id is `front-door.<state>.<slot>`, where the slot is one of
|
|
14
|
+
* `title`, `description`, `label`, `primary`, `secondary`, `notice` or
|
|
15
|
+
* `alt`. This version ships 63 ids; `FRONT_DOOR_COPY_IDS` is
|
|
16
|
+
* the closed list and `FrontDoorKey` its element union.
|
|
17
|
+
* - F2: a `{token}` in a default text is a noun from `FRONT_DOOR_NOUNS`
|
|
18
|
+
* and is declared in the entry's `placeholders`. The nouns are a closed
|
|
19
|
+
* set: a caller cannot invent one.
|
|
20
|
+
* - F3: `resolveFrontDoorCopy` hands `resolveCopyRef` only the nouns the
|
|
21
|
+
* entry declares. A known noun the entry does not use is dropped, not
|
|
22
|
+
* an error; a name outside the closed set is `"unknown-noun"`.
|
|
23
|
+
* - F4: a declared noun that is absent or blank (after trimming) is
|
|
24
|
+
* `"missing-noun"`. A blank noun never renders as an empty gap.
|
|
25
|
+
* - F5: every issue is reported, in order: unknown nouns, then missing
|
|
26
|
+
* nouns. `text` and `resolution` exist only when `issues` is empty.
|
|
27
|
+
* Nothing here throws for any input.
|
|
28
|
+
* - F6: a site overrides one entry by registering the same id in its own
|
|
29
|
+
* registry and resolving it with `resolveCopyRef`; these defaults are
|
|
30
|
+
* the fallback, not a lock.
|
|
31
|
+
*
|
|
32
|
+
* The defaults are US English, shipped on purpose, and carry no approval
|
|
33
|
+
* record: their `approved` status is the package's own statement, not a
|
|
34
|
+
* consumer's sign-off.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { resolveCopyRef } from "./resolve.js";
|
|
38
|
+
import type { CopyRegistry, CopyRegistryEntry, CopyResolution } from "./types.js";
|
|
39
|
+
|
|
40
|
+
/** All reserved front-door copy ids, in catalog order. */
|
|
41
|
+
export const FRONT_DOOR_COPY_IDS = [
|
|
42
|
+
"front-door.sign-in.title",
|
|
43
|
+
"front-door.sign-in.description",
|
|
44
|
+
"front-door.sign-in.label",
|
|
45
|
+
"front-door.sign-in.primary",
|
|
46
|
+
"front-door.password.title",
|
|
47
|
+
"front-door.password.description",
|
|
48
|
+
"front-door.password.label",
|
|
49
|
+
"front-door.password.primary",
|
|
50
|
+
"front-door.password.secondary",
|
|
51
|
+
"front-door.identifier-not-found.notice",
|
|
52
|
+
"front-door.forgot-password.label",
|
|
53
|
+
"front-door.password.notice",
|
|
54
|
+
"front-door.sign-in.alt",
|
|
55
|
+
"front-door.code.title",
|
|
56
|
+
"front-door.code.description",
|
|
57
|
+
"front-door.code.label",
|
|
58
|
+
"front-door.code.primary",
|
|
59
|
+
"front-door.code.secondary",
|
|
60
|
+
"front-door.code.notice",
|
|
61
|
+
"front-door.unavailable.notice",
|
|
62
|
+
"front-door.rate-limited.notice",
|
|
63
|
+
"front-door.locked.notice",
|
|
64
|
+
"front-door.expired.notice",
|
|
65
|
+
"front-door.signed-out.notice",
|
|
66
|
+
"front-door.error.title",
|
|
67
|
+
"front-door.error.description",
|
|
68
|
+
"front-door.error.primary",
|
|
69
|
+
"front-door.not-found.title",
|
|
70
|
+
"front-door.not-found.description",
|
|
71
|
+
"front-door.not-found.primary",
|
|
72
|
+
"front-door.not-authorized.title",
|
|
73
|
+
"front-door.not-authorized.description",
|
|
74
|
+
"front-door.not-authorized.primary",
|
|
75
|
+
"front-door.not-authorized.secondary",
|
|
76
|
+
"front-door.access-pending.title",
|
|
77
|
+
"front-door.access-pending.description",
|
|
78
|
+
"front-door.access-pending.primary",
|
|
79
|
+
"front-door.service-unavailable.title",
|
|
80
|
+
"front-door.service-unavailable.description",
|
|
81
|
+
"front-door.service-unavailable.primary",
|
|
82
|
+
"front-door.request-access.description",
|
|
83
|
+
"front-door.request-access.label",
|
|
84
|
+
"front-door.reset.title",
|
|
85
|
+
"front-door.reset.description",
|
|
86
|
+
"front-door.reset.label",
|
|
87
|
+
"front-door.reset.primary",
|
|
88
|
+
"front-door.reset.secondary",
|
|
89
|
+
"front-door.reset.notice",
|
|
90
|
+
"front-door.activation.title",
|
|
91
|
+
"front-door.activation.description",
|
|
92
|
+
"front-door.activation.primary",
|
|
93
|
+
"front-door.activation.notice",
|
|
94
|
+
"front-door.internal-note.label",
|
|
95
|
+
"front-door.identifier-required.notice",
|
|
96
|
+
"front-door.password-required.notice",
|
|
97
|
+
"front-door.code-required.notice",
|
|
98
|
+
"front-door.network.notice",
|
|
99
|
+
"front-door.password-weak.notice",
|
|
100
|
+
"front-door.reset-code.primary",
|
|
101
|
+
"front-door.activation.label",
|
|
102
|
+
"front-door.activation-first-name.label",
|
|
103
|
+
"front-door.activation-last-name.label",
|
|
104
|
+
"front-door.name-required.notice",
|
|
105
|
+
] as const;
|
|
106
|
+
|
|
107
|
+
/** A reserved front-door copy id. */
|
|
108
|
+
export type FrontDoorKey = (typeof FRONT_DOOR_COPY_IDS)[number];
|
|
109
|
+
|
|
110
|
+
/** The closed set of nouns a front-door text may name as a `{token}`. */
|
|
111
|
+
export const FRONT_DOOR_NOUNS = ["brand", "surface", "identifier", "digest", "requestAccessLabel"] as const;
|
|
112
|
+
|
|
113
|
+
/** One noun a front-door text may name. */
|
|
114
|
+
export type FrontDoorNoun = (typeof FRONT_DOOR_NOUNS)[number];
|
|
115
|
+
|
|
116
|
+
/** The nouns a caller supplies; each is read only when the entry declares it. */
|
|
117
|
+
export type FrontDoorNouns = Partial<Record<FrontDoorNoun, string>>;
|
|
118
|
+
|
|
119
|
+
/** Why a front-door entry could not be resolved. */
|
|
120
|
+
export type FrontDoorCopyIssueReason = "unknown-copy-id" | "missing-noun" | "unknown-noun";
|
|
121
|
+
|
|
122
|
+
/** One reason a front-door entry could not be resolved. */
|
|
123
|
+
export interface FrontDoorCopyIssue {
|
|
124
|
+
reason: FrontDoorCopyIssueReason;
|
|
125
|
+
message: string;
|
|
126
|
+
/** The id asked for, on `"unknown-copy-id"`. */
|
|
127
|
+
id?: string;
|
|
128
|
+
/** The noun at fault, on `"missing-noun"` and `"unknown-noun"`. */
|
|
129
|
+
noun?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The outcome of `resolveFrontDoorCopy`. `text` and `resolution` are
|
|
134
|
+
* present only when `complete` is `true`; `issues` is empty exactly then.
|
|
135
|
+
*/
|
|
136
|
+
export interface FrontDoorCopyResolution {
|
|
137
|
+
complete: boolean;
|
|
138
|
+
text?: string;
|
|
139
|
+
resolution?: CopyResolution;
|
|
140
|
+
issues: FrontDoorCopyIssue[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const DEFAULTS: Readonly<Record<FrontDoorKey, { text: string; context: string }>> = {
|
|
144
|
+
"front-door.sign-in.title": { text: "Sign in", context: "sign-in page: heading" },
|
|
145
|
+
"front-door.sign-in.description": { text: "Continue to {surface}.", context: "sign-in page: line under the heading" },
|
|
146
|
+
"front-door.sign-in.label": { text: "Email", context: "sign-in page: email field label" },
|
|
147
|
+
"front-door.sign-in.primary": { text: "Continue", context: "sign-in page: main button" },
|
|
148
|
+
"front-door.password.title": { text: "Enter your password", context: "password page: heading" },
|
|
149
|
+
"front-door.password.description": { text: "Signing in as {identifier}.", context: "password page: line under the heading" },
|
|
150
|
+
"front-door.password.label": { text: "Password", context: "password page: password field label" },
|
|
151
|
+
"front-door.password.primary": { text: "Sign in", context: "password page: main button" },
|
|
152
|
+
"front-door.password.secondary": { text: "Use a different email", context: "password page: link back to the email step" },
|
|
153
|
+
"front-door.identifier-not-found.notice": {
|
|
154
|
+
text: "We couldn’t find an account for that email. Check it and try again.",
|
|
155
|
+
context: "sign-in page: notice when no account matches the email",
|
|
156
|
+
},
|
|
157
|
+
"front-door.forgot-password.label": { text: "Forgot password?", context: "sign-in page: link to start a password reset" },
|
|
158
|
+
"front-door.password.notice": { text: "That password isn’t right. Try again or reset it.", context: "password page: notice when the password is wrong" },
|
|
159
|
+
"front-door.sign-in.alt": { text: "Sign in to {brand}", context: "sign-in page: alt text for the brand mark" },
|
|
160
|
+
"front-door.code.title": { text: "Check your email", context: "code page: heading" },
|
|
161
|
+
"front-door.code.description": { text: "Enter the code sent to {identifier}.", context: "code page: line under the heading" },
|
|
162
|
+
"front-door.code.label": { text: "Code", context: "code page: code field label" },
|
|
163
|
+
"front-door.code.primary": { text: "Verify", context: "code page: main button" },
|
|
164
|
+
"front-door.code.secondary": { text: "Send a new code", context: "code page: link to request another code" },
|
|
165
|
+
"front-door.code.notice": { text: "That code isn’t right or has expired. Try again or send a new code.", context: "code page: notice when the code is wrong or expired" },
|
|
166
|
+
"front-door.unavailable.notice": { text: "Sign-in isn’t available right now. Try again in a few minutes.", context: "sign-in page: notice when sign-in is temporarily unavailable" },
|
|
167
|
+
"front-door.rate-limited.notice": { text: "Too many attempts. Wait a few minutes, then try again.", context: "sign-in page: notice when attempts are rate limited" },
|
|
168
|
+
"front-door.locked.notice": { text: "This account is temporarily locked after too many attempts. Try again later.", context: "sign-in page: notice when the account is temporarily locked" },
|
|
169
|
+
"front-door.expired.notice": { text: "Your session expired. Sign in again to continue.", context: "sign-in page: notice when the session has expired" },
|
|
170
|
+
"front-door.signed-out.notice": { text: "You’re signed out.", context: "sign-in page: notice after signing out" },
|
|
171
|
+
"front-door.error.title": { text: "This page didn’t load", context: "error page: heading" },
|
|
172
|
+
"front-door.error.description": { text: "Something went wrong. Error: {digest}.", context: "error page: line under the heading, with the error digest" },
|
|
173
|
+
"front-door.error.primary": { text: "Try again", context: "error page: main button" },
|
|
174
|
+
"front-door.not-found.title": { text: "Page not found", context: "not-found page: heading" },
|
|
175
|
+
"front-door.not-found.description": { text: "This page doesn’t exist or has moved.", context: "not-found page: line under the heading" },
|
|
176
|
+
"front-door.not-found.primary": { text: "Go to {surface}", context: "not-found page: main button back to the surface" },
|
|
177
|
+
"front-door.not-authorized.title": { text: "You don’t have access", context: "not-authorized page: heading" },
|
|
178
|
+
"front-door.not-authorized.description": { text: "{identifier} doesn’t have access to {surface}.", context: "not-authorized page: line under the heading" },
|
|
179
|
+
"front-door.not-authorized.primary": { text: "Switch account", context: "not-authorized page: main button" },
|
|
180
|
+
"front-door.not-authorized.secondary": { text: "Sign out", context: "not-authorized page: link to sign out" },
|
|
181
|
+
"front-door.access-pending.title": { text: "Access pending", context: "access-pending page: heading" },
|
|
182
|
+
"front-door.access-pending.description": { text: "Your access to {surface} awaits approval. Check back later.", context: "access-pending page: line under the heading" },
|
|
183
|
+
"front-door.access-pending.primary": { text: "Sign out", context: "access-pending page: main button" },
|
|
184
|
+
"front-door.service-unavailable.title": { text: "{surface} isn’t available", context: "service-unavailable page: heading" },
|
|
185
|
+
"front-door.service-unavailable.description": { text: "This is temporary. Try again soon.", context: "service-unavailable page: line under the heading" },
|
|
186
|
+
"front-door.service-unavailable.primary": { text: "Try again", context: "service-unavailable page: main button" },
|
|
187
|
+
"front-door.request-access.description": { text: "Don’t have an account?", context: "sign-in page: line before the request-access link" },
|
|
188
|
+
"front-door.request-access.label": { text: "{requestAccessLabel}", context: "sign-in page: request-access link label" },
|
|
189
|
+
"front-door.reset.title": { text: "Reset your password", context: "reset page: heading" },
|
|
190
|
+
"front-door.reset.description": { text: "Enter your email and we’ll send you a code.", context: "reset page: line under the heading" },
|
|
191
|
+
"front-door.reset.label": { text: "New password", context: "reset page: new password field label" },
|
|
192
|
+
"front-door.reset.primary": { text: "Send code", context: "reset page: main button" },
|
|
193
|
+
"front-door.reset.secondary": { text: "Back to sign in", context: "reset page: link back to sign-in" },
|
|
194
|
+
"front-door.reset.notice": { text: "Password updated. Sign in with your new password.", context: "sign-in page: notice after a password reset" },
|
|
195
|
+
"front-door.activation.title": { text: "Set up your account", context: "activation page: heading" },
|
|
196
|
+
"front-door.activation.description": { text: "You’re invited to {surface}. Choose a password to finish.", context: "activation page: line under the heading" },
|
|
197
|
+
"front-door.activation.primary": { text: "Activate account", context: "activation page: main button" },
|
|
198
|
+
"front-door.activation.notice": { text: "This invitation has expired or was already used. Ask for a new invitation.", context: "activation page: notice when the invitation is no longer valid" },
|
|
199
|
+
"front-door.internal-note.label": { text: "Internal", context: "label marking a note visible only inside the organization" },
|
|
200
|
+
"front-door.identifier-required.notice": { text: "Enter your email.", context: "sign-in page: notice when the email is empty" },
|
|
201
|
+
"front-door.password-required.notice": { text: "Enter your password.", context: "password page: notice when the password is empty" },
|
|
202
|
+
"front-door.code-required.notice": { text: "Enter the code.", context: "code page: notice when the code is empty" },
|
|
203
|
+
"front-door.network.notice": { text: "Couldn’t reach {surface}. Check your connection and try again.", context: "any page: notice when the request could not reach the surface" },
|
|
204
|
+
"front-door.password-weak.notice": {
|
|
205
|
+
text: "That password isn’t strong enough. Use a longer one you haven’t used anywhere else.",
|
|
206
|
+
context: "reset and activation pages: notice when the new password is refused as too weak",
|
|
207
|
+
},
|
|
208
|
+
"front-door.reset-code.primary": { text: "Reset password", context: "reset page, code step: main button" },
|
|
209
|
+
"front-door.activation.label": { text: "Password", context: "activation page: password field label" },
|
|
210
|
+
"front-door.activation-first-name.label": { text: "First name", context: "activation page: first name field label" },
|
|
211
|
+
"front-door.activation-last-name.label": { text: "Last name", context: "activation page: last name field label" },
|
|
212
|
+
"front-door.name-required.notice": { text: "Enter your first and last name.", context: "activation page: notice when a name field is empty" },
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
function placeholdersOf(text: string): string[] {
|
|
216
|
+
return [...new Set([...text.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]!))];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function entryFor(id: FrontDoorKey): CopyRegistryEntry {
|
|
220
|
+
const { text, context } = DEFAULTS[id];
|
|
221
|
+
const placeholders = placeholdersOf(text);
|
|
222
|
+
return {
|
|
223
|
+
id,
|
|
224
|
+
text,
|
|
225
|
+
context,
|
|
226
|
+
status: "approved",
|
|
227
|
+
...(placeholders.length > 0 ? { placeholders } : {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The shipped English defaults for every id in `FRONT_DOOR_COPY_IDS`. */
|
|
232
|
+
export const FRONT_DOOR_COPY_EN: CopyRegistry = {
|
|
233
|
+
id: "front-door",
|
|
234
|
+
locale: "en",
|
|
235
|
+
revision: "1",
|
|
236
|
+
source: { kind: "imported", reference: "@clossys/writer/front-door.en.json" },
|
|
237
|
+
entries: FRONT_DOOR_COPY_IDS.map(entryFor),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/** Whether `value` is one of the reserved front-door copy ids. */
|
|
241
|
+
export function isFrontDoorCopyId(value: unknown): value is FrontDoorKey {
|
|
242
|
+
return typeof value === "string" && (FRONT_DOOR_COPY_IDS as readonly string[]).includes(value);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Copies the own, enumerable string-keyed values of `nouns`; anything unreadable reads as no nouns at all. */
|
|
246
|
+
function readNouns(nouns: unknown): Map<string, unknown> {
|
|
247
|
+
const read = new Map<string, unknown>();
|
|
248
|
+
if (typeof nouns !== "object" || nouns === null || Array.isArray(nouns)) return read;
|
|
249
|
+
try {
|
|
250
|
+
for (const name of Object.keys(nouns)) read.set(name, (nouns as Record<string, unknown>)[name]);
|
|
251
|
+
} catch {
|
|
252
|
+
return new Map();
|
|
253
|
+
}
|
|
254
|
+
return read;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Names a lookup key for a message; a key that cannot be stringified (a null-prototype object, a throwing `toString`) reads as its type. */
|
|
258
|
+
function describeKey(key: unknown): string {
|
|
259
|
+
try {
|
|
260
|
+
return String(key);
|
|
261
|
+
} catch {
|
|
262
|
+
return `<${typeof key}>`;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Resolves one front-door entry from the shipped English defaults, naming
|
|
268
|
+
* the nouns it declares. Only nouns the entry declares reach
|
|
269
|
+
* `resolveCopyRef`; a name outside `FRONT_DOOR_NOUNS` is reported as
|
|
270
|
+
* `"unknown-noun"` and a declared noun that is absent or blank as
|
|
271
|
+
* `"missing-noun"`. An id outside `FRONT_DOOR_COPY_IDS` is
|
|
272
|
+
* `"unknown-copy-id"`. Pure and total: the defaults and `nouns` are not
|
|
273
|
+
* modified and no input makes this throw.
|
|
274
|
+
*/
|
|
275
|
+
export function resolveFrontDoorCopy(key: FrontDoorKey, nouns: FrontDoorNouns): FrontDoorCopyResolution {
|
|
276
|
+
if (!isFrontDoorCopyId(key)) {
|
|
277
|
+
return {
|
|
278
|
+
complete: false,
|
|
279
|
+
issues: [{ reason: "unknown-copy-id", id: describeKey(key), message: `Front-door copy id "${describeKey(key)}" is not a reserved id.` }],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const supplied = readNouns(nouns);
|
|
284
|
+
const issues: FrontDoorCopyIssue[] = [];
|
|
285
|
+
for (const name of supplied.keys()) {
|
|
286
|
+
if (!(FRONT_DOOR_NOUNS as readonly string[]).includes(name)) {
|
|
287
|
+
issues.push({ reason: "unknown-noun", noun: name, message: `"${name}" is not a front-door noun.` });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const entry = FRONT_DOOR_COPY_EN.entries.find((candidate) => candidate.id === key)!;
|
|
292
|
+
const values: Record<string, string> = {};
|
|
293
|
+
for (const name of entry.placeholders ?? []) {
|
|
294
|
+
const value = supplied.get(name);
|
|
295
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
296
|
+
issues.push({ reason: "missing-noun", id: key, noun: name, message: `Front-door copy "${key}" needs the noun "${name}", which is absent or blank.` });
|
|
297
|
+
} else {
|
|
298
|
+
values[name] = value;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (issues.length > 0) return { complete: false, issues };
|
|
302
|
+
|
|
303
|
+
const result = resolveCopyRef(FRONT_DOOR_COPY_EN, { id: key, values });
|
|
304
|
+
if (!result.complete || !result.resolution) {
|
|
305
|
+
// Unreachable for the shipped catalog (every id resolves, which the tests pin); kept so nothing here can throw or pass silently.
|
|
306
|
+
return {
|
|
307
|
+
complete: false,
|
|
308
|
+
issues: [{ reason: "unknown-copy-id", id: key, message: result.issues[0]?.message ?? `Front-door copy "${key}" could not be resolved.` }],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return { complete: true, text: result.resolution.text, resolution: result.resolution, issues: [] };
|
|
312
|
+
}
|