@clossys/writer 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +103 -4
  2. package/dist/addressability.d.ts +11 -1
  3. package/dist/addressability.d.ts.map +1 -1
  4. package/dist/addressability.js +169 -3
  5. package/dist/addressability.js.map +1 -1
  6. package/dist/approved-copy-coverage-rate-cli.d.ts +10 -0
  7. package/dist/approved-copy-coverage-rate-cli.d.ts.map +1 -0
  8. package/dist/approved-copy-coverage-rate-cli.js +59 -0
  9. package/dist/approved-copy-coverage-rate-cli.js.map +1 -0
  10. package/dist/approved-copy-coverage-rate.d.ts +23 -0
  11. package/dist/approved-copy-coverage-rate.d.ts.map +1 -0
  12. package/dist/approved-copy-coverage-rate.js +153 -0
  13. package/dist/approved-copy-coverage-rate.js.map +1 -0
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +216 -9
  16. package/dist/cli.js.map +1 -1
  17. package/dist/fold-wallpaper-gate.d.ts +7 -0
  18. package/dist/fold-wallpaper-gate.d.ts.map +1 -0
  19. package/dist/fold-wallpaper-gate.js +29 -0
  20. package/dist/fold-wallpaper-gate.js.map +1 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/live-copy-gate.d.ts +16 -0
  26. package/dist/live-copy-gate.d.ts.map +1 -0
  27. package/dist/live-copy-gate.js +135 -0
  28. package/dist/live-copy-gate.js.map +1 -0
  29. package/dist/render-registry-parity.d.ts +20 -0
  30. package/dist/render-registry-parity.d.ts.map +1 -0
  31. package/dist/render-registry-parity.js +45 -0
  32. package/dist/render-registry-parity.js.map +1 -0
  33. package/dist/schema.d.ts.map +1 -1
  34. package/dist/schema.js +22 -0
  35. package/dist/schema.js.map +1 -1
  36. package/dist/treatment-word-budget.d.ts +31 -0
  37. package/dist/treatment-word-budget.d.ts.map +1 -0
  38. package/dist/treatment-word-budget.js +80 -0
  39. package/dist/treatment-word-budget.js.map +1 -0
  40. package/dist/types.d.ts +11 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/fit-signals.json +11 -0
  43. package/package.json +202 -6
  44. package/skill/SKILL.md +49 -0
  45. package/src/addressability.ts +186 -4
  46. package/src/approved-copy-coverage-rate-cli.ts +55 -0
  47. package/src/approved-copy-coverage-rate.ts +201 -0
  48. package/src/cli.ts +228 -9
  49. package/src/fold-wallpaper-gate.ts +29 -0
  50. package/src/index.ts +16 -0
  51. package/src/live-copy-gate.ts +150 -0
  52. package/src/render-registry-parity.ts +65 -0
  53. package/src/schema.ts +24 -0
  54. package/src/treatment-word-budget.ts +100 -0
  55. package/src/types.ts +11 -0
  56. package/CHANGELOG.md +0 -307
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Computes the Writer charter metric `approved copy coverage rate` from
3
+ * consumer-supplied independent observations.
4
+ *
5
+ * This module does not invent observations and does not relabel
6
+ * `checkCopyRecord` or `checkCopyTraceability`. An empty evaluated set is
7
+ * indeterminate, never a perfect rate of 1. `writer-check` remains the
8
+ * multi-mode CLI.
9
+ */
10
+ const METRIC = "approved copy coverage rate";
11
+ const RESERVED_OBSERVERS = new Set(["writer", "@clossys/writer"]);
12
+ const PROPOSED_POSITIONS = [];
13
+ function record(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ function text(value) {
17
+ return typeof value === "string" && value.trim().length > 0;
18
+ }
19
+ function timestamp(value) {
20
+ return text(value) && !Number.isNaN(Date.parse(value));
21
+ }
22
+ function finding(rule, message, path) {
23
+ return path === undefined ? { rule, severity: "error", message } : { rule, severity: "error", message, path };
24
+ }
25
+ function evidenceCounts(value) {
26
+ if (!Array.isArray(value) || value.length === 0)
27
+ return false;
28
+ return value.every((item) => record(item) && text(item.id) && text(item.description));
29
+ }
30
+ function reservedObserver(value) {
31
+ return RESERVED_OBSERVERS.has(value.trim().toLowerCase());
32
+ }
33
+ function report(state, evaluatedCopy, approvedCoveredCopy, rate, findings) {
34
+ return {
35
+ metric: METRIC,
36
+ state,
37
+ rate,
38
+ evaluatedCopy,
39
+ approvedCoveredCopy,
40
+ findings,
41
+ proposedPositions: PROPOSED_POSITIONS,
42
+ };
43
+ }
44
+ function isCovered(entry) {
45
+ return entry.approved && entry.traceable && entry.resolvedFromRegistry;
46
+ }
47
+ /**
48
+ * Computes `approved copy coverage rate`: shipped audience-facing copy
49
+ * resolved from approved traceable registry entries / all shipped
50
+ * audience-facing copy evaluated.
51
+ */
52
+ export function assessApprovedCopyCoverageRate(input) {
53
+ if (!record(input)) {
54
+ return report("indeterminate", 0, 0, null, [finding("assessment-shape", "Assessment input must be an object.", "$")]);
55
+ }
56
+ const findings = [];
57
+ if (!timestamp(input.asOf)) {
58
+ findings.push(finding("assessment-as-of", "asOf must be an interpretable timestamp.", "asOf"));
59
+ }
60
+ const declaredCopy = input.declaredCopy;
61
+ if (!Array.isArray(declaredCopy)) {
62
+ findings.push(finding("declared-copy-required", "declaredCopy must be an array of { id } objects.", "declaredCopy"));
63
+ return report("indeterminate", 0, 0, null, findings);
64
+ }
65
+ const declaredIds = [];
66
+ const declaredSet = new Set();
67
+ declaredCopy.forEach((item, index) => {
68
+ const path = `declaredCopy[${index}]`;
69
+ if (!record(item) || !text(item.id)) {
70
+ findings.push(finding("copy-id-required", "id must be a non-empty string.", `${path}.id`));
71
+ return;
72
+ }
73
+ if (declaredSet.has(item.id)) {
74
+ findings.push(finding("duplicate-copy-id", `Duplicate id "${item.id}".`, `${path}.id`));
75
+ return;
76
+ }
77
+ declaredSet.add(item.id);
78
+ declaredIds.push(item.id);
79
+ });
80
+ if (declaredIds.length === 0) {
81
+ findings.push(finding("declared-copy-empty", "No declared copy is available for the approved-copy-coverage metric; the rate is indeterminate, never 1.", "declaredCopy"));
82
+ }
83
+ const observations = input.observations;
84
+ if (observations !== undefined && !Array.isArray(observations)) {
85
+ findings.push(finding("observations-shape", "observations must be an array.", "observations"));
86
+ }
87
+ const counting = new Map();
88
+ if (Array.isArray(observations)) {
89
+ observations.forEach((item, index) => {
90
+ const path = `observations[${index}]`;
91
+ if (!record(item)) {
92
+ findings.push(finding("observation-shape", "An observation must be an object.", path));
93
+ return;
94
+ }
95
+ if (text(item.observerRef) && reservedObserver(item.observerRef)) {
96
+ findings.push(finding("self-observation", "Writer cannot observe itself; reserved observerRef values are not independent.", `${path}.observerRef`));
97
+ return;
98
+ }
99
+ if (text(item.copyId) && !declaredSet.has(item.copyId)) {
100
+ findings.push(finding("unknown-copy", `Observation copyId "${item.copyId}" is not declared copy.`, `${path}.copyId`));
101
+ return;
102
+ }
103
+ if (item.independent !== true)
104
+ return;
105
+ if (typeof item.approved !== "boolean" ||
106
+ typeof item.traceable !== "boolean" ||
107
+ typeof item.resolvedFromRegistry !== "boolean")
108
+ return;
109
+ if (!text(item.observerRef) || !evidenceCounts(item.evidence) || !text(item.copyId) || !declaredSet.has(item.copyId))
110
+ return;
111
+ const bucket = counting.get(item.copyId) ?? [];
112
+ bucket.push({
113
+ approved: item.approved,
114
+ traceable: item.traceable,
115
+ resolvedFromRegistry: item.resolvedFromRegistry,
116
+ });
117
+ counting.set(item.copyId, bucket);
118
+ });
119
+ }
120
+ let evaluatedCopy = 0;
121
+ let approvedCoveredCopy = 0;
122
+ for (const id of declaredIds) {
123
+ const observed = counting.get(id) ?? [];
124
+ if (observed.length === 0) {
125
+ findings.push(finding("copy-unevaluated", `Declared copy "${id}" has no counting independent observation.`, `declaredCopy.${id}`));
126
+ continue;
127
+ }
128
+ evaluatedCopy += 1;
129
+ const allCovered = observed.every((entry) => isCovered(entry));
130
+ if (allCovered)
131
+ approvedCoveredCopy += 1;
132
+ else {
133
+ findings.push(finding("copy-not-covered", `Declared copy "${id}" was independently observed not resolved from an approved traceable registry entry.`, `declaredCopy.${id}`));
134
+ }
135
+ if (observed.length > 1) {
136
+ const first = observed[0];
137
+ const disagree = observed.some((entry) => entry.approved !== first.approved ||
138
+ entry.traceable !== first.traceable ||
139
+ entry.resolvedFromRegistry !== first.resolvedFromRegistry);
140
+ if (disagree) {
141
+ findings.push(finding("observation-disagreement", `Counting observations for "${id}" disagree.`, `observations.${id}`));
142
+ }
143
+ }
144
+ }
145
+ const rate = evaluatedCopy === 0 ? null : approvedCoveredCopy / evaluatedCopy;
146
+ if (evaluatedCopy === 0)
147
+ return report("indeterminate", 0, 0, null, findings);
148
+ const coverageComplete = evaluatedCopy === declaredIds.length;
149
+ const noErrorFindings = findings.length === 0;
150
+ const state = coverageComplete && approvedCoveredCopy === evaluatedCopy && noErrorFindings ? "satisfied" : "violated";
151
+ return report(state, evaluatedCopy, approvedCoveredCopy, rate, findings);
152
+ }
153
+ //# sourceMappingURL=approved-copy-coverage-rate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approved-copy-coverage-rate.js","sourceRoot":"","sources":["../src/approved-copy-coverage-rate.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwHG;AAWH,OAAO,EAAyB,KAAK,eAAe,EAAE,KAAK,eAAe,EAAuB,MAAM,gBAAgB,CAAC;AAIxH,OAAO,EAAuC,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAkEpF,qHAAqH;AACrH,qBAAa,aAAc,SAAQ,KAAK;CAAG;AAsgB3C;;;;;;;GAOG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,GAAG,UAAU,GAAG,eAAe,CAAC;IAChD,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,aAAa,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACnD;AAmBD,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAyL3C;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"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwHG;AAWH,OAAO,EAAyB,KAAK,eAAe,EAAE,KAAK,eAAe,EAAuB,MAAM,gBAAgB,CAAC;AAMxH,OAAO,EAAuC,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAsEpF,qHAAqH;AACrH,qBAAa,aAAc,SAAQ,KAAK;CAAG;AAojB3C;;;;;;;GAOG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,GAAG,UAAU,GAAG,eAAe,CAAC;IAChD,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,aAAa,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACnD;AAmBD,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CA2R3C;AAsGD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAqCxD;AA4ID;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAwC9D"}
package/dist/cli.js CHANGED
@@ -126,10 +126,13 @@ import { fileURLToPath } from "node:url";
126
126
  import { checkAddressability, scanAddressabilitySources, } from "./addressability.js";
127
127
  import { checkCopyTraceability } from "./copy-gate.js";
128
128
  import { checkLocaleCoverage } from "./locale-coverage.js";
129
+ import { liveTreeExists, scanLiveCopyTrees } from "./live-copy-gate.js";
129
130
  import { checkPassageComposition, readPassageRecord } from "./passage.js";
130
131
  import { readCopyRecord } from "./registry.js";
132
+ import { checkRenderRegistryParity } from "./render-registry-parity.js";
131
133
  import { scanCopySourceTree } from "./scan.js";
132
- import { checkVoiceDerivationCoverage } from "./voice/index.js";
134
+ import { checkTreatmentWordBudgets } from "./treatment-word-budget.js";
135
+ import { checkVoiceDerivationCoverage, parseVoiceRecord } from "./voice/index.js";
133
136
  const USAGE = `Usage: writer-check <record-file> [scan-dir] [options]
134
137
  or: writer-check voice-derivation-coverage <obligations-file> <brand-derived-rule-ids-file> [options]
135
138
  or: writer-check locale-coverage <registries-file> <source-locale> [declared-locale...] [options]
@@ -147,6 +150,9 @@ are separate, stricter gates; neither runs as part of this default command.
147
150
 
148
151
  Options:
149
152
  --help Print this message and exit 0.
153
+ --render-registry <file> Path the surface passes to createCopyResolver at render. When set, must be the same file as record-file (after realpath). When omitted, record-file itself must be a CopyRegistry — not a plain CopyRecord or an in-memory-only store.
154
+ --live <dir> Declared live-copy tree the page actually renders (repeatable). Requires --voice-record.
155
+ --voice-record <file> Voice record JSON for claims when --live is set.
150
156
  --format <text|json> Output format. Defaults to text. "json" prints exactly one
151
157
  machine-readable object to stdout (see CopyTraceabilityReport in
152
158
  cli.ts) instead of the human-readable report — "verdict" states the
@@ -196,6 +202,9 @@ export class CliInputError extends Error {
196
202
  function parseArgs(argv) {
197
203
  let recordFile;
198
204
  let scanDir;
205
+ let renderRegistryFile;
206
+ const liveTrees = [];
207
+ let voiceRecordFile;
199
208
  let format = "text";
200
209
  let help = false;
201
210
  for (let i = 0; i < argv.length; i++) {
@@ -204,6 +213,14 @@ function parseArgs(argv) {
204
213
  help = true;
205
214
  continue;
206
215
  }
216
+ if (arg === "--render-registry") {
217
+ const value = argv[++i];
218
+ if (value === undefined || value.startsWith("-")) {
219
+ throw new CliInputError("--render-registry requires a file path");
220
+ }
221
+ renderRegistryFile = value;
222
+ continue;
223
+ }
207
224
  if (arg === "--format") {
208
225
  const value = argv[++i];
209
226
  if (value !== "text" && value !== "json") {
@@ -212,6 +229,20 @@ function parseArgs(argv) {
212
229
  format = value;
213
230
  continue;
214
231
  }
232
+ if (arg === "--live") {
233
+ const value = argv[++i];
234
+ if (value === undefined || value.startsWith("-"))
235
+ throw new CliInputError("--live requires a directory path");
236
+ liveTrees.push(value);
237
+ continue;
238
+ }
239
+ if (arg === "--voice-record") {
240
+ const value = argv[++i];
241
+ if (value === undefined || value.startsWith("-"))
242
+ throw new CliInputError("--voice-record requires a file path");
243
+ voiceRecordFile = value;
244
+ continue;
245
+ }
215
246
  if (arg.startsWith("-")) {
216
247
  throw new CliInputError(`unknown flag "${arg}"`);
217
248
  }
@@ -225,7 +256,20 @@ function parseArgs(argv) {
225
256
  throw new CliInputError(`unexpected extra argument "${arg}"`);
226
257
  }
227
258
  }
228
- return { recordFile, scanDir, format, help };
259
+ if (liveTrees.length > 0 && !voiceRecordFile) {
260
+ throw new CliInputError("--voice-record is required when --live is declared");
261
+ }
262
+ return { recordFile, scanDir, renderRegistryFile, liveTrees, voiceRecordFile, format, help };
263
+ }
264
+ function printTreatmentWordBudgetReport(findings) {
265
+ if (findings.length === 0) {
266
+ console.log("No treatment word-budget findings.");
267
+ return;
268
+ }
269
+ console.log(`\n${findings.length} treatment word-budget finding(s):`);
270
+ for (const f of findings) {
271
+ console.log(` [${f.rule}] ${f.entryId} ${f.message}`);
272
+ }
229
273
  }
230
274
  function requireDirectory(label, path) {
231
275
  if (!existsSync(path))
@@ -637,7 +681,11 @@ function printAddressabilityReport(result) {
637
681
  else {
638
682
  console.log(`\n[addressability] ${result.violations.length} violation(s):`);
639
683
  for (const v of result.violations) {
640
- const where = v.position === "markup-text" ? "a markup text node" : `the "${v.attribute}" attribute`;
684
+ const where = v.position === "markup-text"
685
+ ? "a markup text node"
686
+ : v.position === "object-literal-copy"
687
+ ? `object-literal key "${v.keyPath ?? v.objectKey}"`
688
+ : `the "${v.attribute}" attribute`;
641
689
  console.log(` [copy-addressability] ${v.file}:${v.line} inline prose in ${where}, not resolved from the copy registry by id: ${v.raw}`);
642
690
  }
643
691
  }
@@ -711,6 +759,35 @@ export function main(argv) {
711
759
  emitJson(emptyReport(recordFile, scanDir, 2, `copy record could not be loaded: ${read.issues.map((i) => `[${i.reason}] ${i.detail}`).join("; ")}`));
712
760
  return 2;
713
761
  }
762
+ let recordRaw;
763
+ try {
764
+ recordRaw = JSON.parse(readFileSync(recordFile, "utf8"));
765
+ }
766
+ catch (error) {
767
+ const detail = error instanceof Error ? error.message : String(error);
768
+ if (!jsonMode) {
769
+ console.error(`\nCopy record could not be re-read for registry parity: ${detail}`);
770
+ console.error("Refusing to report a pass without confirming the render registry store.");
771
+ }
772
+ emitJson(emptyReport(recordFile, scanDir, 2, `copy record could not be re-read: ${detail}`));
773
+ return 2;
774
+ }
775
+ const renderRegistryPath = args.renderRegistryFile ? resolve(args.renderRegistryFile) : undefined;
776
+ if (renderRegistryPath)
777
+ requireFile("render-registry", renderRegistryPath);
778
+ const parity = checkRenderRegistryParity(recordFile, recordRaw, renderRegistryPath);
779
+ if (!parity.ok || !parity.registry) {
780
+ if (!jsonMode) {
781
+ console.error(`\nRender registry parity: ${parity.detail ?? parity.reason}`);
782
+ console.error("Refusing to report a pass when the CLI is not reading the same store createCopyResolver uses.");
783
+ }
784
+ emitJson(emptyReport(recordFile, scanDir, 2, parity.detail ?? parity.reason ?? "render registry parity failed"));
785
+ return 2;
786
+ }
787
+ const treatmentBudget = checkTreatmentWordBudgets(parity.registry);
788
+ if (!jsonMode && treatmentBudget.entriesChecked > 0) {
789
+ console.log(`Treatment word budgets: ${treatmentBudget.entriesChecked} approved entr${treatmentBudget.entriesChecked === 1 ? "y" : "ies"} with a treatment checked.`);
790
+ }
714
791
  // No argv flag populates `pathExclusions` today — this CLI always calls
715
792
  // `scanCopySourceTree` with defaults (`[]`), so `scan.excludedFiles`/
716
793
  // `scan.pathExclusionFindings` are always empty in THIS binary as
@@ -781,6 +858,70 @@ export function main(argv) {
781
858
  const result = checkCopyTraceability(scan.candidates, scan.citations, read.record, scan.filesScanned, scan.unchecked);
782
859
  if (!jsonMode)
783
860
  printGateReport(result);
861
+ if (!jsonMode)
862
+ printTreatmentWordBudgetReport(treatmentBudget.findings);
863
+ const hasTreatmentFindings = treatmentBudget.findings.length > 0;
864
+ if (args.liveTrees.length > 0) {
865
+ for (const livePath of args.liveTrees) {
866
+ const resolvedLive = resolve(livePath);
867
+ if (!liveTreeExists(resolvedLive)) {
868
+ if (!jsonMode) {
869
+ console.error(`\nDeclared live-copy tree "${resolvedLive}" does not exist.`);
870
+ console.error("Refusing to report a pass without scanning every declared live tree.");
871
+ }
872
+ emitJson(emptyReport(recordFile, scanDir, 2, `declared live-copy tree missing: ${resolvedLive}`));
873
+ return 2;
874
+ }
875
+ }
876
+ const voicePath = resolve(args.voiceRecordFile);
877
+ requireFile("voice-record", voicePath);
878
+ let voiceRaw;
879
+ try {
880
+ voiceRaw = JSON.parse(readFileSync(voicePath, "utf8"));
881
+ }
882
+ catch (error) {
883
+ if (!jsonMode)
884
+ console.error(`Voice record could not be loaded: ${error instanceof Error ? error.message : String(error)}`);
885
+ emitJson(emptyReport(recordFile, scanDir, 2, "voice record could not be loaded"));
886
+ return 2;
887
+ }
888
+ let voiceRecord;
889
+ try {
890
+ voiceRecord = parseVoiceRecord(voiceRaw);
891
+ }
892
+ catch (error) {
893
+ if (!jsonMode)
894
+ console.error(`Voice record is invalid: ${error instanceof Error ? error.message : String(error)}`);
895
+ emitJson(emptyReport(recordFile, scanDir, 2, "voice record is invalid"));
896
+ return 2;
897
+ }
898
+ const liveScan = scanLiveCopyTrees(voiceRecord, args.liveTrees.map((p) => resolve(p)));
899
+ if (!jsonMode) {
900
+ console.log(`Live-copy trees: ${args.liveTrees.length}, ${liveScan.filesScanned} file(s) scanned.`);
901
+ if (liveScan.findings.length > 0) {
902
+ console.log(`\n${liveScan.findings.length} live-copy finding(s):`);
903
+ for (const f of liveScan.findings)
904
+ console.log(` [${f.rule}] ${f.file}:${f.line} ${f.message}`);
905
+ }
906
+ }
907
+ if (liveScan.findings.length > 0) {
908
+ const exitCode = result.unchecked.length > 0 ? 2 : 1;
909
+ emitJson({
910
+ recordFile,
911
+ scanDir,
912
+ verdict: result.unchecked.length > 0 ? "indeterminate" : "findings",
913
+ exitCode,
914
+ filesScanned: scan.filesScanned,
915
+ candidatesScanned: result.candidatesScanned,
916
+ matched: result.matched,
917
+ findings: result.findings,
918
+ ignored: result.ignored,
919
+ unchecked: result.unchecked,
920
+ parseFailures: scan.parseFailures.map((p) => ({ file: p.file, detail: p.detail })),
921
+ });
922
+ return exitCode;
923
+ }
924
+ }
784
925
  // `unchecked` wins over everything else in the exit-code decision — see
785
926
  // this file's own top doc comment for why it is a `2`, not a `1`: it
786
927
  // means part of a matched file was never actually examined, which is
@@ -799,8 +940,12 @@ export function main(argv) {
799
940
  // the SAME object, so a consumer keying on `exitCode`/`verdict` alone
800
941
  // still fails closed exactly as this package intends, while a consumer
801
942
  // that reads further is never forced to throw away what was measured.
802
- const exitCode = result.unchecked.length > 0 ? 2 : result.findings.length > 0 ? 1 : 0;
803
- const verdict = result.unchecked.length > 0 ? "indeterminate" : result.findings.length > 0 ? "findings" : "clean";
943
+ const exitCode = result.unchecked.length > 0 ? 2 : result.findings.length > 0 || hasTreatmentFindings ? 1 : 0;
944
+ const verdict = result.unchecked.length > 0
945
+ ? "indeterminate"
946
+ : result.findings.length > 0 || hasTreatmentFindings
947
+ ? "findings"
948
+ : "clean";
804
949
  emitJson({
805
950
  recordFile,
806
951
  scanDir,
@@ -992,17 +1137,68 @@ registry-text-match escape hatch here, unlike the default command.
992
1137
 
993
1138
  Options:
994
1139
  --help Print this message and exit 0.
1140
+ --extensions <ext> File extension(s) to scan, including the leading dot (for example .mjs). Repeatable, and each occurrence may itself be a comma-separated list (for example --extensions .mjs,.cjs); values union across both forms. When omitted, default is .ts, .tsx, .js, .jsx. When present, replaces that default. Every value must include the leading dot; a flag that resolves to zero extensions (for example a bare comma) is a usage error, never a vacuous pass.
1141
+ --chrome <file> Persistent chrome (site header, footer, skip link, nav labels) shell or layout file to scan in addition to scan-dir (repeatable; paths relative to scan-dir unless absolute). Each file must exist.
1142
+ --require-chrome Refuse to run (exit 2) when no --chrome file was declared — use when the surface mounts persistent chrome outside scan-dir.
995
1143
 
996
- Exit codes: 0 = clean, 1 = at least one inline user-facing string found, 2 = could not run (bad input, nothing matched to scan, every matched file failed to parse, or a string position could not be confidently classified).
1144
+ Exit codes: 0 = clean, 1 = at least one inline user-facing string found, 2 = could not run (bad input, nothing matched to scan, every matched file failed to parse, a string position could not be confidently classified, a declared chrome file is missing, or --require-chrome was set without --chrome).
997
1145
  `;
1146
+ const ADDRESSABILITY_EXTENSION_RE = /^\.[a-zA-Z0-9]+$/;
998
1147
  function parseAddressabilityArgs(argv) {
999
1148
  let scanDir;
1149
+ const extensions = [];
1150
+ const chromeFiles = [];
1000
1151
  let help = false;
1001
- for (const arg of argv) {
1152
+ let sawExtensions = false;
1153
+ let requireChrome = false;
1154
+ for (let i = 0; i < argv.length; i++) {
1155
+ const arg = argv[i];
1002
1156
  if (arg === "--help" || arg === "-h") {
1003
1157
  help = true;
1004
1158
  continue;
1005
1159
  }
1160
+ if (arg === "--require-chrome") {
1161
+ requireChrome = true;
1162
+ continue;
1163
+ }
1164
+ if (arg === "--chrome") {
1165
+ const value = argv[++i];
1166
+ if (value === undefined || value.startsWith("-")) {
1167
+ throw new CliInputError("--chrome requires a file path");
1168
+ }
1169
+ chromeFiles.push(value);
1170
+ continue;
1171
+ }
1172
+ if (arg === "--extensions") {
1173
+ sawExtensions = true;
1174
+ const value = argv[++i];
1175
+ if (value === undefined || value.startsWith("-")) {
1176
+ throw new CliInputError("--extensions requires an extension such as .mjs");
1177
+ }
1178
+ // Repeatable AND comma-separated: `--extensions .mjs,.cjs` and
1179
+ // `--extensions .mjs --extensions .cjs` union to the same set. Every
1180
+ // token from the split is validated individually — a bare comma or a
1181
+ // trailing/leading comma yields an empty token, which is a usage
1182
+ // error below rather than being silently dropped (an --extensions
1183
+ // flag that resolves to zero extensions must never fall back to the
1184
+ // default set — that would be a vacuous pass on the caller's own
1185
+ // explicit-but-empty request).
1186
+ const tokens = value.split(",");
1187
+ if (tokens.length === 0 || tokens.some((token) => token.trim().length === 0)) {
1188
+ throw new CliInputError(`--extensions requires at least one non-empty extension such as .mjs, got ${JSON.stringify(value)}`);
1189
+ }
1190
+ for (const rawToken of tokens) {
1191
+ const token = rawToken.trim().toLowerCase();
1192
+ if (!token.startsWith(".")) {
1193
+ throw new CliInputError(`--extensions values must include the leading dot, got ${JSON.stringify(token)}`);
1194
+ }
1195
+ if (!ADDRESSABILITY_EXTENSION_RE.test(token)) {
1196
+ throw new CliInputError(`invalid --extensions value ${JSON.stringify(token)}`);
1197
+ }
1198
+ extensions.push(token);
1199
+ }
1200
+ continue;
1201
+ }
1006
1202
  if (arg.startsWith("-")) {
1007
1203
  throw new CliInputError(`unknown flag "${arg}"`);
1008
1204
  }
@@ -1013,7 +1209,7 @@ function parseAddressabilityArgs(argv) {
1013
1209
  throw new CliInputError(`unexpected extra argument "${arg}"`);
1014
1210
  }
1015
1211
  }
1016
- return { scanDir, help };
1212
+ return { scanDir, help, extensions: sawExtensions ? extensions : undefined, chromeFiles, requireChrome };
1017
1213
  }
1018
1214
  /**
1019
1215
  * Exported (unlike a typical CLI `main`) for the identical reason `main`
@@ -1029,11 +1225,22 @@ export function mainAddressabilityCheck(argv) {
1029
1225
  }
1030
1226
  const scanDir = resolve(args.scanDir ?? process.cwd());
1031
1227
  requireDirectory("scan-dir", scanDir);
1228
+ if (args.requireChrome && args.chromeFiles.length === 0) {
1229
+ throw new CliInputError("--require-chrome was set but no --chrome file was declared");
1230
+ }
1032
1231
  console.log(`Scan directory: ${scanDir}`);
1232
+ if (args.chromeFiles.length > 0) {
1233
+ console.log(`Chrome files: ${args.chromeFiles.join(", ")}`);
1234
+ }
1235
+ const scanOptions = {};
1236
+ if (args.extensions)
1237
+ scanOptions.extensions = args.extensions;
1238
+ if (args.chromeFiles.length > 0)
1239
+ scanOptions.chromeFiles = args.chromeFiles;
1033
1240
  // Throws (fail-closed) on an unreadable directory, exactly like
1034
1241
  // `scanCopySourceTree` — caught by `runAddressabilityCheck()`'s own
1035
1242
  // catch-all below.
1036
- const scan = scanAddressabilitySources(scanDir);
1243
+ const scan = scanAddressabilitySources(scanDir, scanOptions);
1037
1244
  printAddressabilityAccounting(scan);
1038
1245
  const result = checkAddressability(scan);
1039
1246
  printAddressabilityReport(result);