@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
package/src/copy-gate.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `checkCopyTraceability` — the gate that makes `scan.ts`'s extraction
|
|
3
|
+
* worth having. Pure: takes already-extracted `CopyCandidate[]`/
|
|
4
|
+
* `Citation[]` (see `scan.ts`) and an already-loaded `CopyRecord` (see
|
|
5
|
+
* `./types.js`), does no I/O of its own, and never throws on any input
|
|
6
|
+
* shape — the same discipline `@example/strategy`'s
|
|
7
|
+
* `checkFactsTraceability` holds to in `facts-gate.ts`. `scanCopySourceTree`
|
|
8
|
+
* (see `scan.ts`) is the half that gathers candidates/citations from a
|
|
9
|
+
* real directory; `cli.ts` is what loads a real `CopyRecord` from disk
|
|
10
|
+
* (via the sibling `registry.ts`) and turns this function's result into an
|
|
11
|
+
* exit code.
|
|
12
|
+
*
|
|
13
|
+
* THE RULE, in one sentence: every user-facing string or template literal
|
|
14
|
+
* `scan.ts` extracts from a source tree must either match a registered
|
|
15
|
+
* `CopyEntry.text` (compared by STATIC SHAPE — interpolations collapsed to
|
|
16
|
+
* `PLACEHOLDER_SENTINEL` on both sides, so a source literal's actual
|
|
17
|
+
* expression content and a registered entry's placeholder NAMES never
|
|
18
|
+
* need to agree) or be annotated with a `copy:<id>` comment citing a real
|
|
19
|
+
* entry in the record on the same line — anything else is reported as
|
|
20
|
+
* untraced. This is `@example/strategy`'s `fact:<key>` citation
|
|
21
|
+
* pattern, applied to copy instead of facts.
|
|
22
|
+
*
|
|
23
|
+
* WHAT THIS DELIBERATELY DOES NOT DO, and why:
|
|
24
|
+
*
|
|
25
|
+
* - It does not decide what counts as a "user-facing string" at all —
|
|
26
|
+
* that classification (and every excluded category: import
|
|
27
|
+
* specifiers, object keys, `aria-*`/`data-*` values, class-name
|
|
28
|
+
* builder arguments, developer diagnostics, decorative glyphs, enum
|
|
29
|
+
* tokens, ...) already happened in `scan.ts`, which is the file with
|
|
30
|
+
* the actual judgment calls and the comment explaining each one. This
|
|
31
|
+
* file only ever sees candidates `scan.ts` already decided are
|
|
32
|
+
* in-scope; it has no opinion of its own about scope.
|
|
33
|
+
* - It does not scan raw source text for `copy:<id>` citations or
|
|
34
|
+
* `copy-gate:ignore` markers itself — `scan.ts` already did that (see
|
|
35
|
+
* its `scanLineMarkers`) while it had the file's content in hand, and
|
|
36
|
+
* attached the result to each `CopyCandidate`/the scan's flat
|
|
37
|
+
* `Citation[]`. This file only ever looks those up against a
|
|
38
|
+
* `CopyRecord`'s real ids.
|
|
39
|
+
* - It does not validate the `CopyRecord` itself (a placeholder declared
|
|
40
|
+
* but missing from its own entry's `text`, a malformed id, a
|
|
41
|
+
* duplicate id) — that is `schema.ts`'s job, upstream of this
|
|
42
|
+
* function ever being called. A `CopyRecord` that reaches this
|
|
43
|
+
* function is assumed already well-formed; `cli.ts` is responsible for
|
|
44
|
+
* refusing to call this function at all on one that isn't (see its own
|
|
45
|
+
* exit-code discipline).
|
|
46
|
+
* - It does not resolve a `CopyEntry.factRef` against a real facts
|
|
47
|
+
* registry — the exact same deliberately-left-open seam
|
|
48
|
+
* `@clossys/writer/voice`'s `Claim.factRef` and this package's own
|
|
49
|
+
* `CopyEntry.factRef` both describe in their doc comments. A gate with
|
|
50
|
+
* visibility into both a `CopyRecord` and a real facts registry is a
|
|
51
|
+
* different, later gate.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import { PLACEHOLDER_SENTINEL, type Citation, type CopyCandidate, type UncheckedItem } from "./scan.js";
|
|
55
|
+
import type { CopyRecord } from "./types.js";
|
|
56
|
+
|
|
57
|
+
export type CopyGateRule = "unregistered-copy" | "unknown-copy-citation";
|
|
58
|
+
|
|
59
|
+
export interface CopyGateFinding {
|
|
60
|
+
rule: CopyGateRule;
|
|
61
|
+
/** Always `"error"` — the same single-severity design `@example/strategy`'s `FactsGateFinding` uses: a piece of copy is either traced to a registered entry or it isn't. */
|
|
62
|
+
severity: "error";
|
|
63
|
+
file: string;
|
|
64
|
+
line: number;
|
|
65
|
+
message: string;
|
|
66
|
+
/** The literal's raw source text (quotes included), trimmed/length-capped, for a human scanning a report. */
|
|
67
|
+
snippet: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface CopyGateIgnored {
|
|
71
|
+
file: string;
|
|
72
|
+
line: number;
|
|
73
|
+
snippet: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CopyGateResult {
|
|
77
|
+
findings: CopyGateFinding[];
|
|
78
|
+
/** Every candidate explicitly suppressed via a `copy-gate:ignore` marker on its own source line — recorded, never silent. */
|
|
79
|
+
ignored: CopyGateIgnored[];
|
|
80
|
+
/** Echoed straight from `ScanResult.filesScanned` (see `scan.ts`) so a caller/report has one number to check for "at least one file scanned" without re-deriving it from `candidates`, which could legitimately be empty even for a real, successful scan. */
|
|
81
|
+
filesScanned: number;
|
|
82
|
+
/** Total candidates evaluated — a sanity total, the same role `FactsGateResult.claimsScanned` plays: lets a caller tell "zero findings because nothing looked like copy" apart from "zero findings because every candidate traced cleanly". */
|
|
83
|
+
candidatesScanned: number;
|
|
84
|
+
/** Candidates that matched a registered entry's text shape or a valid `copy:<id>` citation. */
|
|
85
|
+
matched: number;
|
|
86
|
+
/**
|
|
87
|
+
* Echoed straight from `ScanResult.unchecked` (see `scan.ts`) —
|
|
88
|
+
* constructs the scanner recognized as JSX-shaped but could not
|
|
89
|
+
* classify. This is passed through UNCHANGED, never filtered or
|
|
90
|
+
* summarized away: a caller that reads `findings` and ignores this
|
|
91
|
+
* field can end up exactly where issue #37 started — reporting a clean
|
|
92
|
+
* result over a file part of which was never actually examined.
|
|
93
|
+
* `cli.ts` is the one that turns a non-empty list here into something
|
|
94
|
+
* an operator cannot miss; see its own doc comment for exactly where
|
|
95
|
+
* that lands in the exit-code contract.
|
|
96
|
+
*/
|
|
97
|
+
unchecked: UncheckedItem[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The same `{name}` brace-delimited placeholder convention `schema.ts` checks `CopyEntry.text` against — collapsed here to the identical sentinel `scan.ts` uses for a source literal's `${...}` interpolations, so the two sides compare as the same shape regardless of what name either side chose. */
|
|
101
|
+
const ENTRY_PLACEHOLDER_RE = /\{[^{}]+\}/g;
|
|
102
|
+
|
|
103
|
+
function normalizeEntryText(text: string): string {
|
|
104
|
+
return text.replace(ENTRY_PLACEHOLDER_RE, PLACEHOLDER_SENTINEL);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function snippetOf(text: string, max = 120): string {
|
|
108
|
+
const trimmed = text.trim();
|
|
109
|
+
return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface CopyIndex {
|
|
113
|
+
byId: Set<string>;
|
|
114
|
+
byNormalizedText: Set<string>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildCopyIndex(record: CopyRecord): CopyIndex {
|
|
118
|
+
const byId = new Set<string>();
|
|
119
|
+
const byNormalizedText = new Set<string>();
|
|
120
|
+
for (const entry of record.entries) {
|
|
121
|
+
byId.add(entry.id);
|
|
122
|
+
byNormalizedText.add(normalizeEntryText(entry.text));
|
|
123
|
+
}
|
|
124
|
+
return { byId, byNormalizedText };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Evaluates every `candidate` against `record`, using `citations` for the
|
|
129
|
+
* "cites an id that does not exist" check. Never throws: an `entries`
|
|
130
|
+
* array that is empty is valid input (every candidate will simply be
|
|
131
|
+
* untraced), and a `CopyCandidate[]`/`Citation[]` produced from any file
|
|
132
|
+
* content — however unusual — is evaluated with no assumption about its
|
|
133
|
+
* shape beyond what `scan.ts`'s own types already guarantee. `unchecked`
|
|
134
|
+
* is required, not optional, and passed straight through onto
|
|
135
|
+
* `CopyGateResult.unchecked` unmodified — a caller cannot construct a
|
|
136
|
+
* `CopyGateResult` from this function while accidentally forgetting that
|
|
137
|
+
* field exists.
|
|
138
|
+
*/
|
|
139
|
+
export function checkCopyTraceability(
|
|
140
|
+
candidates: CopyCandidate[],
|
|
141
|
+
citations: Citation[],
|
|
142
|
+
record: CopyRecord,
|
|
143
|
+
filesScanned: number,
|
|
144
|
+
unchecked: UncheckedItem[],
|
|
145
|
+
): CopyGateResult {
|
|
146
|
+
const index = buildCopyIndex(record);
|
|
147
|
+
const findings: CopyGateFinding[] = [];
|
|
148
|
+
const ignored: CopyGateIgnored[] = [];
|
|
149
|
+
let matched = 0;
|
|
150
|
+
|
|
151
|
+
// Pass 1: citations to a copy id that does not exist — checked over
|
|
152
|
+
// EVERY citation found anywhere in the scanned tree, independent of
|
|
153
|
+
// whether a candidate happens to share its line, mirroring
|
|
154
|
+
// `@example/strategy`'s `facts-gate.ts` Pass 1 (see `scan.ts`'s
|
|
155
|
+
// `Citation` doc comment for why).
|
|
156
|
+
for (const citation of citations) {
|
|
157
|
+
if (!index.byId.has(citation.id)) {
|
|
158
|
+
findings.push({
|
|
159
|
+
rule: "unknown-copy-citation",
|
|
160
|
+
severity: "error",
|
|
161
|
+
file: citation.file,
|
|
162
|
+
line: citation.line,
|
|
163
|
+
message: `cites copy id "${citation.id}", which does not exist in the copy record — a citation to a rotted or misspelled id silently defeats this gate`,
|
|
164
|
+
snippet: `copy:${citation.id}`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Pass 2: every candidate, traced or not.
|
|
170
|
+
for (const candidate of candidates) {
|
|
171
|
+
if (candidate.hasIgnoreMarker) {
|
|
172
|
+
ignored.push({ file: candidate.file, line: candidate.line, snippet: candidate.raw });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const hasValidCitation = candidate.citedIds.some((id) => index.byId.has(id));
|
|
176
|
+
if (hasValidCitation || index.byNormalizedText.has(candidate.normalized)) {
|
|
177
|
+
matched++;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
findings.push({
|
|
182
|
+
rule: "unregistered-copy",
|
|
183
|
+
severity: "error",
|
|
184
|
+
file: candidate.file,
|
|
185
|
+
line: candidate.line,
|
|
186
|
+
message:
|
|
187
|
+
candidate.kind === "template"
|
|
188
|
+
? `template literal ${candidate.raw} does not match any registered copy entry's text (shape-compared, interpolations ignored), and carries no valid "copy:<id>" citation`
|
|
189
|
+
: candidate.kind === "jsx-text"
|
|
190
|
+
? `JSX text "${candidate.raw}" does not match any registered copy entry's text, and carries no valid "copy:<id>" citation`
|
|
191
|
+
: `string ${candidate.raw} does not match any registered copy entry's text, and carries no valid "copy:<id>" citation`,
|
|
192
|
+
snippet: snippetOf(candidate.raw),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { findings, ignored, filesScanned, candidatesScanned: candidates.length, matched, unchecked };
|
|
197
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `computeCopyFingerprint` — a deterministic, content-derived digest of a
|
|
3
|
+
* copy entry's `text`, and nothing else (no locale, no timestamp, no id
|
|
4
|
+
* folded in). This is the one piece of machinery that makes real
|
|
5
|
+
* stale-translation detection possible: `locale-coverage.ts`'s
|
|
6
|
+
* `checkLocaleCoverage` compares a target entry's recorded
|
|
7
|
+
* `translation.sourceFingerprint` (`types.ts`'s `CopyTranslationProvenance`)
|
|
8
|
+
* against a fresh `computeCopyFingerprint(sourceEntry.text)` to decide
|
|
9
|
+
* whether a translation is still current.
|
|
10
|
+
*
|
|
11
|
+
* WHY CONTENT-DERIVED, NEVER A HUMAN-MAINTAINED REVISION NUMBER
|
|
12
|
+
* ---------------------------------------------------------------------------
|
|
13
|
+
* A translation is stale exactly when the source text it was translated
|
|
14
|
+
* from no longer matches the current source text — that is the whole
|
|
15
|
+
* definition. A hand-bumped revision counter answers a related but weaker
|
|
16
|
+
* question ("did someone remember to bump the number"), and nothing in this
|
|
17
|
+
* package's schema, or any schema, can enforce that a human touches the
|
|
18
|
+
* counter in lockstep with every edit to `text`. A bumped counter next to
|
|
19
|
+
* an untouched sentence looks identical, at the type level, to a genuinely
|
|
20
|
+
* re-reviewed one — the discipline can drift silently, the same
|
|
21
|
+
* "check that passes because it checked nothing" failure mode
|
|
22
|
+
* `locale-coverage.ts`'s own top-of-file doc comment (and
|
|
23
|
+
* `scripts/check-release-readiness.mjs`'s header, which it cites) already
|
|
24
|
+
* warns about elsewhere in this package.
|
|
25
|
+
*
|
|
26
|
+
* A content hash cannot drift the same way. If the source `text` changed by
|
|
27
|
+
* one character, `computeCopyFingerprint` returns a different value with
|
|
28
|
+
* certainty; if it did not change, it returns the same value with equal
|
|
29
|
+
* certainty. It requires no human bookkeeping to stay correct — the
|
|
30
|
+
* fingerprint IS the source text, deterministically restated, rather than
|
|
31
|
+
* someone's claim about the source text.
|
|
32
|
+
*
|
|
33
|
+
* Uses `node:crypto`, a Node.js built-in — not a runtime dependency. This
|
|
34
|
+
* follows the exact precedent `registry.ts` already set for `node:fs`: see
|
|
35
|
+
* this package's README, "The single most important constraint", and
|
|
36
|
+
* `types.ts`'s own doc comment on this package's zero-runtime-dependency
|
|
37
|
+
* discipline.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { createHash } from "node:crypto";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The digest algorithm `computeCopyFingerprint` uses, and the value every
|
|
44
|
+
* `CopyTranslationProvenance.fingerprintAlgorithm` produced by this function
|
|
45
|
+
* should record. Exported as a named constant, not hardcoded at each call
|
|
46
|
+
* site, so a future algorithm change is one edit instead of several
|
|
47
|
+
* independently-drifting string literals.
|
|
48
|
+
*/
|
|
49
|
+
export const COPY_FINGERPRINT_ALGORITHM = "sha256";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Computes a deterministic, content-derived fingerprint of `text` — a
|
|
53
|
+
* `sha256` hex digest, nothing else folded in (no locale, no id, no
|
|
54
|
+
* timestamp). Pure: the same `text` always produces the same fingerprint,
|
|
55
|
+
* on any machine, in any process.
|
|
56
|
+
*
|
|
57
|
+
* A consumer's own translation tooling should call this at the moment a
|
|
58
|
+
* translation is actually produced, recording the result as
|
|
59
|
+
* `translation.sourceFingerprint` alongside `COPY_FINGERPRINT_ALGORITHM` as
|
|
60
|
+
* `translation.fingerprintAlgorithm` — the same function
|
|
61
|
+
* `checkLocaleCoverage` will later call again, on the then-current source
|
|
62
|
+
* `text`, to decide whether that recorded fingerprint still matches.
|
|
63
|
+
*/
|
|
64
|
+
export function computeCopyFingerprint(text: string): string {
|
|
65
|
+
return createHash(COPY_FINGERPRINT_ALGORITHM).update(text, "utf8").digest("hex");
|
|
66
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clossys/writer — public entry point. See README.md for the full
|
|
3
|
+
* picture: the machinery-vs-values split this package mirrors from
|
|
4
|
+
* the package's voice contract (itself mirroring @example/ui/tokens), the
|
|
5
|
+
* frozen CopyEntry/CopyRecord contract, and exactly what checkCopyRecord
|
|
6
|
+
* does and does not attempt.
|
|
7
|
+
*
|
|
8
|
+
* Two halves ship from here, mirroring the split @example/strategy
|
|
9
|
+
* draws between its own entity/reader half and its facts gate:
|
|
10
|
+
*
|
|
11
|
+
* 1. THE REGISTRY. CopyEntry/CopyRecord (`types.ts`), their validation
|
|
12
|
+
* (`schema.ts`), and `readCopyRecord` (`registry.ts`) — a consumer
|
|
13
|
+
* registers its own copy and this package can load and validate it.
|
|
14
|
+
* 2. THE SCANNER GATE. `scanCopySourceTree` (`scan.ts`) walks a real
|
|
15
|
+
* source tree and extracts every user-facing string/template literal
|
|
16
|
+
* as a `CopyCandidate`; `checkCopyTraceability` (`copy-gate.ts`) is
|
|
17
|
+
* the pure gate that checks each one against a `CopyRecord`. `cli.ts`
|
|
18
|
+
* wires both, plus `readCopyRecord`, into `writer-check`, an installable
|
|
19
|
+
* CLI with the same three-state exit-code contract
|
|
20
|
+
* `strategy-facts-check` uses: 0 clean, 1 findings, 2 could not run.
|
|
21
|
+
* `cli.ts` itself is intentionally NOT re-exported here — matching
|
|
22
|
+
* `@example/strategy`'s own index.ts, a CLI's `main`/argv
|
|
23
|
+
* handling is not library surface, only its `bin` entry is.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export type {
|
|
27
|
+
CopyEntry,
|
|
28
|
+
CopyEntryId,
|
|
29
|
+
CopyEntryStatus,
|
|
30
|
+
CopyFinding,
|
|
31
|
+
CopyLocale,
|
|
32
|
+
CopyRef,
|
|
33
|
+
CopyRegistry,
|
|
34
|
+
CopyRegistryEntry,
|
|
35
|
+
CopyResolution,
|
|
36
|
+
CopyResolver,
|
|
37
|
+
CopySource,
|
|
38
|
+
CopyTranslationProvenance,
|
|
39
|
+
CopyValue,
|
|
40
|
+
CopyRecord,
|
|
41
|
+
} from "./types.js";
|
|
42
|
+
|
|
43
|
+
// Content-derived fingerprinting for translation provenance — see
|
|
44
|
+
// fingerprint.ts's and locale-coverage.ts's doc comments for why a source
|
|
45
|
+
// text is fingerprinted rather than tracked by a human-maintained revision.
|
|
46
|
+
export { computeCopyFingerprint, COPY_FINGERPRINT_ALGORITHM } from "./fingerprint.js";
|
|
47
|
+
|
|
48
|
+
export { parseCopyRecord, parseCopyRegistry, validateCopyRecordShape, validateCopyRegistryShape } from "./schema.js";
|
|
49
|
+
|
|
50
|
+
export { createCopyResolver, resolveCopyRef } from "./resolve.js";
|
|
51
|
+
export type { CopyResolveIssue, CopyResolveIssueReason, CopyResolveResult } from "./resolve.js";
|
|
52
|
+
|
|
53
|
+
export { readCopyRecord } from "./registry.js";
|
|
54
|
+
export type { CopyRegistryReadIssue, CopyRegistryReadIssueReason, CopyRegistryReadResult } from "./registry.js";
|
|
55
|
+
|
|
56
|
+
export { checkCopyRecord } from "./checker.js";
|
|
57
|
+
export type {
|
|
58
|
+
CopyEntryCheckResult,
|
|
59
|
+
CopyEntrySkip,
|
|
60
|
+
CopyRecordCheckOptions,
|
|
61
|
+
CopyRecordCheckReport,
|
|
62
|
+
CopyRecordFinding,
|
|
63
|
+
CopyRecordWaivedFinding,
|
|
64
|
+
} from "./checker.js";
|
|
65
|
+
|
|
66
|
+
// Voice is a first-class part of copy. It is available from this root entry
|
|
67
|
+
// point for a single-package experience and from the focused `copy/voice`
|
|
68
|
+
// subpath for consumers that only need the verbal contract.
|
|
69
|
+
export * from "./voice/index.js";
|
|
70
|
+
|
|
71
|
+
export { extractCopyCandidates, PLACEHOLDER_SENTINEL, scanCopySourceTree } from "./scan.js";
|
|
72
|
+
export type {
|
|
73
|
+
Citation,
|
|
74
|
+
CopyCandidate,
|
|
75
|
+
ExcludedLiteral,
|
|
76
|
+
ExclusionReason,
|
|
77
|
+
ParseFailure,
|
|
78
|
+
ScanOptions,
|
|
79
|
+
ScanResult,
|
|
80
|
+
SkippedFile,
|
|
81
|
+
UncheckedItem,
|
|
82
|
+
} from "./scan.js";
|
|
83
|
+
|
|
84
|
+
// Path exclusions for scanCopySourceTree's scanning surface — see
|
|
85
|
+
// path-exclusions.ts's top doc comment for the "mention vs. use" case this
|
|
86
|
+
// exists for, and why it is a DIFFERENT feature from scan.ts's own
|
|
87
|
+
// ExclusionReason (a per-literal classification, not a per-file scope
|
|
88
|
+
// decision).
|
|
89
|
+
export { validatePathExclusions } from "./path-exclusions.js";
|
|
90
|
+
export type {
|
|
91
|
+
ExcludedPath,
|
|
92
|
+
PathExclusion,
|
|
93
|
+
PathExclusionFinding,
|
|
94
|
+
PathExclusionFindingRule,
|
|
95
|
+
PathExclusionValidation,
|
|
96
|
+
} from "./path-exclusions.js";
|
|
97
|
+
|
|
98
|
+
export { checkCopyTraceability } from "./copy-gate.js";
|
|
99
|
+
export type { CopyGateFinding, CopyGateIgnored, CopyGateResult, CopyGateRule } from "./copy-gate.js";
|
|
100
|
+
|
|
101
|
+
// Copy ADDRESSABILITY: is user-facing prose resolved from the copy
|
|
102
|
+
// registry by id, or typed inline in a component? A different, stricter
|
|
103
|
+
// question than traceability above — see addressability.ts's top doc
|
|
104
|
+
// comment for the full split.
|
|
105
|
+
export {
|
|
106
|
+
checkAddressability,
|
|
107
|
+
extractAddressabilityCandidates,
|
|
108
|
+
scanAddressabilitySources,
|
|
109
|
+
} from "./addressability.js";
|
|
110
|
+
export type {
|
|
111
|
+
AddressabilityGateResult,
|
|
112
|
+
AddressabilityPosition,
|
|
113
|
+
AddressabilityScanOptions,
|
|
114
|
+
AddressabilityScanResult,
|
|
115
|
+
AddressabilityUncheckedItem,
|
|
116
|
+
AddressabilityVerdict,
|
|
117
|
+
AddressabilityViolation,
|
|
118
|
+
} from "./addressability.js";
|
|
119
|
+
|
|
120
|
+
// Locale-coverage governance: is every declared locale covered by a source
|
|
121
|
+
// locale's registry, where has a target locale drifted, has a translation
|
|
122
|
+
// gone stale against an edited source (via content fingerprint), and does
|
|
123
|
+
// every entry's interpolation stay in parity across locales? See
|
|
124
|
+
// locale-coverage.ts's top-of-file doc comment for the full design.
|
|
125
|
+
export { checkLocaleCoverage } from "./locale-coverage.js";
|
|
126
|
+
export type {
|
|
127
|
+
LocaleCoverageFinding,
|
|
128
|
+
LocaleCoverageReport,
|
|
129
|
+
LocaleCoverageSkip,
|
|
130
|
+
LocaleCoverageSkipReason,
|
|
131
|
+
} from "./locale-coverage.js";
|
|
132
|
+
|
|
133
|
+
// The PASSAGE LAYER (issue #373) — the missing middle between a single
|
|
134
|
+
// CopyEntry and a whole document: a Passage composes entry/term
|
|
135
|
+
// REFERENCES the way a UI block composes atoms. See passage.ts's top doc
|
|
136
|
+
// comment for the full design, the ternary, and what this gate
|
|
137
|
+
// deliberately does not attempt (referential integrity against a real
|
|
138
|
+
// CopyRecord/glossary — a different, later gate).
|
|
139
|
+
export {
|
|
140
|
+
checkPassageComposition,
|
|
141
|
+
classifyPassageField,
|
|
142
|
+
parsePassageRecord,
|
|
143
|
+
readPassageRecord,
|
|
144
|
+
validatePassageRecordShape,
|
|
145
|
+
} from "./passage.js";
|
|
146
|
+
export type {
|
|
147
|
+
Passage,
|
|
148
|
+
PassageEntryReference,
|
|
149
|
+
PassageFieldClassification,
|
|
150
|
+
PassageFinding,
|
|
151
|
+
PassageGateResult,
|
|
152
|
+
PassageId,
|
|
153
|
+
PassageRecord,
|
|
154
|
+
PassageReference,
|
|
155
|
+
PassageRegistryReadIssue,
|
|
156
|
+
PassageRegistryReadIssueReason,
|
|
157
|
+
PassageRegistryReadResult,
|
|
158
|
+
PassageTermReference,
|
|
159
|
+
PassageUnclassifiedItem,
|
|
160
|
+
PassageVerdict,
|
|
161
|
+
PassageViolation,
|
|
162
|
+
PassageViolationRule,
|
|
163
|
+
} from "./passage.js";
|