@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,159 @@
1
+ /**
2
+ * `checkCopyRecord` — runs every entry in a `CopyRecord` through
3
+ * `@clossys/writer/voice`'s own `checkCopy`, against a single
4
+ * `VoiceRecord`, and reports the result. This is the one file in this
5
+ * package that imports `@clossys/writer/voice`: `types.ts` and
6
+ * `schema.ts` are pure, dependency-free data/validation, exactly like
7
+ * `voice`'s own `types.ts`/`schema.ts`; this file is where the two
8
+ * packages' machinery actually meet.
9
+ *
10
+ * FIVE AGENTS FINISHED A REPAIR ROUND ON THIS REPOSITORY AN HOUR BEFORE
11
+ * THIS FILE WAS WRITTEN, AND EVERY ONE OF THEM SHIPPED A FIRST DRAFT THAT
12
+ * REPRODUCED THE EXACT DEFECT IT WAS FIXING. The recurring shape was "a
13
+ * check that passes while asserting nothing" — `checkCopyRecord` IS a
14
+ * check, so this file is written against that failure mode directly, not
15
+ * as an afterthought:
16
+ *
17
+ * 1. NO SILENT NARROWING. If an entry is not run through `checkCopy`
18
+ * for any reason, it is recorded in `report.skipped`, by id — never
19
+ * folded silently into a "0 findings" result that looks identical
20
+ * to "everything ran and was clean". `report.checkedCount` and
21
+ * `report.skippedCount` are always populated (not just inferable
22
+ * from array lengths), so a caller reading only those two numbers,
23
+ * without inspecting either array, still cannot mistake one for the
24
+ * other.
25
+ * 2. FAILS CLOSED. An invalid `CopyRecord`, an invalid `VoiceRecord`,
26
+ * or a `CopyRecord` with zero entries are each treated as a
27
+ * FAILURE — `report.complete` is `false` and `report.findings`
28
+ * names the reason — never as a clean pass with nothing to check.
29
+ * `checkCopyRecord` re-validates BOTH its arguments itself
30
+ * (`validateCopyRecordShape` and, imported from `voice`,
31
+ * `validateVoiceRecordShape`), even though both are typed inputs a
32
+ * TypeScript caller "should" have already validated — because a
33
+ * plain-JS caller, or a value that merely satisfies the type at
34
+ * compile time without actually conforming at runtime, is exactly
35
+ * the case a check that "passes while asserting nothing" would fail
36
+ * to catch. This is a deliberate difference from `voice`'s own
37
+ * `checkCopy`, which does NOT self-validate its `VoiceRecord`
38
+ * argument (see that function's own doc comment) — this package
39
+ * chooses the stricter behavior for its own entry point because it
40
+ * is the thing this whole package's design brief was written to
41
+ * distrust in itself.
42
+ * 3. `report.complete` MEANS "did this run check everything it could
43
+ * have", never "did everything pass" — the same non-negotiable
44
+ * distinction `voice`'s own `VoiceCheckReport.complete` draws. A
45
+ * `CopyRecord` with real voice violations in it can still be
46
+ * `complete: true` (every entry got checked; some of them failed);
47
+ * a `CopyRecord` this function refused to check at all is always
48
+ * `complete: false`, no matter how "clean" an empty `findings`
49
+ * array might otherwise look.
50
+ *
51
+ * Pure, no I/O: `checkCopyRecord` never reads a file — that is
52
+ * `registry.ts`'s job — and never resolves a `factRef` against anything,
53
+ * for the same reason `voice`'s `checkCopy` does not: see
54
+ * `@clossys/writer/voice`'s README, "The `factRef` seam".
55
+ */
56
+ import { type VoiceCheckReport, type VoiceCheckWaiver, type VoiceFinding, type VoiceRecord } from "./voice/index.js";
57
+ import type { CopyEntryId, CopyRecord } from "./types.js";
58
+ /** One entry `checkCopyRecord` did NOT run through `voice`'s `checkCopy` this run, and why. */
59
+ export interface CopyEntrySkip {
60
+ entryId: CopyEntryId;
61
+ /**
62
+ * Human-readable reason. In practice always because the `CopyRecord` or
63
+ * `VoiceRecord` given to this call failed its own shape validation —
64
+ * see `report.findings` for the specific violation(s). An entry that
65
+ * passes `validateCopyRecordShape` (id well-formed and unique, `text`
66
+ * and `context` non-empty, every declared placeholder present) is
67
+ * always run, never skipped for a reason internal to the entry itself;
68
+ * `voice`'s own `checkCopy` has its own, separately-reported notion of
69
+ * an incomplete run (see `CopyEntryCheckResult.report.skipped`) for a
70
+ * dimension it could not evaluate against a structurally valid entry.
71
+ */
72
+ reason: string;
73
+ }
74
+ /** One entry `checkCopyRecord` DID run through `voice`'s `checkCopy`, and the resulting report. */
75
+ export interface CopyEntryCheckResult {
76
+ entryId: CopyEntryId;
77
+ /**
78
+ * `voice`'s own `checkCopy` report for this entry's `text`, unmodified.
79
+ * Its own `.complete`/`.skipped` describe whether `checkCopy` itself
80
+ * could evaluate every dimension against this ONE entry — a separate
81
+ * question from whether `checkCopyRecord` ran this entry at all.
82
+ */
83
+ report: VoiceCheckReport;
84
+ }
85
+ /** One `VoiceFinding` from one entry's `checkCopy` run, flattened into `report.findings` and tagged with the entry it came from. */
86
+ export interface CopyRecordFinding extends VoiceFinding {
87
+ /** The entry this finding is about, or `undefined` for a record-level finding not specific to any one entry (an invalid `CopyRecord`/`VoiceRecord`, or zero entries). */
88
+ entryId?: CopyEntryId;
89
+ }
90
+ /** A `CopyRecordFinding` that was waived, with the waiver that covered it — mirrors `voice`'s `WaivedVoiceFinding`. */
91
+ export interface CopyRecordWaivedFinding extends CopyRecordFinding {
92
+ waiver: VoiceCheckWaiver;
93
+ }
94
+ export interface CopyRecordCheckOptions {
95
+ /**
96
+ * Waivers applied uniformly to EVERY entry's `checkCopy` call — the
97
+ * same `VoiceCheckWaiver[]` shape `voice`'s own `checkCopy` accepts.
98
+ * There is no per-entry waiver targeting in this version: a waiver
99
+ * scoped to one entry's finding still matches the same `rule`+`path`
100
+ * pair in every other entry's report too. A caller that genuinely needs
101
+ * per-entry waiver scoping can call `voice`'s `checkCopy` directly,
102
+ * per entry, with its own waiver list — this package's own `checker.ts`
103
+ * does exactly that under the hood.
104
+ */
105
+ waivers?: VoiceCheckWaiver[];
106
+ }
107
+ export interface CopyRecordCheckReport {
108
+ recordId: string;
109
+ /** Every entry actually run through `checkCopy`, with its own `VoiceCheckReport`. */
110
+ checked: CopyEntryCheckResult[];
111
+ /** Every entry NOT run, by id, and why. Empty does not by itself mean "nothing was skipped for a bad reason" — see `report.complete`, which is `false` whenever this run could not be trusted enough to check every entry, including the cases where this is legitimately `[]` (zero entries; see `checkedCount`). */
112
+ skipped: CopyEntrySkip[];
113
+ /** `checked.length`, restated as its own field so a caller does not have to inspect the array to get this number — see this file's top-of-file "no silent narrowing" note. */
114
+ checkedCount: number;
115
+ /** `skipped.length`, restated as its own field for the same reason as `checkedCount`. */
116
+ skippedCount: number;
117
+ /**
118
+ * Every non-waived finding this run produced, flattened across every
119
+ * checked entry AND any record-level problem (an invalid `CopyRecord`,
120
+ * an invalid `VoiceRecord`, or zero entries), each entry-sourced
121
+ * finding tagged with the `entryId` it came from. This is the one field
122
+ * a caller checking "is anything wrong" should read — it is populated
123
+ * in every failure case this function knows about, never left empty to
124
+ * imply a clean run when the run in fact could not be trusted (see
125
+ * `.complete`).
126
+ */
127
+ findings: CopyRecordFinding[];
128
+ /** Every finding covered by a waiver, across every checked entry, each with the waiver that covered it — mirrors `voice`'s `VoiceCheckReport.waived`. */
129
+ waived: CopyRecordWaivedFinding[];
130
+ /**
131
+ * `true` exactly when: the given `CopyRecord` was structurally valid,
132
+ * the given `VoiceRecord` was structurally valid, the record had at
133
+ * least one entry, and every entry was actually run (`skipped` is
134
+ * empty). `false` in every other case. Independent of `findings` —
135
+ * see this file's top-of-file note #3. Read this field, not an empty
136
+ * `findings` array, to decide whether a run can be trusted as "checked
137
+ * everything, found nothing".
138
+ */
139
+ complete: boolean;
140
+ }
141
+ /**
142
+ * Checks every entry in `record` against `voiceRecord`, via `voice`'s own
143
+ * `checkCopy`. See this file's top-of-file doc comment for the fail-
144
+ * closed behavior on an invalid `record`, an invalid `voiceRecord`, and a
145
+ * `record` with zero entries.
146
+ *
147
+ * Throws a plain `Error` (not a finding) for caller-input problems that
148
+ * are not this function's job to interpret as data: `record` or
149
+ * `voiceRecord` not an object at all. This mirrors `voice`'s own
150
+ * `checkCopy`, which treats a malformed call the same way — a hard
151
+ * upfront rejection, not a soft finding that could be mistaken for
152
+ * something the checker discovered about the record itself. A `record`
153
+ * or `voiceRecord` that IS an object but fails shape validation is,
154
+ * unlike a non-object, a real finding (`report.findings`), not a thrown
155
+ * error — see note #2 above for why this function validates both itself
156
+ * rather than trusting its own type signature.
157
+ */
158
+ export declare function checkCopyRecord(record: CopyRecord, voiceRecord: VoiceRecord, options?: CopyRecordCheckOptions): CopyRecordCheckReport;
159
+ //# sourceMappingURL=checker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checker.d.ts","sourceRoot":"","sources":["../src/checker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1D,+FAA+F;AAC/F,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,WAAW,CAAC;IACrB;;;;;;;;;;OAUG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mGAAmG;AACnG,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,WAAW,CAAC;IACrB;;;;;OAKG;IACH,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED,oIAAoI;AACpI,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD,yKAAyK;IACzK,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,uHAAuH;AACvH,MAAM,WAAW,uBAAwB,SAAQ,iBAAiB;IAChE,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,sTAAsT;IACtT,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,8KAA8K;IAC9K,YAAY,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;;;OASG;IACH,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,yJAAyJ;IACzJ,MAAM,EAAE,uBAAuB,EAAE,CAAC;IAClC;;;;;;;;OAQG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAyCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,WAAW,EACxB,OAAO,GAAE,sBAA2B,GACnC,qBAAqB,CAuEvB"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * `checkCopyRecord` — runs every entry in a `CopyRecord` through
3
+ * `@clossys/writer/voice`'s own `checkCopy`, against a single
4
+ * `VoiceRecord`, and reports the result. This is the one file in this
5
+ * package that imports `@clossys/writer/voice`: `types.ts` and
6
+ * `schema.ts` are pure, dependency-free data/validation, exactly like
7
+ * `voice`'s own `types.ts`/`schema.ts`; this file is where the two
8
+ * packages' machinery actually meet.
9
+ *
10
+ * FIVE AGENTS FINISHED A REPAIR ROUND ON THIS REPOSITORY AN HOUR BEFORE
11
+ * THIS FILE WAS WRITTEN, AND EVERY ONE OF THEM SHIPPED A FIRST DRAFT THAT
12
+ * REPRODUCED THE EXACT DEFECT IT WAS FIXING. The recurring shape was "a
13
+ * check that passes while asserting nothing" — `checkCopyRecord` IS a
14
+ * check, so this file is written against that failure mode directly, not
15
+ * as an afterthought:
16
+ *
17
+ * 1. NO SILENT NARROWING. If an entry is not run through `checkCopy`
18
+ * for any reason, it is recorded in `report.skipped`, by id — never
19
+ * folded silently into a "0 findings" result that looks identical
20
+ * to "everything ran and was clean". `report.checkedCount` and
21
+ * `report.skippedCount` are always populated (not just inferable
22
+ * from array lengths), so a caller reading only those two numbers,
23
+ * without inspecting either array, still cannot mistake one for the
24
+ * other.
25
+ * 2. FAILS CLOSED. An invalid `CopyRecord`, an invalid `VoiceRecord`,
26
+ * or a `CopyRecord` with zero entries are each treated as a
27
+ * FAILURE — `report.complete` is `false` and `report.findings`
28
+ * names the reason — never as a clean pass with nothing to check.
29
+ * `checkCopyRecord` re-validates BOTH its arguments itself
30
+ * (`validateCopyRecordShape` and, imported from `voice`,
31
+ * `validateVoiceRecordShape`), even though both are typed inputs a
32
+ * TypeScript caller "should" have already validated — because a
33
+ * plain-JS caller, or a value that merely satisfies the type at
34
+ * compile time without actually conforming at runtime, is exactly
35
+ * the case a check that "passes while asserting nothing" would fail
36
+ * to catch. This is a deliberate difference from `voice`'s own
37
+ * `checkCopy`, which does NOT self-validate its `VoiceRecord`
38
+ * argument (see that function's own doc comment) — this package
39
+ * chooses the stricter behavior for its own entry point because it
40
+ * is the thing this whole package's design brief was written to
41
+ * distrust in itself.
42
+ * 3. `report.complete` MEANS "did this run check everything it could
43
+ * have", never "did everything pass" — the same non-negotiable
44
+ * distinction `voice`'s own `VoiceCheckReport.complete` draws. A
45
+ * `CopyRecord` with real voice violations in it can still be
46
+ * `complete: true` (every entry got checked; some of them failed);
47
+ * a `CopyRecord` this function refused to check at all is always
48
+ * `complete: false`, no matter how "clean" an empty `findings`
49
+ * array might otherwise look.
50
+ *
51
+ * Pure, no I/O: `checkCopyRecord` never reads a file — that is
52
+ * `registry.ts`'s job — and never resolves a `factRef` against anything,
53
+ * for the same reason `voice`'s `checkCopy` does not: see
54
+ * `@clossys/writer/voice`'s README, "The `factRef` seam".
55
+ */
56
+ import { checkCopy, validateVoiceRecordShape, } from "./voice/index.js";
57
+ import { validateCopyRecordShape } from "./schema.js";
58
+ function recordLevelFailure(recordId, findings, skipped) {
59
+ return {
60
+ recordId,
61
+ checked: [],
62
+ skipped,
63
+ checkedCount: 0,
64
+ skippedCount: skipped.length,
65
+ findings,
66
+ waived: [],
67
+ complete: false,
68
+ };
69
+ }
70
+ /**
71
+ * Best-effort per-entry skip attribution for a `CopyRecord` whose own
72
+ * shape is invalid. `record.entries` is typed as `CopyEntry[]`, but the
73
+ * whole point of the branch that calls this is that the type cannot be
74
+ * trusted at runtime — so this reads defensively, using whatever id it
75
+ * can find and falling back to a positional label (`"$3"`, meaning index
76
+ * 3) for an entry too malformed to have a usable id at all. Never throws.
77
+ */
78
+ function bestEffortSkipList(entriesValue, reason) {
79
+ if (!Array.isArray(entriesValue))
80
+ return [];
81
+ return entriesValue.map((entry, i) => {
82
+ const hasUsableId = typeof entry === "object" &&
83
+ entry !== null &&
84
+ !Array.isArray(entry) &&
85
+ typeof entry.id === "string" &&
86
+ entry.id.length > 0;
87
+ const entryId = hasUsableId ? entry.id : `$${i}`;
88
+ return { entryId, reason };
89
+ });
90
+ }
91
+ /**
92
+ * Checks every entry in `record` against `voiceRecord`, via `voice`'s own
93
+ * `checkCopy`. See this file's top-of-file doc comment for the fail-
94
+ * closed behavior on an invalid `record`, an invalid `voiceRecord`, and a
95
+ * `record` with zero entries.
96
+ *
97
+ * Throws a plain `Error` (not a finding) for caller-input problems that
98
+ * are not this function's job to interpret as data: `record` or
99
+ * `voiceRecord` not an object at all. This mirrors `voice`'s own
100
+ * `checkCopy`, which treats a malformed call the same way — a hard
101
+ * upfront rejection, not a soft finding that could be mistaken for
102
+ * something the checker discovered about the record itself. A `record`
103
+ * or `voiceRecord` that IS an object but fails shape validation is,
104
+ * unlike a non-object, a real finding (`report.findings`), not a thrown
105
+ * error — see note #2 above for why this function validates both itself
106
+ * rather than trusting its own type signature.
107
+ */
108
+ export function checkCopyRecord(record, voiceRecord, options = {}) {
109
+ if (record === null || typeof record !== "object") {
110
+ throw new TypeError("checkCopyRecord: record must be a CopyRecord object");
111
+ }
112
+ if (voiceRecord === null || typeof voiceRecord !== "object") {
113
+ throw new TypeError("checkCopyRecord: voiceRecord must be a VoiceRecord object");
114
+ }
115
+ const recordIdForReport = isNonEmptyStringField(record, "id") ? record.id : "(unknown)";
116
+ // --- fail closed: the CopyRecord itself must be well-formed --------------
117
+ const recordShapeFindings = validateCopyRecordShape(record);
118
+ if (recordShapeFindings.length > 0) {
119
+ const findings = recordShapeFindings.map((f) => ({ ...f }));
120
+ const entriesValue = record.entries;
121
+ return recordLevelFailure(recordIdForReport, findings, bestEffortSkipList(entriesValue, "record-shape-invalid"));
122
+ }
123
+ // --- fail closed: the VoiceRecord itself must be well-formed --------------
124
+ // Unlike voice's own checkCopy, this function does not trust its typed
125
+ // input — see this file's top-of-file note #2 for why.
126
+ const voiceShapeFindings = validateVoiceRecordShape(voiceRecord);
127
+ if (voiceShapeFindings.length > 0) {
128
+ const findings = voiceShapeFindings.map((f) => ({ ...f }));
129
+ // record is now known well-formed (previous branch would have returned
130
+ // otherwise), so every entry has a real, unique id to attribute a skip to.
131
+ const skipped = record.entries.map((entry) => ({
132
+ entryId: entry.id,
133
+ reason: "voice-record-invalid",
134
+ }));
135
+ return recordLevelFailure(recordIdForReport, findings, skipped);
136
+ }
137
+ // --- fail closed: zero entries is nothing to check, not a clean pass -----
138
+ if (record.entries.length === 0) {
139
+ const findings = [
140
+ {
141
+ rule: "record:no-entries",
142
+ severity: "error",
143
+ message: `CopyRecord "${recordIdForReport}" has zero entries — there is nothing for checkCopyRecord to check.`,
144
+ },
145
+ ];
146
+ return recordLevelFailure(recordIdForReport, findings, []);
147
+ }
148
+ // --- the real work: run every entry through voice's checkCopy ------------
149
+ const checked = [];
150
+ const findings = [];
151
+ const waived = [];
152
+ for (const entry of record.entries) {
153
+ const entryReport = checkCopy(voiceRecord, entry.text, { waivers: options.waivers });
154
+ checked.push({ entryId: entry.id, report: entryReport });
155
+ for (const finding of entryReport.findings) {
156
+ findings.push({ ...finding, entryId: entry.id });
157
+ }
158
+ for (const w of entryReport.waived) {
159
+ waived.push({ ...w, entryId: entry.id });
160
+ }
161
+ }
162
+ return {
163
+ recordId: recordIdForReport,
164
+ checked,
165
+ skipped: [],
166
+ checkedCount: checked.length,
167
+ skippedCount: 0,
168
+ findings,
169
+ waived,
170
+ complete: true,
171
+ };
172
+ }
173
+ function isNonEmptyStringField(value, key) {
174
+ const v = value[key];
175
+ return typeof v === "string" && v.length > 0;
176
+ }
177
+ //# sourceMappingURL=checker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checker.js","sourceRoot":"","sources":["../src/checker.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `writer-check` — the CLI for `checkCopyTraceability` — AND, in this same
4
+ * file and under this SAME `writer-check` bin, a subcommand
5
+ * (`writer-check addressability`, dispatched on `argv[0] === "addressability"`
6
+ * before any of the existing argument parsing below runs — see the bottom
7
+ * of this file), `checkAddressability`: is user-facing prose resolved from
8
+ * the copy registry by id, rather than typed inline? A stricter, DIFFERENT
9
+ * question from traceability — see `addressability.ts`'s top doc comment
10
+ * for the full split. Deliberately NOT merged into the default command's
11
+ * own exit code: traceability's own fixtures/tests are built entirely
12
+ * around bare literal assignments (a registered string, an unregistered
13
+ * one, a `copy-gate:ignore` marker, a `copy:<id>` citation) — exactly the
14
+ * shapes addressability treats as either a violation (an attribute/
15
+ * text-node literal) or an unclassifiable position (a bare assignment,
16
+ * position 3 — see `addressability.ts`), by design and on purpose.
17
+ * Combining the two into one number would make the default command's own
18
+ * "clean pass" fixtures structurally unwritable (a literal a
19
+ * registered-text match needs to exist for traceability to prove anything
20
+ * is exactly a literal addressability cannot confirm is safe) rather than
21
+ * genuinely complementary. A SEPARATE `bin` entry pointing at this same
22
+ * compiled file was rejected for the identical reason `@example/ui`
23
+ * never does that: this repository's own root `package.json` invokes every
24
+ * gate by its COMPILED PATH (`node packages/ui/dist/tokens/contrast-cli.js`,
25
+ * `node packages/controller/dist/cli.js`, ...), never by installed `bin`
26
+ * name — a second `bin` entry pointing at the same file is invisible to
27
+ * that invocation style, and worse, silently falls through to the OTHER
28
+ * command rather than erroring. An explicit `argv[0]` subcommand is
29
+ * reachable however the file is invoked, by path or by name. Both
30
+ * commands are still presentation-only wrappers over this package's pure
31
+ * gates, both still print every skip/exclusion count unconditionally, and
32
+ * both still use the identical three-state exit-code contract described
33
+ * below — same shape, same file, same bin, two subcommands, two numbers,
34
+ * matching `@example/strategy`'s `strategy-facts-check` shape
35
+ * closely, deliberately: same argument order (the thing being checked
36
+ * FOR, then the thing being checked, where applicable), same `--help`,
37
+ * same `CliInputError` split between "bad arguments" and "ran, found
38
+ * something wrong".
39
+ *
40
+ * A second subcommand, `voice-derivation-coverage`, wires
41
+ * `checkVoiceDerivationCoverage` (`./voice/derivation-coverage.ts`) in with
42
+ * the identical shape: parse argv, load two JSON files, run the pure check,
43
+ * print a report, pick an exit code from the same 0/1/2 contract — see
44
+ * `runVoiceDerivationCoverage`'s own doc comment below. `main()` dispatches
45
+ * to it only when `argv[0]` is exactly `"voice-derivation-coverage"`; any
46
+ * other first argument (including every existing caller's real
47
+ * `record-file` path) falls through to the original, unchanged behavior
48
+ * above — this addition is purely additive to the argv contract.
49
+ *
50
+ * A third subcommand, `locale-coverage`, wires `checkLocaleCoverage`
51
+ * (`./locale-coverage.ts`) in with the identical shape again: parse argv,
52
+ * load a JSON registries file, run the pure check, print a report, pick an
53
+ * exit code — see `runLocaleCoverage`'s own doc comment below.
54
+ * `checkLocaleCoverage` already defines its own caller contract for turning
55
+ * a `LocaleCoverageReport` into this package's usual three-state exit code
56
+ * (see `LocaleCoverageReport.complete`'s own doc comment in
57
+ * `locale-coverage.ts`); `runLocaleCoverage` applies that mapping exactly,
58
+ * rather than inventing a second one. `main()` dispatches to it only when
59
+ * `argv[0]` is exactly `"locale-coverage"` — checked alongside
60
+ * `"voice-derivation-coverage"`, before either falls through to the
61
+ * original `record-file` handling — so this, too, is purely additive to the
62
+ * argv contract.
63
+ *
64
+ * A FOURTH subcommand, `passages` (`writer-check passages <registry-file>`),
65
+ * wires `checkPassageComposition` (`./passage.ts`) in — the passage-layer
66
+ * COMPOSITION gate: does every passage in a `PassageRecord` reference only
67
+ * registered entries and glossary terms, never an inlined literal string
68
+ * or another passage's own internals? Dispatched EXACTLY the way
69
+ * `addressability` is (see below): a fully separate top-level branch at
70
+ * the very bottom of this file, its own exported `mainPassagesCheck`, its
71
+ * own `runPassagesCheck` wrapper — never folded through `main()` at all,
72
+ * for the identical reason addressability is not: this gate's own ternary
73
+ * precedence ("a real violation wins over an incomplete picture" — see
74
+ * `passage.ts`'s own top doc comment, "THE TERNARY") is independent of
75
+ * every other subcommand's exit-code contract, and keeping it a fully
76
+ * separate dispatch path keeps that independence visible in this file's
77
+ * own structure, not just in a comment.
78
+ *
79
+ * Exit codes — a contract a consumer's CI depends on, matching this
80
+ * repository's `foundry-check` convention (`@example/gates`):
81
+ *
82
+ * 0 — ran cleanly: the copy record loaded, at least one file was
83
+ * actually scanned (successfully tokenized — see `scan.ts`'s
84
+ * `ScanResult.filesScanned`), zero findings.
85
+ * 1 — ran cleanly, at least one finding (an unregistered copy candidate,
86
+ * or a citation to a copy id that does not exist).
87
+ * 2 — could not run: bad input, the copy record missing/unreadable/
88
+ * invalid, the scan directory does not exist, the walk matched zero
89
+ * files, ANY matched file failed to tokenize (see `scan.parseFailures`
90
+ * — a file that could not be parsed was never examined for copy, and
91
+ * one such file among a hundred clean ones is still incomplete
92
+ * coverage, not a clean scan), an unreadable directory
93
+ * during the walk, an unexpected exception, a malformed
94
+ * `ScanOptions.pathExclusions` entry (see below), OR — as of the JSX
95
+ * text-node scanning added for issue #37 — at least one
96
+ * `ScanResult.unchecked` entry: a JSX construct the scanner
97
+ * recognized but could not reliably classify. That last case lands
98
+ * here, not as a `1`-severity finding, deliberately: `unchecked`
99
+ * means a REGION of an otherwise-matched, otherwise-parseable file
100
+ * was never actually examined for copy — the exact same shape of
101
+ * problem `parseFailures`/`filesScanned === 0` already are, just at
102
+ * finer grain (part of one file, rather than a whole file), so it
103
+ * gets the same answer they do. Every real finding is still printed
104
+ * first (see `printGateReport`) — `unchecked` does not suppress
105
+ * what WAS learned, it just refuses to let that partial picture
106
+ * read as "clean" the way a bare exit `0` would. Kept strictly
107
+ * distinct from 1 — a gate that reports "clean" after failing to
108
+ * run, OR after only partially running, is worse than no gate at
109
+ * all. This is the explicit third state this gate is built around:
110
+ * "could not check" must never be reported as a pass.
111
+ *
112
+ * A malformed `pathExclusions` entry (see `scan.ts`/`path-exclusions.ts`)
113
+ * gets the same `2` treatment as `unchecked`, for the identical reason: an
114
+ * exclusion list this run cannot trust means this run cannot say which
115
+ * files were correctly in or out of scope, which is "could not run", not "a
116
+ * clean scan that happened to find nothing". This CLI does not currently
117
+ * accept `pathExclusions` from argv — see `main()`'s own comment — so this
118
+ * only fires for a caller driving `scanCopySourceTree` directly with a
119
+ * malformed list and inspecting `ScanResult` themselves; it is handled here
120
+ * anyway so the accounting/exit-code contract stays correct for a future
121
+ * CLI flag without a second pass over this logic.
122
+ */
123
+ /** Exported for `cli.test.ts` — anything wrong with the arguments themselves always maps to exit code 2, never 1. */
124
+ export declare class CliInputError extends Error {
125
+ }
126
+ /**
127
+ * Exported (unlike a typical CLI `main`) so `cli.test.ts` can exercise the
128
+ * whole argv-to-exit-code contract directly, against real `mkdtemp` temp
129
+ * directories, without spawning a subprocess for every case. Takes `argv`
130
+ * as a parameter rather than reading `process.argv` itself for exactly
131
+ * that reason — `run()` below is the only caller that reads the real
132
+ * `process.argv`.
133
+ */
134
+ export declare function main(argv: string[]): number;
135
+ /**
136
+ * Exported (unlike a typical CLI `main`) for the identical reason `main`
137
+ * and `mainAddressabilityCheck` above are — `cli.test.ts` exercises the
138
+ * whole argv-to-exit-code contract directly against real fixture files,
139
+ * without spawning a subprocess.
140
+ */
141
+ export declare function mainPassagesCheck(argv: string[]): number;
142
+ /**
143
+ * Exported (unlike a typical CLI `main`) for the identical reason `main`
144
+ * above is — `cli.test.ts` exercises the whole argv-to-exit-code contract
145
+ * directly against real `mkdtemp` directories, without spawning a
146
+ * subprocess.
147
+ */
148
+ export declare function mainAddressabilityCheck(argv: string[]): number;
149
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwHG;AAuEH,qHAAqH;AACrH,qBAAa,aAAc,SAAQ,KAAK;CAAG;AA2f3C;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAyH3C;AAsGD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAqCxD;AAkFD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CA6B9D"}