@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.
- package/README.md +103 -4
- package/dist/addressability.d.ts +11 -1
- package/dist/addressability.d.ts.map +1 -1
- package/dist/addressability.js +169 -3
- package/dist/addressability.js.map +1 -1
- package/dist/approved-copy-coverage-rate-cli.d.ts +10 -0
- package/dist/approved-copy-coverage-rate-cli.d.ts.map +1 -0
- package/dist/approved-copy-coverage-rate-cli.js +59 -0
- package/dist/approved-copy-coverage-rate-cli.js.map +1 -0
- package/dist/approved-copy-coverage-rate.d.ts +23 -0
- package/dist/approved-copy-coverage-rate.d.ts.map +1 -0
- package/dist/approved-copy-coverage-rate.js +153 -0
- package/dist/approved-copy-coverage-rate.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +216 -9
- package/dist/cli.js.map +1 -1
- package/dist/fold-wallpaper-gate.d.ts +7 -0
- package/dist/fold-wallpaper-gate.d.ts.map +1 -0
- package/dist/fold-wallpaper-gate.js +29 -0
- package/dist/fold-wallpaper-gate.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/live-copy-gate.d.ts +16 -0
- package/dist/live-copy-gate.d.ts.map +1 -0
- package/dist/live-copy-gate.js +135 -0
- package/dist/live-copy-gate.js.map +1 -0
- package/dist/render-registry-parity.d.ts +20 -0
- package/dist/render-registry-parity.d.ts.map +1 -0
- package/dist/render-registry-parity.js +45 -0
- package/dist/render-registry-parity.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +22 -0
- package/dist/schema.js.map +1 -1
- package/dist/treatment-word-budget.d.ts +31 -0
- package/dist/treatment-word-budget.d.ts.map +1 -0
- package/dist/treatment-word-budget.js +80 -0
- package/dist/treatment-word-budget.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/fit-signals.json +11 -0
- package/package.json +202 -6
- package/skill/SKILL.md +49 -0
- package/src/addressability.ts +186 -4
- package/src/approved-copy-coverage-rate-cli.ts +55 -0
- package/src/approved-copy-coverage-rate.ts +201 -0
- package/src/cli.ts +228 -9
- package/src/fold-wallpaper-gate.ts +29 -0
- package/src/index.ts +16 -0
- package/src/live-copy-gate.ts +150 -0
- package/src/render-registry-parity.ts +65 -0
- package/src/schema.ts +24 -0
- package/src/treatment-word-budget.ts +100 -0
- package/src/types.ts +11 -0
- 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;
|
|
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 {
|
|
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
|
-
|
|
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"
|
|
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
|
|
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,
|
|
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
|
-
|
|
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);
|