@clossys/writer 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +196 -0
- package/LICENSE +21 -0
- package/README.md +593 -0
- package/dist/addressability.d.ts +245 -0
- package/dist/addressability.d.ts.map +1 -0
- package/dist/addressability.js +567 -0
- package/dist/addressability.js.map +1 -0
- package/dist/checker.d.ts +159 -0
- package/dist/checker.d.ts.map +1 -0
- package/dist/checker.js +177 -0
- package/dist/checker.js.map +1 -0
- package/dist/cli.d.ts +149 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1049 -0
- package/dist/cli.js.map +1 -0
- package/dist/copy-gate.d.ts +107 -0
- package/dist/copy-gate.d.ts.map +1 -0
- package/dist/copy-gate.js +132 -0
- package/dist/copy-gate.js.map +1 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +64 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/locale-coverage.d.ts +227 -0
- package/dist/locale-coverage.d.ts.map +1 -0
- package/dist/locale-coverage.js +414 -0
- package/dist/locale-coverage.js.map +1 -0
- package/dist/passage.d.ts +294 -0
- package/dist/passage.d.ts.map +1 -0
- package/dist/passage.js +463 -0
- package/dist/passage.js.map +1 -0
- package/dist/path-exclusions.d.ts +114 -0
- package/dist/path-exclusions.d.ts.map +1 -0
- package/dist/path-exclusions.js +213 -0
- package/dist/path-exclusions.js.map +1 -0
- package/dist/registry.d.ts +61 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +64 -0
- package/dist/registry.js.map +1 -0
- package/dist/resolve.d.ts +33 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +133 -0
- package/dist/resolve.js.map +1 -0
- package/dist/scan.d.ts +457 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +1698 -0
- package/dist/scan.js.map +1 -0
- package/dist/schema.d.ts +81 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +482 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +268 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +40 -0
- package/dist/types.js.map +1 -0
- package/dist/voice/checker.d.ts +186 -0
- package/dist/voice/checker.d.ts.map +1 -0
- package/dist/voice/checker.js +483 -0
- package/dist/voice/checker.js.map +1 -0
- package/dist/voice/derivation-coverage.d.ts +168 -0
- package/dist/voice/derivation-coverage.d.ts.map +1 -0
- package/dist/voice/derivation-coverage.js +181 -0
- package/dist/voice/derivation-coverage.js.map +1 -0
- package/dist/voice/fields.d.ts +107 -0
- package/dist/voice/fields.d.ts.map +1 -0
- package/dist/voice/fields.js +144 -0
- package/dist/voice/fields.js.map +1 -0
- package/dist/voice/index.d.ts +19 -0
- package/dist/voice/index.d.ts.map +1 -0
- package/dist/voice/index.js +24 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/internal/parse-template.d.ts +27 -0
- package/dist/voice/internal/parse-template.d.ts.map +1 -0
- package/dist/voice/internal/parse-template.js +111 -0
- package/dist/voice/internal/parse-template.js.map +1 -0
- package/dist/voice/internal/pattern-safety.d.ts +111 -0
- package/dist/voice/internal/pattern-safety.d.ts.map +1 -0
- package/dist/voice/internal/pattern-safety.js +278 -0
- package/dist/voice/internal/pattern-safety.js.map +1 -0
- package/dist/voice/schema.d.ts +65 -0
- package/dist/voice/schema.d.ts.map +1 -0
- package/dist/voice/schema.js +535 -0
- package/dist/voice/schema.js.map +1 -0
- package/dist/voice/types.d.ts +282 -0
- package/dist/voice/types.d.ts.map +1 -0
- package/dist/voice/types.js +37 -0
- package/dist/voice/types.js.map +1 -0
- package/package.json +72 -0
- package/src/addressability.ts +664 -0
- package/src/checker.ts +293 -0
- package/src/cli.ts +1166 -0
- package/src/copy-gate.ts +197 -0
- package/src/fingerprint.ts +66 -0
- package/src/index.ts +163 -0
- package/src/locale-coverage.ts +512 -0
- package/src/passage.ts +644 -0
- package/src/path-exclusions.ts +265 -0
- package/src/registry.ts +103 -0
- package/src/resolve.ts +172 -0
- package/src/scan.ts +1959 -0
- package/src/schema.ts +528 -0
- package/src/types.ts +301 -0
- package/src/voice/checker.ts +590 -0
- package/src/voice/derivation-coverage.ts +217 -0
- package/src/voice/fields.ts +160 -0
- package/src/voice/index.ts +55 -0
- package/src/voice/internal/parse-template.ts +114 -0
- package/src/voice/internal/pattern-safety.ts +336 -0
- package/src/voice/schema.ts +585 -0
- package/src/voice/types.ts +329 -0
- package/templates/voice-record.template.jsonc +135 -0
package/dist/passage.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PASSAGE LAYER — issue #373. `@example/copy` (now `writer`,
|
|
3
|
+
* per issue #373's own retargeting comment) has terms (a glossary) and
|
|
4
|
+
* entries (single addressable strings) and nothing between them.
|
|
5
|
+
* `@clossys/designer` has tokens, atoms, AND BLOCKS. This file is the
|
|
6
|
+
* missing middle: a `Passage` composes `CopyEntry`/glossary-term
|
|
7
|
+
* REFERENCES the way a block composes atoms — never a raw sentence of its
|
|
8
|
+
* own. Terms ≈ tokens, entries ≈ atoms, passages ≈ blocks, documents (a
|
|
9
|
+
* later, composition-layer concern, out of scope here — see the issue)
|
|
10
|
+
* ≈ views.
|
|
11
|
+
*
|
|
12
|
+
* WHERE THE MIRROR STOPS. This file deliberately does NOT port
|
|
13
|
+
* `@clossys/designer/tokens`' `brandable` boolean onto a `Passage`
|
|
14
|
+
* field. In tokens, `brandable` marks a subset WITHIN one namespace (154
|
|
15
|
+
* ship, 42 are brandable). The voice record's own consumer/machinery
|
|
16
|
+
* split runs between FILES instead (a consumer's own `VoiceRecord` vs.
|
|
17
|
+
* this package's shipped machinery) — forcing a boolean into a field here
|
|
18
|
+
* would be false symmetry with a split that does not exist at this
|
|
19
|
+
* layer. This file mirrors the LADDER (structure: terms/entries/passages/
|
|
20
|
+
* documents), not the binding mechanism.
|
|
21
|
+
*
|
|
22
|
+
* ============================================================================
|
|
23
|
+
* WHAT A `Passage` IS
|
|
24
|
+
* ============================================================================
|
|
25
|
+
*
|
|
26
|
+
* One reusable, addressable unit of composed language — an empty-state
|
|
27
|
+
* (title + body + action), an FAQ item (question + answer), an error
|
|
28
|
+
* (message + recovery) — never a single string, and never real prose of
|
|
29
|
+
* its own. `fields` is a plain `Record<string, unknown>` AT THE TYPE
|
|
30
|
+
* LEVEL, deliberately: a field's value is exactly what
|
|
31
|
+
* `classifyPassageField`/`checkPassageComposition` below classify at
|
|
32
|
+
* RUNTIME, not what a compile-time type could statically forbid a
|
|
33
|
+
* malformed registry file (real JSON on disk, not TypeScript-checked
|
|
34
|
+
* code) from containing. See "THE TERNARY" below for why a field holding
|
|
35
|
+
* a raw literal string must reach the GATE as a real, reportable
|
|
36
|
+
* violation rather than being rejected upstream as a schema error.
|
|
37
|
+
*
|
|
38
|
+
* This file ships no example content of anyone's real copy — the same
|
|
39
|
+
* "vocabulary layer, never a source of real words" constraint `types.ts`
|
|
40
|
+
* documents for `CopyEntry`/`CopyRecord`. Every fixture in this package's
|
|
41
|
+
* own tests is structural placeholder text, never something a product
|
|
42
|
+
* would actually show a user.
|
|
43
|
+
*
|
|
44
|
+
* ============================================================================
|
|
45
|
+
* THE GATE, WITH THE TERNARY (issue #373)
|
|
46
|
+
* ============================================================================
|
|
47
|
+
*
|
|
48
|
+
* 0 — SATISFIED: every passage references only entries and terms, and
|
|
49
|
+
* at least one passage was evaluated.
|
|
50
|
+
* 1 — VIOLATED: a passage inlines a literal string instead of
|
|
51
|
+
* referencing an entry (the verbal equivalent of a hardcoded color
|
|
52
|
+
* instead of a token), OR a passage references another passage's
|
|
53
|
+
* OWN INTERNALS (`{ ref: "passage", id, field }`) rather than
|
|
54
|
+
* composing entries/terms the way a block composes atoms, never
|
|
55
|
+
* reaching directly into a sibling block's own implementation.
|
|
56
|
+
* 2 — INDETERMINATE: the registry could not be read/parsed/validated,
|
|
57
|
+
* zero passages were evaluated (an empty `passages: []`), or a
|
|
58
|
+
* field's value could not be confidently classified as any of the
|
|
59
|
+
* above (not a string, not a recognized `{ ref: ... }` shape).
|
|
60
|
+
*
|
|
61
|
+
* PRECEDENCE — a real violation outranks an incomplete picture. This
|
|
62
|
+
* package already settled this exact question for a different gate
|
|
63
|
+
* (`addressability.ts`, issue #407/#433): letting "at least one
|
|
64
|
+
* unclassifiable field" outrank a confirmed violation would make the
|
|
65
|
+
* `"violated"` branch practically unreachable the moment a registry ever
|
|
66
|
+
* mixes a genuinely new/unknown field shape in with a real inlined
|
|
67
|
+
* literal — the identical collapse #407 fixed for addressability is fixed
|
|
68
|
+
* here from the start, not discovered later. `checkPassageComposition`
|
|
69
|
+
* applies the same "violated wins" ordering `checkAddressability` does.
|
|
70
|
+
*
|
|
71
|
+
* ============================================================================
|
|
72
|
+
* THE ADVERSARIAL SEPARATION (the weaker tool this gate must beat)
|
|
73
|
+
* ============================================================================
|
|
74
|
+
*
|
|
75
|
+
* A weaker tool that only checks "every referenced entry id exists"
|
|
76
|
+
* passes a passage built ENTIRELY from inline literals, because such a
|
|
77
|
+
* passage has ZERO references for that tool to check — nothing to
|
|
78
|
+
* validate is indistinguishable, to that tool, from everything being
|
|
79
|
+
* valid. The separating fixture: a passage with zero references and real
|
|
80
|
+
* prose sitting directly in its fields. The reference-checker reports
|
|
81
|
+
* clean (0 references, 0 problems). This gate must exit 1 — see
|
|
82
|
+
* `passage.adversarial.test.ts`, which runs both tools against the
|
|
83
|
+
* identical fixture in the same test, plus the sanity check that the weak
|
|
84
|
+
* tool is not simply broken (it correctly reports 0 when references truly
|
|
85
|
+
* do all exist).
|
|
86
|
+
*
|
|
87
|
+
* ============================================================================
|
|
88
|
+
* WHAT THIS GATE DELIBERATELY DOES NOT DO
|
|
89
|
+
* ============================================================================
|
|
90
|
+
*
|
|
91
|
+
* - It does not verify a referenced entry id or term actually EXISTS in
|
|
92
|
+
* a real `CopyRecord`/glossary. That is a DIFFERENT, weaker question
|
|
93
|
+
* ("does this id resolve") than the one this gate answers ("is this
|
|
94
|
+
* field a reference at all, or a smuggled-in literal") — the same
|
|
95
|
+
* split `addressability.ts` draws from `copy-gate.ts`'s traceability
|
|
96
|
+
* check (see that file's own top doc comment: "this gate has no
|
|
97
|
+
* `CopyRecord` input at all"). A gate with visibility into both a
|
|
98
|
+
* `PassageRecord` and a real `CopyRecord`/glossary to check
|
|
99
|
+
* referential integrity is a different, later gate — precisely the
|
|
100
|
+
* "weaker tool" this file's own adversarial proof names and beats on
|
|
101
|
+
* the property that actually matters here: composition purity.
|
|
102
|
+
* - It does not resolve/render a passage into text. Rendering a
|
|
103
|
+
* `Passage` against a real `CopyRecord`/`VoiceRecord` glossary is a
|
|
104
|
+
* consumer-owned, later concern — this file only ever validates
|
|
105
|
+
* SHAPE and COMPOSITION PURITY.
|
|
106
|
+
*/
|
|
107
|
+
import { readFileSync } from "node:fs";
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Shape validation
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
function isPlainObject(value) {
|
|
112
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
113
|
+
}
|
|
114
|
+
function isNonEmptyString(value) {
|
|
115
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
116
|
+
}
|
|
117
|
+
/** Same shape `COPY_ENTRY_ID_RE` (`schema.ts`) checks a `CopyEntryId` against — see `PassageId`'s own doc comment for why this is redefined locally rather than imported. */
|
|
118
|
+
const PASSAGE_ID_RE = /^[a-z0-9]+(-[a-z0-9]+)*(\.[a-z0-9]+(-[a-z0-9]+)*)+$/;
|
|
119
|
+
/**
|
|
120
|
+
* Validates one `Passage`'s SHAPE only: `id` present/well-formed,
|
|
121
|
+
* `context` present, `fields` a non-empty object. Deliberately does NOT
|
|
122
|
+
* validate each field VALUE's shape — see this file's top doc comment,
|
|
123
|
+
* "THE GATE": rejecting a field that holds a raw literal string here, as
|
|
124
|
+
* a schema error, would turn every real violation `checkPassageComposition`
|
|
125
|
+
* exists to catch into an "indeterminate" (invalid-schema) read instead
|
|
126
|
+
* of a "violated" one, the exact collapse #407/#433 already fixed for a
|
|
127
|
+
* different gate in this same package.
|
|
128
|
+
*/
|
|
129
|
+
function validatePassageShape(value, path) {
|
|
130
|
+
if (!isPlainObject(value)) {
|
|
131
|
+
return [{ rule: "passage-shape", severity: "error", message: `${path} must be an object.`, path }];
|
|
132
|
+
}
|
|
133
|
+
// Snapshot every field this function reads, exactly once, before any
|
|
134
|
+
// check runs — the same defensive discipline `schema.ts`'s
|
|
135
|
+
// `validateCopyEntryShape` documents (a hostile/badly-behaved `value`
|
|
136
|
+
// could otherwise return a different result on each property read).
|
|
137
|
+
const id = value.id;
|
|
138
|
+
const context = value.context;
|
|
139
|
+
const fields = value.fields;
|
|
140
|
+
const findings = [];
|
|
141
|
+
if (!isNonEmptyString(id)) {
|
|
142
|
+
findings.push({
|
|
143
|
+
rule: "id-shape",
|
|
144
|
+
severity: "error",
|
|
145
|
+
message: `${path}.id must be a non-empty string, got ${JSON.stringify(id)}.`,
|
|
146
|
+
path: `${path}.id`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else if (!PASSAGE_ID_RE.test(id)) {
|
|
150
|
+
findings.push({
|
|
151
|
+
rule: "id-well-formed",
|
|
152
|
+
severity: "error",
|
|
153
|
+
message: `${path}.id "${id}" must be dot-separated and lowercase, with at least one dot (e.g. "onboarding.empty-state"), got ${JSON.stringify(id)}.`,
|
|
154
|
+
path: `${path}.id`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (!isNonEmptyString(context)) {
|
|
158
|
+
findings.push({
|
|
159
|
+
rule: "context-shape",
|
|
160
|
+
severity: "error",
|
|
161
|
+
message: `${path}.context must be a non-empty string, got ${JSON.stringify(context)}. A passage with no stated location cannot be reviewed.`,
|
|
162
|
+
path: `${path}.context`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (!isPlainObject(fields)) {
|
|
166
|
+
findings.push({
|
|
167
|
+
rule: "fields-shape",
|
|
168
|
+
severity: "error",
|
|
169
|
+
message: `${path}.fields must be an object, got ${JSON.stringify(fields)}.`,
|
|
170
|
+
path: `${path}.fields`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else if (Object.keys(fields).length === 0) {
|
|
174
|
+
findings.push({
|
|
175
|
+
rule: "fields-non-empty",
|
|
176
|
+
severity: "error",
|
|
177
|
+
message: `${path}.fields must have at least one field — a passage with zero fields composes nothing.`,
|
|
178
|
+
path: `${path}.fields`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return findings;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Validates that `value` conforms to the `PassageRecord` shape. Returns a
|
|
185
|
+
* `PassageFinding[]`; empty means `value` is a well-formed `PassageRecord`.
|
|
186
|
+
* Never throws — mirrors `schema.ts`'s `validateCopyRecordShape` exactly:
|
|
187
|
+
* any input, including `null`/a string/a wildly malformed object,
|
|
188
|
+
* produces findings rather than an exception.
|
|
189
|
+
*/
|
|
190
|
+
export function validatePassageRecordShape(value) {
|
|
191
|
+
if (!isPlainObject(value)) {
|
|
192
|
+
return [{ rule: "record-present", severity: "error", message: "A passage record must be an object.", path: "$" }];
|
|
193
|
+
}
|
|
194
|
+
const id = value.id;
|
|
195
|
+
const passages = value.passages;
|
|
196
|
+
const findings = [];
|
|
197
|
+
if (!isNonEmptyString(id)) {
|
|
198
|
+
findings.push({
|
|
199
|
+
rule: "id-shape",
|
|
200
|
+
severity: "error",
|
|
201
|
+
message: `id must be a non-empty string, got ${JSON.stringify(id)}.`,
|
|
202
|
+
path: "id",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (!Array.isArray(passages)) {
|
|
206
|
+
findings.push({
|
|
207
|
+
rule: "passages-shape",
|
|
208
|
+
severity: "error",
|
|
209
|
+
message: `passages must be an array, got ${JSON.stringify(passages)}.`,
|
|
210
|
+
path: "passages",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
passages.forEach((passage, i) => findings.push(...validatePassageShape(passage, `passages.${i}`)));
|
|
215
|
+
// Uniqueness is a whole-record property, checked here rather than
|
|
216
|
+
// inside validatePassageShape — mirrors validateCopyRecordShape's own
|
|
217
|
+
// id-unique pass exactly, including only comparing passages whose own
|
|
218
|
+
// id already passed shape validation.
|
|
219
|
+
const seen = new Map();
|
|
220
|
+
passages.forEach((passage, i) => {
|
|
221
|
+
if (!isPlainObject(passage))
|
|
222
|
+
return;
|
|
223
|
+
const passageId = passage.id;
|
|
224
|
+
if (typeof passageId !== "string" || passageId.length === 0)
|
|
225
|
+
return;
|
|
226
|
+
const firstIndex = seen.get(passageId);
|
|
227
|
+
if (firstIndex === undefined) {
|
|
228
|
+
seen.set(passageId, i);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
findings.push({
|
|
232
|
+
rule: "id-unique",
|
|
233
|
+
severity: "error",
|
|
234
|
+
message: `passages.${i}.id "${passageId}" duplicates passages.${firstIndex}.id — every passage's id must be unique within a PassageRecord.`,
|
|
235
|
+
path: `passages.${i}.id`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return findings;
|
|
241
|
+
}
|
|
242
|
+
function buildPassageRecord(value) {
|
|
243
|
+
const passagesRaw = value.passages;
|
|
244
|
+
const built = passagesRaw.map((raw) => {
|
|
245
|
+
const passage = raw;
|
|
246
|
+
return {
|
|
247
|
+
id: passage.id,
|
|
248
|
+
context: passage.context,
|
|
249
|
+
fields: { ...passage.fields },
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
return {
|
|
253
|
+
id: value.id,
|
|
254
|
+
passages: built,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Parses `value` as a `PassageRecord`, throwing a plain `Error` (never a
|
|
259
|
+
* `PassageFinding[]`) if it does not conform — mirrors `schema.ts`'s
|
|
260
|
+
* `parseCopyRecord` exactly, including the two-pass validate-then-build
|
|
261
|
+
* shape and why (see that function's own doc comment for the fuller
|
|
262
|
+
* argument).
|
|
263
|
+
*/
|
|
264
|
+
export function parsePassageRecord(value) {
|
|
265
|
+
const findings = validatePassageRecordShape(value);
|
|
266
|
+
if (findings.length > 0) {
|
|
267
|
+
const detail = findings.map((f) => ` - ${f.path ?? "(root)"}: ${f.message}`).join("\n");
|
|
268
|
+
throw new Error(`parsePassageRecord: value is not a valid PassageRecord:\n${detail}`);
|
|
269
|
+
}
|
|
270
|
+
// Safe: validatePassageRecordShape returning no findings means every
|
|
271
|
+
// field buildPassageRecord reads below is present and correctly typed.
|
|
272
|
+
return buildPassageRecord(value);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Reads and validates the `PassageRecord` at `path`. Never throws: every
|
|
276
|
+
* failure — an unreadable file, invalid JSON, or a schema violation — is
|
|
277
|
+
* recorded into `issues` and reflected in `.complete`. Mirrors
|
|
278
|
+
* `registry.ts`'s `readCopyRecord` exactly, including the discipline it
|
|
279
|
+
* documents: an unreadable/invalid registry is "could not run" (fed into
|
|
280
|
+
* exit `2` by the CLI), never a clean pass produced from a record this
|
|
281
|
+
* function could not actually trust.
|
|
282
|
+
*/
|
|
283
|
+
export function readPassageRecord(path) {
|
|
284
|
+
let raw;
|
|
285
|
+
try {
|
|
286
|
+
raw = readFileSync(path, "utf8");
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
return {
|
|
290
|
+
path,
|
|
291
|
+
issues: [{ reason: "unreadable", detail: error instanceof Error ? error.message : String(error) }],
|
|
292
|
+
complete: false,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
let parsed;
|
|
296
|
+
try {
|
|
297
|
+
parsed = JSON.parse(raw);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
return {
|
|
301
|
+
path,
|
|
302
|
+
issues: [{ reason: "unparseable", detail: error instanceof Error ? error.message : String(error) }],
|
|
303
|
+
complete: false,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const record = parsePassageRecord(parsed);
|
|
308
|
+
return { path, record, issues: [], complete: true };
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
return {
|
|
312
|
+
path,
|
|
313
|
+
issues: [{ reason: "invalid-schema", detail: error instanceof Error ? error.message : String(error) }],
|
|
314
|
+
complete: false,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** A short, length-capped JSON preview of an arbitrary field value, for a finding's own message — never large enough to make a report unreadable. */
|
|
319
|
+
function safePreview(value) {
|
|
320
|
+
let text;
|
|
321
|
+
try {
|
|
322
|
+
text = JSON.stringify(value) ?? String(value);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
text = String(value);
|
|
326
|
+
}
|
|
327
|
+
return text.length > 120 ? `${text.slice(0, 120)}…` : text;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Classifies one `Passage` field's raw value into exactly one of the five
|
|
331
|
+
* `PassageFieldClassification` outcomes. Pure — no I/O, never throws on
|
|
332
|
+
* any input shape.
|
|
333
|
+
*
|
|
334
|
+
* - A plain string is ALWAYS `"inline-literal"` — real prose (or
|
|
335
|
+
* anything else) sitting directly in the field instead of a
|
|
336
|
+
* reference. This is the shape the adversarial proof's separating
|
|
337
|
+
* fixture is built from — see this file's top doc comment.
|
|
338
|
+
* - `{ ref: "entry", id: <non-empty string> }` is `"entry-reference"`.
|
|
339
|
+
* - `{ ref: "term", term: <non-empty string> }` is `"term-reference"`.
|
|
340
|
+
* - `{ ref: "passage", ... }` — REGARDLESS of what else it carries — is
|
|
341
|
+
* always `"passage-internals-reference"`: a passage may compose
|
|
342
|
+
* entries and terms, never reach directly into another passage's own
|
|
343
|
+
* fields. This is a VIOLATION, not something this function treats as
|
|
344
|
+
* merely unclassifiable, because the shape is unambiguous the moment
|
|
345
|
+
* `ref === "passage"` is seen.
|
|
346
|
+
* - `{ ref: "entry" | "term", ... }` with a missing/malformed
|
|
347
|
+
* `id`/`term` is `"unclassifiable"` — the AUTHOR'S intent (compose an
|
|
348
|
+
* entry or a term) is legible, but the reference itself is broken, so
|
|
349
|
+
* this function refuses to guess which finding shape to promote it
|
|
350
|
+
* to rather than silently treating a broken reference as either
|
|
351
|
+
* "fine" or "definitely a literal."
|
|
352
|
+
* - Anything else (a number, boolean, null, array, or an object with no
|
|
353
|
+
* recognized `ref`) is `"unclassifiable"`.
|
|
354
|
+
*/
|
|
355
|
+
export function classifyPassageField(value) {
|
|
356
|
+
if (typeof value === "string") {
|
|
357
|
+
return {
|
|
358
|
+
classification: "inline-literal",
|
|
359
|
+
detail: `field holds a literal string ${JSON.stringify(safePreview(value))} instead of a reference to a registered entry — the verbal equivalent of inlining a raw value instead of a token`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
if (isPlainObject(value)) {
|
|
363
|
+
const ref = value.ref;
|
|
364
|
+
if (ref === "entry") {
|
|
365
|
+
const id = value.id;
|
|
366
|
+
if (isNonEmptyString(id)) {
|
|
367
|
+
return { classification: "entry-reference", detail: `references entry "${id}"` };
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
classification: "unclassifiable",
|
|
371
|
+
detail: `{ ref: "entry" } is missing a valid non-empty "id" (got ${safePreview(id)})`,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (ref === "term") {
|
|
375
|
+
const term = value.term;
|
|
376
|
+
if (isNonEmptyString(term)) {
|
|
377
|
+
return { classification: "term-reference", detail: `references term "${term}"` };
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
classification: "unclassifiable",
|
|
381
|
+
detail: `{ ref: "term" } is missing a valid non-empty "term" (got ${safePreview(term)})`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (ref === "passage") {
|
|
385
|
+
const id = typeof value.id === "string" && value.id.length > 0 ? value.id : "?";
|
|
386
|
+
const field = typeof value.field === "string" && value.field.length > 0 ? value.field : "?";
|
|
387
|
+
return {
|
|
388
|
+
classification: "passage-internals-reference",
|
|
389
|
+
detail: `reaches directly into passage "${id}"'s own field "${field}" instead of referencing an entry or a term — a passage may compose entries and terms, never another passage's internals`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
classification: "unclassifiable",
|
|
395
|
+
detail: `field value ${safePreview(value)} is neither a literal string, an { ref: "entry" }/{ ref: "term" } reference, nor an { ref: "passage" } internals reference — cannot be classified`,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Pure — takes an already-parsed `PassageRecord` (see `readPassageRecord`/
|
|
400
|
+
* `parsePassageRecord`), classifies every field of every passage, and
|
|
401
|
+
* decides the ternary verdict. Never throws, does no I/O.
|
|
402
|
+
*
|
|
403
|
+
* PRECEDENCE: a real violation wins over an incomplete picture — see this
|
|
404
|
+
* file's top doc comment, "THE TERNARY", for why this mirrors
|
|
405
|
+
* `checkAddressability`'s own post-#407 ordering rather than the older
|
|
406
|
+
* "indeterminate wins" precedence `checkCopyTraceability`/`scan.ts` still
|
|
407
|
+
* use for a different reason (see `checkAddressability`'s own doc comment
|
|
408
|
+
* for the fuller argument against letting a permanent-in-practice
|
|
409
|
+
* coverage gap make a real violation unreachable).
|
|
410
|
+
*/
|
|
411
|
+
export function checkPassageComposition(record) {
|
|
412
|
+
const violations = [];
|
|
413
|
+
const unclassified = [];
|
|
414
|
+
let passagesEvaluated = 0;
|
|
415
|
+
for (const passage of record.passages) {
|
|
416
|
+
const fieldNames = Object.keys(passage.fields);
|
|
417
|
+
if (fieldNames.length === 0)
|
|
418
|
+
continue; // defensive only — validatePassageRecordShape already requires >= 1 field
|
|
419
|
+
passagesEvaluated++;
|
|
420
|
+
for (const field of fieldNames) {
|
|
421
|
+
const value = passage.fields[field];
|
|
422
|
+
const { classification, detail } = classifyPassageField(value);
|
|
423
|
+
if (classification === "inline-literal") {
|
|
424
|
+
violations.push({
|
|
425
|
+
passageId: passage.id,
|
|
426
|
+
field,
|
|
427
|
+
rule: "field-inlines-literal",
|
|
428
|
+
message: `${passage.id}.${field}: ${detail}`,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
else if (classification === "passage-internals-reference") {
|
|
432
|
+
violations.push({
|
|
433
|
+
passageId: passage.id,
|
|
434
|
+
field,
|
|
435
|
+
rule: "field-references-passage-internals",
|
|
436
|
+
message: `${passage.id}.${field}: ${detail}`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
else if (classification === "unclassifiable") {
|
|
440
|
+
unclassified.push({ passageId: passage.id, field, detail: `${passage.id}.${field}: ${detail}` });
|
|
441
|
+
}
|
|
442
|
+
// "entry-reference" / "term-reference": exactly the property this gate requires — no finding.
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const reasons = [];
|
|
446
|
+
if (passagesEvaluated === 0) {
|
|
447
|
+
reasons.push(record.passages.length === 0 ? "no passages are registered" : "no passage had at least one field to evaluate");
|
|
448
|
+
}
|
|
449
|
+
if (unclassified.length > 0) {
|
|
450
|
+
reasons.push(`${unclassified.length} field(s) could not be confidently classified`);
|
|
451
|
+
}
|
|
452
|
+
const base = { violations, unclassified, passagesEvaluated };
|
|
453
|
+
// A real violation wins over an incomplete picture — see this file's
|
|
454
|
+
// top doc comment, "THE TERNARY". The coverage gap is not hidden:
|
|
455
|
+
// `reasons`/`unclassified` are still populated and still printed by
|
|
456
|
+
// `cli.ts`'s `printPassagesReport` regardless of verdict.
|
|
457
|
+
if (violations.length > 0)
|
|
458
|
+
return { verdict: "violated", reasons, ...base };
|
|
459
|
+
if (reasons.length > 0)
|
|
460
|
+
return { verdict: "indeterminate", reasons, ...base };
|
|
461
|
+
return { verdict: "satisfied", reasons, ...base };
|
|
462
|
+
}
|
|
463
|
+
//# sourceMappingURL=passage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passage.js","sourceRoot":"","sources":["../src/passage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path exclusions for `scan.ts`'s SCANNING SURFACE — `scanCopySourceTree`'s
|
|
3
|
+
* directory walk — never to be confused with `scan.ts`'s existing
|
|
4
|
+
* `ExclusionReason`/`ExcludedLiteral`, a DIFFERENT feature this file does
|
|
5
|
+
* not touch. Read `scan.ts`'s own top doc comment ("THE BOUNDARY") before
|
|
6
|
+
* this one: that mechanism decides, for a LITERAL already found inside a
|
|
7
|
+
* file that IS being scanned, whether that one literal is real copy or
|
|
8
|
+
* something structural (an import specifier, a CSS class, an aria label,
|
|
9
|
+
* ...) — a per-LITERAL classification made from the few dozen characters of
|
|
10
|
+
* context immediately around it. This file answers a completely different
|
|
11
|
+
* question, at a completely different granularity: should this FILE be
|
|
12
|
+
* looked at for copy candidates AT ALL, before a single character of it is
|
|
13
|
+
* ever tokenized? That is a per-FILE scope decision, made once, from the
|
|
14
|
+
* file's own path alone — extending `ExclusionReason` to cover it would be
|
|
15
|
+
* a coincidence of both being named "exclusion", not a shared mechanism;
|
|
16
|
+
* this is why it is a new, separate type (`PathExclusion`) and a new,
|
|
17
|
+
* separate `ScanResult` field (`excludedFiles`), not a tenth
|
|
18
|
+
* `ExclusionReason`.
|
|
19
|
+
*
|
|
20
|
+
* THE MOTIVATING CASE ("mention vs. use"): a style guide or README that
|
|
21
|
+
* documents this voice's own banned terms — "never say X, say Y instead" —
|
|
22
|
+
* necessarily CONTAINS the literal banned text, as a MENTION, not a USE.
|
|
23
|
+
* Scanned like any other file, that mention looks identical to real,
|
|
24
|
+
* unregistered product copy to `checkCopyTraceability` (`copy-gate.ts`):
|
|
25
|
+
* the traceability gate has no notion of "this file's job is to talk about
|
|
26
|
+
* copy, not ship it". A `PathExclusion` on that one file's path is the fix
|
|
27
|
+
* — the file is never tokenized at all, so it can never produce a
|
|
28
|
+
* candidate, `unregistered-copy` finding, or citation. See this file's
|
|
29
|
+
* README section, "Path exclusions vs. per-literal exclusions", for the
|
|
30
|
+
* fuller argument, including why this stays scoped to `scan.ts`'s existing
|
|
31
|
+
* scanning surface (copy TRACEABILITY) rather than inventing a new
|
|
32
|
+
* directory-scanning feature for VOICE/glossary rules, which this package
|
|
33
|
+
* does not otherwise have — a consumer building their own "walk a
|
|
34
|
+
* directory, then run each file's text through `checkCopy`" tool gets the
|
|
35
|
+
* identical mention-vs-use protection by reusing THIS mechanism (call
|
|
36
|
+
* `scanCopySourceTree` with `pathExclusions`, then feed its candidates'
|
|
37
|
+
* text through `checkCopy` themselves) rather than this package growing a
|
|
38
|
+
* second, redundant exclusion list.
|
|
39
|
+
*
|
|
40
|
+
* FAILS CLOSED, mirroring `checker.ts`'s `VoiceCheckWaiver` handling (the
|
|
41
|
+
* established precedent for "a caller-supplied list of exceptions" in this
|
|
42
|
+
* package's ecosystem) almost exactly:
|
|
43
|
+
*
|
|
44
|
+
* - A MALFORMED entry (not an object, `path`/`reason` missing or not a
|
|
45
|
+
* non-empty string, or a `path` this file's own small pattern language
|
|
46
|
+
* cannot parse — see `classifyPathExclusionPattern`) is never applied:
|
|
47
|
+
* it exempts NOTHING, and is reported as its own `"error"`-severity
|
|
48
|
+
* `PathExclusionFinding` — the exact "a malformed exclusion entry is a
|
|
49
|
+
* finding, never a silent exemption" requirement. This mirrors
|
|
50
|
+
* `"waiver:invalid"`.
|
|
51
|
+
* - An exclusion that never matched a single scanned file is dead
|
|
52
|
+
* configuration — reported as its own `"warning"`-severity
|
|
53
|
+
* `PathExclusionFinding`, mirroring `"waiver:unused"`, so a stale
|
|
54
|
+
* exclusion (the file it named was renamed or deleted) cannot silently
|
|
55
|
+
* sit in a config forever looking like it is still doing something.
|
|
56
|
+
*/
|
|
57
|
+
export interface PathExclusion {
|
|
58
|
+
/**
|
|
59
|
+
* A repo-relative, `/`-joined path pattern — see
|
|
60
|
+
* `classifyPathExclusionPattern`'s doc comment for the exact, small,
|
|
61
|
+
* hand-rolled pattern language this supports (no glob library — this
|
|
62
|
+
* package's zero-runtime-dependency rule applies here too).
|
|
63
|
+
*/
|
|
64
|
+
path: string;
|
|
65
|
+
/** Required — an exclusion with no stated reason is not auditable, the same discipline `GlossaryEntry.reason` and `VoiceCheckWaiver.reason` already hold every other exception in this package's ecosystem to. */
|
|
66
|
+
reason: string;
|
|
67
|
+
}
|
|
68
|
+
/** One file `scanCopySourceTree` skipped entirely because its path matched a `PathExclusion` — never tokenized, contributes zero candidates/excluded-literals/citations/unchecked-items. */
|
|
69
|
+
export interface ExcludedPath {
|
|
70
|
+
file: string;
|
|
71
|
+
reason: string;
|
|
72
|
+
/** The `PathExclusion.path` pattern that matched, for audit. */
|
|
73
|
+
pattern: string;
|
|
74
|
+
}
|
|
75
|
+
export type PathExclusionFindingRule = "path-exclusion-invalid" | "path-exclusion-unused";
|
|
76
|
+
/** One problem with the `pathExclusions` CONFIGURATION itself — never about a scanned file's content. See this file's top doc comment for the fail-closed contract both rules implement. */
|
|
77
|
+
export interface PathExclusionFinding {
|
|
78
|
+
rule: PathExclusionFindingRule;
|
|
79
|
+
/** `"error"` for a malformed entry (never applied); `"warning"` for a well-formed entry that matched nothing this run (applied as designed, just possibly stale). */
|
|
80
|
+
severity: "error" | "warning";
|
|
81
|
+
message: string;
|
|
82
|
+
/** The exclusion's own `path` pattern, when there is one to point at (absent only for a top-level shape error, e.g. `pathExclusions` itself not being an array). */
|
|
83
|
+
path?: string;
|
|
84
|
+
}
|
|
85
|
+
/** One validated `PathExclusion`, tagged with which of this file's three supported pattern shapes it is — computed once by `validatePathExclusions` so `matchesPathExclusion` never has to re-derive it (and never has to re-validate what was already validated). */
|
|
86
|
+
export interface ValidatedPathExclusion extends PathExclusion {
|
|
87
|
+
kind: "exact" | "subtree" | "segment-wildcard";
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Matches `relPath` (already `/`-joined, repo-relative — the same shape
|
|
91
|
+
* `scanCopySourceTree` produces for every `CopyCandidate.file`) against one
|
|
92
|
+
* already-classified exclusion. Never called with a pattern that failed
|
|
93
|
+
* `classifyPathExclusionPattern` — `validatePathExclusions` filters those
|
|
94
|
+
* out before anything reaches here, so this function does not re-validate.
|
|
95
|
+
*/
|
|
96
|
+
export declare function matchesPathExclusion(relPath: string, exclusion: ValidatedPathExclusion): boolean;
|
|
97
|
+
export interface PathExclusionValidation {
|
|
98
|
+
/** Every exclusion that passed shape+pattern validation — safe to match files against. */
|
|
99
|
+
valid: ValidatedPathExclusion[];
|
|
100
|
+
/** Malformed entries, as `"error"`-severity findings. Never includes `"path-exclusion-unused"` — that rule is only ever added later, once the walk has actually run; see `scanCopySourceTree`. */
|
|
101
|
+
findings: PathExclusionFinding[];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Validates `value` (`ScanOptions.pathExclusions`, or any raw candidate list
|
|
105
|
+
* — this is called on a TYPED `PathExclusion[]` from a TypeScript caller,
|
|
106
|
+
* but re-validates every field anyway, the same defense-in-depth this
|
|
107
|
+
* repository's other schema functions apply to already-typed input: see
|
|
108
|
+
* `@clossys/writer/voice`'s `schema.ts` header comment for why a
|
|
109
|
+
* plain-JS caller, or a value that merely satisfies the type at compile
|
|
110
|
+
* time without conforming at runtime, is exactly the case this guards
|
|
111
|
+
* against). Never throws.
|
|
112
|
+
*/
|
|
113
|
+
export declare function validatePathExclusions(value: unknown): PathExclusionValidation;
|
|
114
|
+
//# sourceMappingURL=path-exclusions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-exclusions.d.ts","sourceRoot":"","sources":["../src/path-exclusions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,kNAAkN;IAClN,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,4LAA4L;AAC5L,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,wBAAwB,GAAG,wBAAwB,GAAG,uBAAuB,CAAC;AAE1F,4LAA4L;AAC5L,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,wBAAwB,CAAC;IAC/B,qKAAqK;IACrK,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,oKAAoK;IACpK,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,sQAAsQ;AACtQ,MAAM,WAAW,sBAAuB,SAAQ,aAAa;IAC3D,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,kBAAkB,CAAC;CAChD;AA4DD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,sBAAsB,GAAG,OAAO,CAuBhG;AAED,MAAM,WAAW,uBAAuB;IACtC,0FAA0F;IAC1F,KAAK,EAAE,sBAAsB,EAAE,CAAC;IAChC,kMAAkM;IAClM,QAAQ,EAAE,oBAAoB,EAAE,CAAC;CAClC;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,CA+D9E"}
|