@clossys/writer 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/LICENSE +21 -0
  3. package/README.md +593 -0
  4. package/dist/addressability.d.ts +245 -0
  5. package/dist/addressability.d.ts.map +1 -0
  6. package/dist/addressability.js +567 -0
  7. package/dist/addressability.js.map +1 -0
  8. package/dist/checker.d.ts +159 -0
  9. package/dist/checker.d.ts.map +1 -0
  10. package/dist/checker.js +177 -0
  11. package/dist/checker.js.map +1 -0
  12. package/dist/cli.d.ts +149 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +1049 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/copy-gate.d.ts +107 -0
  17. package/dist/copy-gate.d.ts.map +1 -0
  18. package/dist/copy-gate.js +132 -0
  19. package/dist/copy-gate.js.map +1 -0
  20. package/dist/fingerprint.d.ts +61 -0
  21. package/dist/fingerprint.d.ts.map +1 -0
  22. package/dist/fingerprint.js +64 -0
  23. package/dist/fingerprint.js.map +1 -0
  24. package/dist/index.d.ts +47 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +63 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/locale-coverage.d.ts +227 -0
  29. package/dist/locale-coverage.d.ts.map +1 -0
  30. package/dist/locale-coverage.js +414 -0
  31. package/dist/locale-coverage.js.map +1 -0
  32. package/dist/passage.d.ts +294 -0
  33. package/dist/passage.d.ts.map +1 -0
  34. package/dist/passage.js +463 -0
  35. package/dist/passage.js.map +1 -0
  36. package/dist/path-exclusions.d.ts +114 -0
  37. package/dist/path-exclusions.d.ts.map +1 -0
  38. package/dist/path-exclusions.js +213 -0
  39. package/dist/path-exclusions.js.map +1 -0
  40. package/dist/registry.d.ts +61 -0
  41. package/dist/registry.d.ts.map +1 -0
  42. package/dist/registry.js +64 -0
  43. package/dist/registry.js.map +1 -0
  44. package/dist/resolve.d.ts +33 -0
  45. package/dist/resolve.d.ts.map +1 -0
  46. package/dist/resolve.js +133 -0
  47. package/dist/resolve.js.map +1 -0
  48. package/dist/scan.d.ts +457 -0
  49. package/dist/scan.d.ts.map +1 -0
  50. package/dist/scan.js +1698 -0
  51. package/dist/scan.js.map +1 -0
  52. package/dist/schema.d.ts +81 -0
  53. package/dist/schema.d.ts.map +1 -0
  54. package/dist/schema.js +482 -0
  55. package/dist/schema.js.map +1 -0
  56. package/dist/types.d.ts +268 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/dist/types.js +40 -0
  59. package/dist/types.js.map +1 -0
  60. package/dist/voice/checker.d.ts +186 -0
  61. package/dist/voice/checker.d.ts.map +1 -0
  62. package/dist/voice/checker.js +483 -0
  63. package/dist/voice/checker.js.map +1 -0
  64. package/dist/voice/derivation-coverage.d.ts +168 -0
  65. package/dist/voice/derivation-coverage.d.ts.map +1 -0
  66. package/dist/voice/derivation-coverage.js +181 -0
  67. package/dist/voice/derivation-coverage.js.map +1 -0
  68. package/dist/voice/fields.d.ts +107 -0
  69. package/dist/voice/fields.d.ts.map +1 -0
  70. package/dist/voice/fields.js +144 -0
  71. package/dist/voice/fields.js.map +1 -0
  72. package/dist/voice/index.d.ts +19 -0
  73. package/dist/voice/index.d.ts.map +1 -0
  74. package/dist/voice/index.js +24 -0
  75. package/dist/voice/index.js.map +1 -0
  76. package/dist/voice/internal/parse-template.d.ts +27 -0
  77. package/dist/voice/internal/parse-template.d.ts.map +1 -0
  78. package/dist/voice/internal/parse-template.js +111 -0
  79. package/dist/voice/internal/parse-template.js.map +1 -0
  80. package/dist/voice/internal/pattern-safety.d.ts +111 -0
  81. package/dist/voice/internal/pattern-safety.d.ts.map +1 -0
  82. package/dist/voice/internal/pattern-safety.js +278 -0
  83. package/dist/voice/internal/pattern-safety.js.map +1 -0
  84. package/dist/voice/schema.d.ts +65 -0
  85. package/dist/voice/schema.d.ts.map +1 -0
  86. package/dist/voice/schema.js +535 -0
  87. package/dist/voice/schema.js.map +1 -0
  88. package/dist/voice/types.d.ts +282 -0
  89. package/dist/voice/types.d.ts.map +1 -0
  90. package/dist/voice/types.js +37 -0
  91. package/dist/voice/types.js.map +1 -0
  92. package/package.json +72 -0
  93. package/src/addressability.ts +664 -0
  94. package/src/checker.ts +293 -0
  95. package/src/cli.ts +1166 -0
  96. package/src/copy-gate.ts +197 -0
  97. package/src/fingerprint.ts +66 -0
  98. package/src/index.ts +163 -0
  99. package/src/locale-coverage.ts +512 -0
  100. package/src/passage.ts +644 -0
  101. package/src/path-exclusions.ts +265 -0
  102. package/src/registry.ts +103 -0
  103. package/src/resolve.ts +172 -0
  104. package/src/scan.ts +1959 -0
  105. package/src/schema.ts +528 -0
  106. package/src/types.ts +301 -0
  107. package/src/voice/checker.ts +590 -0
  108. package/src/voice/derivation-coverage.ts +217 -0
  109. package/src/voice/fields.ts +160 -0
  110. package/src/voice/index.ts +55 -0
  111. package/src/voice/internal/parse-template.ts +114 -0
  112. package/src/voice/internal/pattern-safety.ts +336 -0
  113. package/src/voice/schema.ts +585 -0
  114. package/src/voice/types.ts +329 -0
  115. package/templates/voice-record.template.jsonc +135 -0
@@ -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";