@clossys/writer 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +196 -0
- package/LICENSE +21 -0
- package/README.md +593 -0
- package/dist/addressability.d.ts +245 -0
- package/dist/addressability.d.ts.map +1 -0
- package/dist/addressability.js +567 -0
- package/dist/addressability.js.map +1 -0
- package/dist/checker.d.ts +159 -0
- package/dist/checker.d.ts.map +1 -0
- package/dist/checker.js +177 -0
- package/dist/checker.js.map +1 -0
- package/dist/cli.d.ts +149 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1049 -0
- package/dist/cli.js.map +1 -0
- package/dist/copy-gate.d.ts +107 -0
- package/dist/copy-gate.d.ts.map +1 -0
- package/dist/copy-gate.js +132 -0
- package/dist/copy-gate.js.map +1 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +64 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/locale-coverage.d.ts +227 -0
- package/dist/locale-coverage.d.ts.map +1 -0
- package/dist/locale-coverage.js +414 -0
- package/dist/locale-coverage.js.map +1 -0
- package/dist/passage.d.ts +294 -0
- package/dist/passage.d.ts.map +1 -0
- package/dist/passage.js +463 -0
- package/dist/passage.js.map +1 -0
- package/dist/path-exclusions.d.ts +114 -0
- package/dist/path-exclusions.d.ts.map +1 -0
- package/dist/path-exclusions.js +213 -0
- package/dist/path-exclusions.js.map +1 -0
- package/dist/registry.d.ts +61 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +64 -0
- package/dist/registry.js.map +1 -0
- package/dist/resolve.d.ts +33 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +133 -0
- package/dist/resolve.js.map +1 -0
- package/dist/scan.d.ts +457 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +1698 -0
- package/dist/scan.js.map +1 -0
- package/dist/schema.d.ts +81 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +482 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +268 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +40 -0
- package/dist/types.js.map +1 -0
- package/dist/voice/checker.d.ts +186 -0
- package/dist/voice/checker.d.ts.map +1 -0
- package/dist/voice/checker.js +483 -0
- package/dist/voice/checker.js.map +1 -0
- package/dist/voice/derivation-coverage.d.ts +168 -0
- package/dist/voice/derivation-coverage.d.ts.map +1 -0
- package/dist/voice/derivation-coverage.js +181 -0
- package/dist/voice/derivation-coverage.js.map +1 -0
- package/dist/voice/fields.d.ts +107 -0
- package/dist/voice/fields.d.ts.map +1 -0
- package/dist/voice/fields.js +144 -0
- package/dist/voice/fields.js.map +1 -0
- package/dist/voice/index.d.ts +19 -0
- package/dist/voice/index.d.ts.map +1 -0
- package/dist/voice/index.js +24 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/internal/parse-template.d.ts +27 -0
- package/dist/voice/internal/parse-template.d.ts.map +1 -0
- package/dist/voice/internal/parse-template.js +111 -0
- package/dist/voice/internal/parse-template.js.map +1 -0
- package/dist/voice/internal/pattern-safety.d.ts +111 -0
- package/dist/voice/internal/pattern-safety.d.ts.map +1 -0
- package/dist/voice/internal/pattern-safety.js +278 -0
- package/dist/voice/internal/pattern-safety.js.map +1 -0
- package/dist/voice/schema.d.ts +65 -0
- package/dist/voice/schema.d.ts.map +1 -0
- package/dist/voice/schema.js +535 -0
- package/dist/voice/schema.js.map +1 -0
- package/dist/voice/types.d.ts +282 -0
- package/dist/voice/types.d.ts.map +1 -0
- package/dist/voice/types.js +37 -0
- package/dist/voice/types.js.map +1 -0
- package/package.json +72 -0
- package/src/addressability.ts +664 -0
- package/src/checker.ts +293 -0
- package/src/cli.ts +1166 -0
- package/src/copy-gate.ts +197 -0
- package/src/fingerprint.ts +66 -0
- package/src/index.ts +163 -0
- package/src/locale-coverage.ts +512 -0
- package/src/passage.ts +644 -0
- package/src/path-exclusions.ts +265 -0
- package/src/registry.ts +103 -0
- package/src/resolve.ts +172 -0
- package/src/scan.ts +1959 -0
- package/src/schema.ts +528 -0
- package/src/types.ts +301 -0
- package/src/voice/checker.ts +590 -0
- package/src/voice/derivation-coverage.ts +217 -0
- package/src/voice/fields.ts +160 -0
- package/src/voice/index.ts +55 -0
- package/src/voice/internal/parse-template.ts +114 -0
- package/src/voice/internal/pattern-safety.ts +336 -0
- package/src/voice/schema.ts +585 -0
- package/src/voice/types.ts +329 -0
- package/templates/voice-record.template.jsonc +135 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path exclusions for `scan.ts`'s SCANNING SURFACE — `scanCopySourceTree`'s
|
|
3
|
+
* directory walk — never to be confused with `scan.ts`'s existing
|
|
4
|
+
* `ExclusionReason`/`ExcludedLiteral`, a DIFFERENT feature this file does
|
|
5
|
+
* not touch. Read `scan.ts`'s own top doc comment ("THE BOUNDARY") before
|
|
6
|
+
* this one: that mechanism decides, for a LITERAL already found inside a
|
|
7
|
+
* file that IS being scanned, whether that one literal is real copy or
|
|
8
|
+
* something structural (an import specifier, a CSS class, an aria label,
|
|
9
|
+
* ...) — a per-LITERAL classification made from the few dozen characters of
|
|
10
|
+
* context immediately around it. This file answers a completely different
|
|
11
|
+
* question, at a completely different granularity: should this FILE be
|
|
12
|
+
* looked at for copy candidates AT ALL, before a single character of it is
|
|
13
|
+
* ever tokenized? That is a per-FILE scope decision, made once, from the
|
|
14
|
+
* file's own path alone — extending `ExclusionReason` to cover it would be
|
|
15
|
+
* a coincidence of both being named "exclusion", not a shared mechanism;
|
|
16
|
+
* this is why it is a new, separate type (`PathExclusion`) and a new,
|
|
17
|
+
* separate `ScanResult` field (`excludedFiles`), not a tenth
|
|
18
|
+
* `ExclusionReason`.
|
|
19
|
+
*
|
|
20
|
+
* THE MOTIVATING CASE ("mention vs. use"): a style guide or README that
|
|
21
|
+
* documents this voice's own banned terms — "never say X, say Y instead" —
|
|
22
|
+
* necessarily CONTAINS the literal banned text, as a MENTION, not a USE.
|
|
23
|
+
* Scanned like any other file, that mention looks identical to real,
|
|
24
|
+
* unregistered product copy to `checkCopyTraceability` (`copy-gate.ts`):
|
|
25
|
+
* the traceability gate has no notion of "this file's job is to talk about
|
|
26
|
+
* copy, not ship it". A `PathExclusion` on that one file's path is the fix
|
|
27
|
+
* — the file is never tokenized at all, so it can never produce a
|
|
28
|
+
* candidate, `unregistered-copy` finding, or citation. See this file's
|
|
29
|
+
* README section, "Path exclusions vs. per-literal exclusions", for the
|
|
30
|
+
* fuller argument, including why this stays scoped to `scan.ts`'s existing
|
|
31
|
+
* scanning surface (copy TRACEABILITY) rather than inventing a new
|
|
32
|
+
* directory-scanning feature for VOICE/glossary rules, which this package
|
|
33
|
+
* does not otherwise have — a consumer building their own "walk a
|
|
34
|
+
* directory, then run each file's text through `checkCopy`" tool gets the
|
|
35
|
+
* identical mention-vs-use protection by reusing THIS mechanism (call
|
|
36
|
+
* `scanCopySourceTree` with `pathExclusions`, then feed its candidates'
|
|
37
|
+
* text through `checkCopy` themselves) rather than this package growing a
|
|
38
|
+
* second, redundant exclusion list.
|
|
39
|
+
*
|
|
40
|
+
* FAILS CLOSED, mirroring `checker.ts`'s `VoiceCheckWaiver` handling (the
|
|
41
|
+
* established precedent for "a caller-supplied list of exceptions" in this
|
|
42
|
+
* package's ecosystem) almost exactly:
|
|
43
|
+
*
|
|
44
|
+
* - A MALFORMED entry (not an object, `path`/`reason` missing or not a
|
|
45
|
+
* non-empty string, or a `path` this file's own small pattern language
|
|
46
|
+
* cannot parse — see `classifyPathExclusionPattern`) is never applied:
|
|
47
|
+
* it exempts NOTHING, and is reported as its own `"error"`-severity
|
|
48
|
+
* `PathExclusionFinding` — the exact "a malformed exclusion entry is a
|
|
49
|
+
* finding, never a silent exemption" requirement. This mirrors
|
|
50
|
+
* `"waiver:invalid"`.
|
|
51
|
+
* - An exclusion that never matched a single scanned file is dead
|
|
52
|
+
* configuration — reported as its own `"warning"`-severity
|
|
53
|
+
* `PathExclusionFinding`, mirroring `"waiver:unused"`, so a stale
|
|
54
|
+
* exclusion (the file it named was renamed or deleted) cannot silently
|
|
55
|
+
* sit in a config forever looking like it is still doing something.
|
|
56
|
+
*/
|
|
57
|
+
function isPlainObject(value) {
|
|
58
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
59
|
+
}
|
|
60
|
+
function isNonEmptyString(value) {
|
|
61
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Classifies `pattern` into one of this file's three supported shapes, or
|
|
65
|
+
* `undefined` if it matches none of them — the entire "glob" grammar this
|
|
66
|
+
* package supports, deliberately small (no glob library, per this
|
|
67
|
+
* repository's zero-runtime-dependency rule):
|
|
68
|
+
*
|
|
69
|
+
* 1. `"exact"` — no `*` anywhere. Matches only the identical repo-relative
|
|
70
|
+
* path, e.g. `"docs/style-guide.ts"`.
|
|
71
|
+
* 2. `"subtree"` — ends in exactly `"/**"`, with no other `*` anywhere
|
|
72
|
+
* else in the pattern. Matches every file under that directory,
|
|
73
|
+
* recursively, e.g. `"docs/**"` matches `"docs/style-guide.ts"` AND
|
|
74
|
+
* `"docs/internal/glossary.ts"`.
|
|
75
|
+
* 3. `"segment-wildcard"` — exactly one `*` (not `**`), confined to the
|
|
76
|
+
* pattern's FINAL `/`-separated segment, e.g. `"docs/*.ts"`. Every
|
|
77
|
+
* segment before the last must match exactly; the last segment's `*`
|
|
78
|
+
* matches any run of characters (including none) within that one
|
|
79
|
+
* segment only — it does not cross a `/`.
|
|
80
|
+
*
|
|
81
|
+
* A `*`/`**` anywhere else (mid-path, more than one `*` in the final
|
|
82
|
+
* segment, a `**` not forming the whole-pattern `"/**"` suffix, an empty
|
|
83
|
+
* pattern) is rejected — `undefined` — by design: this is a fixed-scope
|
|
84
|
+
* exclusion mechanism for known files/directories, not a general glob
|
|
85
|
+
* engine, and a caller who needs more expressive matching should use
|
|
86
|
+
* `"subtree"` on a containing directory instead.
|
|
87
|
+
*/
|
|
88
|
+
function classifyPathExclusionPattern(pattern) {
|
|
89
|
+
if (pattern.length === 0)
|
|
90
|
+
return undefined;
|
|
91
|
+
if (!pattern.includes("*"))
|
|
92
|
+
return "exact";
|
|
93
|
+
if (pattern.endsWith("/**") && pattern.indexOf("*") === pattern.length - 2) {
|
|
94
|
+
// The only "*" characters are the trailing "**" pair, and nothing
|
|
95
|
+
// precedes them but a "/" — reject a bare "**" with no directory prefix
|
|
96
|
+
// (nothing meaningful to scope to) and reject any other "*" earlier in
|
|
97
|
+
// the string (already true here: `indexOf` found the FIRST "*" at the
|
|
98
|
+
// trailing pair's position, so there is no earlier one).
|
|
99
|
+
const prefix = pattern.slice(0, -3);
|
|
100
|
+
return prefix.length > 0 && !prefix.includes("*") ? "subtree" : undefined;
|
|
101
|
+
}
|
|
102
|
+
const lastSlash = pattern.lastIndexOf("/");
|
|
103
|
+
const lastSegment = lastSlash === -1 ? pattern : pattern.slice(lastSlash + 1);
|
|
104
|
+
const priorSegments = lastSlash === -1 ? "" : pattern.slice(0, lastSlash);
|
|
105
|
+
const starCount = (lastSegment.match(/\*/g) ?? []).length;
|
|
106
|
+
if (starCount === 1 && !lastSegment.includes("**") && !priorSegments.includes("*")) {
|
|
107
|
+
return "segment-wildcard";
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Matches `relPath` (already `/`-joined, repo-relative — the same shape
|
|
113
|
+
* `scanCopySourceTree` produces for every `CopyCandidate.file`) against one
|
|
114
|
+
* already-classified exclusion. Never called with a pattern that failed
|
|
115
|
+
* `classifyPathExclusionPattern` — `validatePathExclusions` filters those
|
|
116
|
+
* out before anything reaches here, so this function does not re-validate.
|
|
117
|
+
*/
|
|
118
|
+
export function matchesPathExclusion(relPath, exclusion) {
|
|
119
|
+
switch (exclusion.kind) {
|
|
120
|
+
case "exact":
|
|
121
|
+
return relPath === exclusion.path;
|
|
122
|
+
case "subtree": {
|
|
123
|
+
const prefix = exclusion.path.slice(0, -2); // "docs/**" -> "docs/"
|
|
124
|
+
return relPath.startsWith(prefix);
|
|
125
|
+
}
|
|
126
|
+
case "segment-wildcard": {
|
|
127
|
+
const lastSlash = exclusion.path.lastIndexOf("/");
|
|
128
|
+
const priorSegments = lastSlash === -1 ? "" : exclusion.path.slice(0, lastSlash + 1);
|
|
129
|
+
const lastSegmentPattern = lastSlash === -1 ? exclusion.path : exclusion.path.slice(lastSlash + 1);
|
|
130
|
+
if (!relPath.startsWith(priorSegments))
|
|
131
|
+
return false;
|
|
132
|
+
const relRest = relPath.slice(priorSegments.length);
|
|
133
|
+
if (relRest.includes("/"))
|
|
134
|
+
return false; // the wildcard segment never crosses a "/"
|
|
135
|
+
const [before, after] = lastSegmentPattern.split("*");
|
|
136
|
+
return relRest.startsWith(before) && relRest.endsWith(after) && relRest.length >= before.length + after.length;
|
|
137
|
+
}
|
|
138
|
+
default: {
|
|
139
|
+
const unhandled = exclusion.kind;
|
|
140
|
+
throw new Error(`matchesPathExclusion: unknown pattern kind ${JSON.stringify(unhandled)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Validates `value` (`ScanOptions.pathExclusions`, or any raw candidate list
|
|
146
|
+
* — this is called on a TYPED `PathExclusion[]` from a TypeScript caller,
|
|
147
|
+
* but re-validates every field anyway, the same defense-in-depth this
|
|
148
|
+
* repository's other schema functions apply to already-typed input: see
|
|
149
|
+
* `@clossys/writer/voice`'s `schema.ts` header comment for why a
|
|
150
|
+
* plain-JS caller, or a value that merely satisfies the type at compile
|
|
151
|
+
* time without conforming at runtime, is exactly the case this guards
|
|
152
|
+
* against). Never throws.
|
|
153
|
+
*/
|
|
154
|
+
export function validatePathExclusions(value) {
|
|
155
|
+
if (value === undefined)
|
|
156
|
+
return { valid: [], findings: [] };
|
|
157
|
+
if (!Array.isArray(value)) {
|
|
158
|
+
return {
|
|
159
|
+
valid: [],
|
|
160
|
+
findings: [
|
|
161
|
+
{
|
|
162
|
+
rule: "path-exclusion-invalid",
|
|
163
|
+
severity: "error",
|
|
164
|
+
message: `pathExclusions must be an array when present, got ${JSON.stringify(value)}.`,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const valid = [];
|
|
170
|
+
const findings = [];
|
|
171
|
+
value.forEach((raw, i) => {
|
|
172
|
+
if (!isPlainObject(raw)) {
|
|
173
|
+
findings.push({
|
|
174
|
+
rule: "path-exclusion-invalid",
|
|
175
|
+
severity: "error",
|
|
176
|
+
message: `pathExclusions[${i}] must be an object with "path" and "reason", got ${JSON.stringify(raw)}.`,
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const path = raw.path;
|
|
181
|
+
const reason = raw.reason;
|
|
182
|
+
if (!isNonEmptyString(path)) {
|
|
183
|
+
findings.push({
|
|
184
|
+
rule: "path-exclusion-invalid",
|
|
185
|
+
severity: "error",
|
|
186
|
+
message: `pathExclusions[${i}].path must be a non-empty string, got ${JSON.stringify(path)}.`,
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (!isNonEmptyString(reason)) {
|
|
191
|
+
findings.push({
|
|
192
|
+
rule: "path-exclusion-invalid",
|
|
193
|
+
severity: "error",
|
|
194
|
+
message: `pathExclusions[${i}].reason must be a non-empty string, got ${JSON.stringify(reason)}. An exclusion with no stated reason is not auditable.`,
|
|
195
|
+
path,
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const kind = classifyPathExclusionPattern(path);
|
|
200
|
+
if (kind === undefined) {
|
|
201
|
+
findings.push({
|
|
202
|
+
rule: "path-exclusion-invalid",
|
|
203
|
+
severity: "error",
|
|
204
|
+
message: `pathExclusions[${i}].path "${path}" is not a supported pattern — supported shapes are an exact path, a "dir/**" subtree, or a single "*" confined to the final path segment. See path-exclusions.ts's classifyPathExclusionPattern doc comment.`,
|
|
205
|
+
path,
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
valid.push({ path, reason, kind });
|
|
210
|
+
});
|
|
211
|
+
return { valid, findings };
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=path-exclusions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-exclusions.js","sourceRoot":"","sources":["../src/path-exclusions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `readCopyRecord` — the one place in this package that touches a
|
|
3
|
+
* filesystem. Everything in `schema.ts` is pure shape; this function is
|
|
4
|
+
* what turns a consumer's real copy registry file into validated data, in
|
|
5
|
+
* the same gather-don't-judge spirit `@example/strategy`'s
|
|
6
|
+
* `readStrategy` uses for a `strategy/` directory: read what's there,
|
|
7
|
+
* validate it, and record — never throw on — anything that could not be
|
|
8
|
+
* turned into usable data. Judgement about what to DO with an unreadable
|
|
9
|
+
* or invalid registry (fail a build, warn, block a release) belongs to
|
|
10
|
+
* the caller, not to this function.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately narrower than `readStrategy`: a `CopyRecord` is a single
|
|
13
|
+
* JSON file, not a directory of several independently-optional files, so
|
|
14
|
+
* there is exactly one thing that can go wrong and exactly one place it
|
|
15
|
+
* can go wrong at.
|
|
16
|
+
*/
|
|
17
|
+
import type { CopyRecord } from "./types.js";
|
|
18
|
+
/**
|
|
19
|
+
* Why `path` did not become a usable `CopyRecord`. `"unreadable"` is a
|
|
20
|
+
* real I/O failure — the file does not exist, a permission was denied, or
|
|
21
|
+
* it vanished mid-read; unknown content could be hiding behind any of
|
|
22
|
+
* those, which is why they are not distinguished further here.
|
|
23
|
+
* `"unparseable"` means the bytes were read but are not valid JSON.
|
|
24
|
+
* `"invalid-schema"` means the JSON parsed but does not satisfy
|
|
25
|
+
* `CopyRecord`'s schema (a missing required field, a wrong type, a
|
|
26
|
+
* duplicate id, a declared placeholder missing from its entry's text) —
|
|
27
|
+
* see `schema.ts`'s `validateCopyRecordShape` for the full list of checks
|
|
28
|
+
* that can produce this.
|
|
29
|
+
*/
|
|
30
|
+
export type CopyRegistryReadIssueReason = "unreadable" | "unparseable" | "invalid-schema";
|
|
31
|
+
export interface CopyRegistryReadIssue {
|
|
32
|
+
reason: CopyRegistryReadIssueReason;
|
|
33
|
+
/** Human-readable detail: the I/O error message, the JSON parse error, or `parseCopyRecord`'s full issue list. */
|
|
34
|
+
detail: string;
|
|
35
|
+
}
|
|
36
|
+
export interface CopyRegistryReadResult {
|
|
37
|
+
/** The path this result was read from, exactly as given — echoed back for a caller building its own error message. */
|
|
38
|
+
path: string;
|
|
39
|
+
/** The parsed, defaulted `CopyRecord`, or `undefined` if `issues` is non-empty. */
|
|
40
|
+
record?: CopyRecord;
|
|
41
|
+
/** Empty means `path` was read, parsed, and validated cleanly. Non-empty means `record` is `undefined` — see `CopyRegistryReadIssueReason`. */
|
|
42
|
+
issues: CopyRegistryReadIssue[];
|
|
43
|
+
/**
|
|
44
|
+
* `true` exactly when `issues` is empty and `record` is populated.
|
|
45
|
+
* Named to match `@example/strategy`'s `StrategyBundle.complete`
|
|
46
|
+
* and `@clossys/writer/voice`'s `VoiceCheckReport.complete` —
|
|
47
|
+
* "everything this function could have checked, it did, and it found
|
|
48
|
+
* nothing wrong" is the same one-boolean-read contract across all
|
|
49
|
+
* three, on purpose.
|
|
50
|
+
*/
|
|
51
|
+
complete: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Reads and validates the `CopyRecord` at `path`. Never throws: every
|
|
55
|
+
* failure — an unreadable file, invalid JSON, or a schema violation — is
|
|
56
|
+
* recorded into the returned `CopyRegistryReadResult.issues` and
|
|
57
|
+
* reflected in `.complete`, the same discipline `readStrategy` holds to
|
|
58
|
+
* for a strategy directory.
|
|
59
|
+
*/
|
|
60
|
+
export declare function readCopyRecord(path: string): CopyRegistryReadResult;
|
|
61
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,2BAA2B,GAAG,YAAY,GAAG,aAAa,GAAG,gBAAgB,CAAC;AAE1F,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,2BAA2B,CAAC;IACpC,kHAAkH;IAClH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,sHAAsH;IACtH,IAAI,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,+IAA+I;IAC/I,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,sBAAsB,CAoCnE"}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `readCopyRecord` — the one place in this package that touches a
|
|
3
|
+
* filesystem. Everything in `schema.ts` is pure shape; this function is
|
|
4
|
+
* what turns a consumer's real copy registry file into validated data, in
|
|
5
|
+
* the same gather-don't-judge spirit `@example/strategy`'s
|
|
6
|
+
* `readStrategy` uses for a `strategy/` directory: read what's there,
|
|
7
|
+
* validate it, and record — never throw on — anything that could not be
|
|
8
|
+
* turned into usable data. Judgement about what to DO with an unreadable
|
|
9
|
+
* or invalid registry (fail a build, warn, block a release) belongs to
|
|
10
|
+
* the caller, not to this function.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately narrower than `readStrategy`: a `CopyRecord` is a single
|
|
13
|
+
* JSON file, not a directory of several independently-optional files, so
|
|
14
|
+
* there is exactly one thing that can go wrong and exactly one place it
|
|
15
|
+
* can go wrong at.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { parseCopyRecord } from "./schema.js";
|
|
19
|
+
/**
|
|
20
|
+
* Reads and validates the `CopyRecord` at `path`. Never throws: every
|
|
21
|
+
* failure — an unreadable file, invalid JSON, or a schema violation — is
|
|
22
|
+
* recorded into the returned `CopyRegistryReadResult.issues` and
|
|
23
|
+
* reflected in `.complete`, the same discipline `readStrategy` holds to
|
|
24
|
+
* for a strategy directory.
|
|
25
|
+
*/
|
|
26
|
+
export function readCopyRecord(path) {
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
raw = readFileSync(path, "utf8");
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return {
|
|
33
|
+
path,
|
|
34
|
+
issues: [{ reason: "unreadable", detail: error instanceof Error ? error.message : String(error) }],
|
|
35
|
+
complete: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(raw);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return {
|
|
44
|
+
path,
|
|
45
|
+
issues: [{ reason: "unparseable", detail: error instanceof Error ? error.message : String(error) }],
|
|
46
|
+
complete: false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const record = parseCopyRecord(parsed);
|
|
51
|
+
return { path, record, issues: [], complete: true };
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// parseCopyRecord throws a single Error whose message already lists
|
|
55
|
+
// every validateCopyRecordShape finding, one per line — reused as-is
|
|
56
|
+
// rather than re-deriving the same detail a second way.
|
|
57
|
+
return {
|
|
58
|
+
path,
|
|
59
|
+
issues: [{ reason: "invalid-schema", detail: error instanceof Error ? error.message : String(error) }],
|
|
60
|
+
complete: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAyC9C;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACnC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,IAAI;YACJ,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YAClG,QAAQ,EAAE,KAAK;SAChB,CAAC;IACJ,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,IAAI;YACJ,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACnG,QAAQ,EAAE,KAAK;SAChB,CAAC;IACJ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QACvC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACtD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,oEAAoE;QACpE,qEAAqE;QACrE,wDAAwD;QACxD,OAAO;YACL,IAAI;YACJ,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtG,QAAQ,EAAE,KAAK;SAChB,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict, deterministic `CopyRef` resolution for rendered surfaces.
|
|
3
|
+
*
|
|
4
|
+
* This is intentionally separate from the source scanner's `CopyRecord`
|
|
5
|
+
* support. A scanner can compare a local sentence against an unversioned
|
|
6
|
+
* record; publishing a page needs stronger evidence: a locale, revision,
|
|
7
|
+
* source provenance, and an approved entry. The result exposes every one of
|
|
8
|
+
* those inputs so `@example/surface` can place them in its output
|
|
9
|
+
* manifest without knowing how a consumer stores copy.
|
|
10
|
+
*/
|
|
11
|
+
import type { CopyRef, CopyRegistry, CopyResolution, CopyResolver } from "./types.js";
|
|
12
|
+
export type CopyResolveIssueReason = "invalid-registry" | "invalid-ref" | "locale-mismatch" | "unknown-copy-id" | "copy-not-approved" | "missing-placeholder-value" | "unexpected-placeholder-value";
|
|
13
|
+
export interface CopyResolveIssue {
|
|
14
|
+
reason: CopyResolveIssueReason;
|
|
15
|
+
message: string;
|
|
16
|
+
id?: string;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface CopyResolveResult {
|
|
20
|
+
resolution?: CopyResolution;
|
|
21
|
+
issues: CopyResolveIssue[];
|
|
22
|
+
complete: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolves a `CopyRef` against one locale-specific registry. Any absence,
|
|
26
|
+
* lifecycle failure, locale mismatch, or placeholder mismatch produces no
|
|
27
|
+
* text and a non-complete result. Consumers may log `issues`; renderers must
|
|
28
|
+
* treat a missing `resolution` as an unresolved required input.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveCopyRef(registry: CopyRegistry | unknown, ref: CopyRef | unknown): CopyResolveResult;
|
|
31
|
+
/** Creates the narrow resolver callback for presentation code. */
|
|
32
|
+
export declare function createCopyResolver(registry: CopyRegistry | unknown): CopyResolver;
|
|
33
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,OAAO,EACP,YAAY,EACZ,cAAc,EACd,YAAY,EAEb,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,sBAAsB,GAC9B,kBAAkB,GAClB,aAAa,GACb,iBAAiB,GACjB,iBAAiB,GACjB,mBAAmB,GACnB,2BAA2B,GAC3B,8BAA8B,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,sBAAsB,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAkBD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,GAAG,iBAAiB,CAsG1G;AAED,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,GAAG,YAAY,CAEjF"}
|
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict, deterministic `CopyRef` resolution for rendered surfaces.
|
|
3
|
+
*
|
|
4
|
+
* This is intentionally separate from the source scanner's `CopyRecord`
|
|
5
|
+
* support. A scanner can compare a local sentence against an unversioned
|
|
6
|
+
* record; publishing a page needs stronger evidence: a locale, revision,
|
|
7
|
+
* source provenance, and an approved entry. The result exposes every one of
|
|
8
|
+
* those inputs so `@example/surface` can place them in its output
|
|
9
|
+
* manifest without knowing how a consumer stores copy.
|
|
10
|
+
*/
|
|
11
|
+
import { validateCopyRegistryShape } from "./schema.js";
|
|
12
|
+
function isNonEmptyString(value) {
|
|
13
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
14
|
+
}
|
|
15
|
+
function isPlainObject(value) {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function isCopyValue(value) {
|
|
19
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
20
|
+
}
|
|
21
|
+
function interpolate(text, values) {
|
|
22
|
+
return text.replace(/\{([^{}]+)\}/g, (_match, name) => String(values[name]));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolves a `CopyRef` against one locale-specific registry. Any absence,
|
|
26
|
+
* lifecycle failure, locale mismatch, or placeholder mismatch produces no
|
|
27
|
+
* text and a non-complete result. Consumers may log `issues`; renderers must
|
|
28
|
+
* treat a missing `resolution` as an unresolved required input.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveCopyRef(registry, ref) {
|
|
31
|
+
const registryFindings = validateCopyRegistryShape(registry);
|
|
32
|
+
if (registryFindings.length > 0) {
|
|
33
|
+
return {
|
|
34
|
+
issues: [{ reason: "invalid-registry", message: "CopyRegistry must pass validateCopyRegistryShape before it can resolve CopyRefs." }],
|
|
35
|
+
complete: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Safe after the validator's complete structural check. The public
|
|
39
|
+
// signature still accepts unknown because JavaScript callers can bypass
|
|
40
|
+
// TypeScript and resolution must fail closed rather than throw.
|
|
41
|
+
const validRegistry = registry;
|
|
42
|
+
if (!isPlainObject(ref) || !isNonEmptyString(ref.id) || (ref.locale !== undefined && !isNonEmptyString(ref.locale)) || (ref.values !== undefined && !isPlainObject(ref.values))) {
|
|
43
|
+
return {
|
|
44
|
+
issues: [{ reason: "invalid-ref", message: "CopyRef.id must be a non-empty string." }],
|
|
45
|
+
complete: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const validRef = ref;
|
|
49
|
+
if (validRef.locale !== undefined && validRef.locale !== validRegistry.locale) {
|
|
50
|
+
return {
|
|
51
|
+
issues: [
|
|
52
|
+
{
|
|
53
|
+
reason: "locale-mismatch",
|
|
54
|
+
id: validRef.id,
|
|
55
|
+
message: `CopyRef "${validRef.id}" requests locale "${validRef.locale}", but this registry provides "${validRegistry.locale}".`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
complete: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const entry = validRegistry.entries.find((candidate) => candidate.id === validRef.id);
|
|
62
|
+
if (!entry) {
|
|
63
|
+
return {
|
|
64
|
+
issues: [
|
|
65
|
+
{ reason: "unknown-copy-id", id: validRef.id, message: `CopyRef "${validRef.id}" is not present in registry "${validRegistry.id}".` },
|
|
66
|
+
],
|
|
67
|
+
complete: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (entry.status !== "approved") {
|
|
71
|
+
return {
|
|
72
|
+
issues: [
|
|
73
|
+
{
|
|
74
|
+
reason: "copy-not-approved",
|
|
75
|
+
id: validRef.id,
|
|
76
|
+
message: `CopyRef "${validRef.id}" is ${entry.status} and cannot be rendered until it is approved.`,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
complete: false,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const values = validRef.values ?? {};
|
|
83
|
+
const issues = [];
|
|
84
|
+
const declared = new Set(entry.placeholders ?? []);
|
|
85
|
+
for (const placeholder of declared) {
|
|
86
|
+
if (!Object.hasOwn(values, placeholder)) {
|
|
87
|
+
issues.push({
|
|
88
|
+
reason: "missing-placeholder-value",
|
|
89
|
+
id: validRef.id,
|
|
90
|
+
placeholder,
|
|
91
|
+
message: `CopyRef "${validRef.id}" is missing a value for required placeholder "${placeholder}".`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else if (!isCopyValue(values[placeholder])) {
|
|
95
|
+
issues.push({
|
|
96
|
+
reason: "missing-placeholder-value",
|
|
97
|
+
id: validRef.id,
|
|
98
|
+
placeholder,
|
|
99
|
+
message: `CopyRef "${validRef.id}" has an invalid value for placeholder "${placeholder}".`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const name of Object.keys(values)) {
|
|
104
|
+
if (!declared.has(name)) {
|
|
105
|
+
issues.push({
|
|
106
|
+
reason: "unexpected-placeholder-value",
|
|
107
|
+
id: validRef.id,
|
|
108
|
+
placeholder: name,
|
|
109
|
+
message: `CopyRef "${validRef.id}" supplies "${name}", which is not declared by that copy entry.`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (issues.length > 0)
|
|
114
|
+
return { issues, complete: false };
|
|
115
|
+
return {
|
|
116
|
+
resolution: {
|
|
117
|
+
ref: validRef,
|
|
118
|
+
text: interpolate(entry.text, values),
|
|
119
|
+
recordId: validRegistry.id,
|
|
120
|
+
revision: validRegistry.revision,
|
|
121
|
+
locale: validRegistry.locale,
|
|
122
|
+
source: validRegistry.source,
|
|
123
|
+
entryId: entry.id,
|
|
124
|
+
},
|
|
125
|
+
issues: [],
|
|
126
|
+
complete: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Creates the narrow resolver callback for presentation code. */
|
|
130
|
+
export function createCopyResolver(registry) {
|
|
131
|
+
return (ref) => resolveCopyRef(registry, ref).resolution;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AASH,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAwBxD,SAAS,gBAAgB,CAAC,KAAc;IACtC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS,CAAC;AAC9F,CAAC;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,MAA2C;IAC5E,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC,MAAM,EAAE,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAE,CAAC,CAAC,CAAC;AACxF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgC,EAAE,GAAsB;IACrF,MAAM,gBAAgB,GAAG,yBAAyB,CAAC,QAAQ,CAAC,CAAC;IAC7D,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,OAAO;YACL,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,kFAAkF,EAAE,CAAC;YACrI,QAAQ,EAAE,KAAK;SAChB,CAAC;IACJ,CAAC;IACD,mEAAmE;IACnE,wEAAwE;IACxE,gEAAgE;IAChE,MAAM,aAAa,GAAG,QAAwB,CAAC;IAE/C,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QAChL,OAAO;YACL,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,wCAAwC,EAAE,CAAC;YACtF,QAAQ,EAAE,KAAK;SAChB,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,GAAkF,CAAC;IAEpG,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,KAAK,aAAa,CAAC,MAAM,EAAE,CAAC;QAC9E,OAAO;YACL,MAAM,EAAE;gBACN;oBACE,MAAM,EAAE,iBAAiB;oBACzB,EAAE,EAAE,QAAQ,CAAC,EAAE;oBACf,OAAO,EAAE,YAAY,QAAQ,CAAC,EAAE,sBAAsB,QAAQ,CAAC,MAAM,kCAAkC,aAAa,CAAC,MAAM,IAAI;iBAChI;aACF;YACD,QAAQ,EAAE,KAAK;SAChB,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,QAAQ,CAAC,EAAE,CAAC,CAAC;IACtF,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;YACL,MAAM,EAAE;gBACN,EAAE,MAAM,EAAE,iBAAiB,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,EAAE,YAAY,QAAQ,CAAC,EAAE,iCAAiC,aAAa,CAAC,EAAE,IAAI,EAAE;aACtI;YACD,QAAQ,EAAE,KAAK;SAChB,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QAChC,OAAO;YACL,MAAM,EAAE;gBACN;oBACE,MAAM,EAAE,mBAAmB;oBAC3B,EAAE,EAAE,QAAQ,CAAC,EAAE;oBACf,OAAO,EAAE,YAAY,QAAQ,CAAC,EAAE,QAAQ,KAAK,CAAC,MAAM,+CAA+C;iBACpG;aACF;YACD,QAAQ,EAAE,KAAK;SAChB,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAC;IACrC,MAAM,MAAM,GAAuB,EAAE,CAAC;IACtC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;IACnD,KAAK,MAAM,WAAW,IAAI,QAAQ,EAAE,CAAC;QACnC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,CAAC;YACxC,MAAM,CAAC,IAAI,CAAC;gBACV,MAAM,EAAE,2BAA2B;gBACnC,EAAE,EAAE,QAAQ,CAAC,EAAE;gBACf,WAAW;gBACX,OAAO,EAAE,YAAY,QAAQ,CAAC,EAAE,kDAAkD,WAAW,IAAI;aAClG,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC;YAC7C,MAAM,CAAC,IAAI,CAAC;gBACV,MAAM,EAAE,2BAA2B;gBACnC,EAAE,EAAE,QAAQ,CAAC,EAAE;gBACf,WAAW;gBACX,OAAO,EAAE,YAAY,QAAQ,CAAC,EAAE,2CAA2C,WAAW,IAAI;aAC3F,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC;gBACV,MAAM,EAAE,8BAA8B;gBACtC,EAAE,EAAE,QAAQ,CAAC,EAAE;gBACf,WAAW,EAAE,IAAI;gBACjB,OAAO,EAAE,YAAY,QAAQ,CAAC,EAAE,eAAe,IAAI,8CAA8C;aAClG,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAE1D,OAAO;QACL,UAAU,EAAE;YACV,GAAG,EAAE,QAA8B;YACnC,IAAI,EAAE,WAAW,CAAC,KAAK,CAAC,IAAI,EAAE,MAA6C,CAAC;YAC5E,QAAQ,EAAE,aAAa,CAAC,EAAE;YAC1B,QAAQ,EAAE,aAAa,CAAC,QAAQ;YAChC,MAAM,EAAE,aAAa,CAAC,MAAM;YAC5B,MAAM,EAAE,aAAa,CAAC,MAAM;YAC5B,OAAO,EAAE,KAAK,CAAC,EAAE;SAClB;QACD,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE,IAAI;KACf,CAAC;AACJ,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,kBAAkB,CAAC,QAAgC;IACjE,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC;AAC3D,CAAC"}
|