@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/src/checker.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `checkCopyRecord` — runs every entry in a `CopyRecord` through
|
|
3
|
+
* `@clossys/writer/voice`'s own `checkCopy`, against a single
|
|
4
|
+
* `VoiceRecord`, and reports the result. This is the one file in this
|
|
5
|
+
* package that imports `@clossys/writer/voice`: `types.ts` and
|
|
6
|
+
* `schema.ts` are pure, dependency-free data/validation, exactly like
|
|
7
|
+
* `voice`'s own `types.ts`/`schema.ts`; this file is where the two
|
|
8
|
+
* packages' machinery actually meet.
|
|
9
|
+
*
|
|
10
|
+
* FIVE AGENTS FINISHED A REPAIR ROUND ON THIS REPOSITORY AN HOUR BEFORE
|
|
11
|
+
* THIS FILE WAS WRITTEN, AND EVERY ONE OF THEM SHIPPED A FIRST DRAFT THAT
|
|
12
|
+
* REPRODUCED THE EXACT DEFECT IT WAS FIXING. The recurring shape was "a
|
|
13
|
+
* check that passes while asserting nothing" — `checkCopyRecord` IS a
|
|
14
|
+
* check, so this file is written against that failure mode directly, not
|
|
15
|
+
* as an afterthought:
|
|
16
|
+
*
|
|
17
|
+
* 1. NO SILENT NARROWING. If an entry is not run through `checkCopy`
|
|
18
|
+
* for any reason, it is recorded in `report.skipped`, by id — never
|
|
19
|
+
* folded silently into a "0 findings" result that looks identical
|
|
20
|
+
* to "everything ran and was clean". `report.checkedCount` and
|
|
21
|
+
* `report.skippedCount` are always populated (not just inferable
|
|
22
|
+
* from array lengths), so a caller reading only those two numbers,
|
|
23
|
+
* without inspecting either array, still cannot mistake one for the
|
|
24
|
+
* other.
|
|
25
|
+
* 2. FAILS CLOSED. An invalid `CopyRecord`, an invalid `VoiceRecord`,
|
|
26
|
+
* or a `CopyRecord` with zero entries are each treated as a
|
|
27
|
+
* FAILURE — `report.complete` is `false` and `report.findings`
|
|
28
|
+
* names the reason — never as a clean pass with nothing to check.
|
|
29
|
+
* `checkCopyRecord` re-validates BOTH its arguments itself
|
|
30
|
+
* (`validateCopyRecordShape` and, imported from `voice`,
|
|
31
|
+
* `validateVoiceRecordShape`), even though both are typed inputs a
|
|
32
|
+
* TypeScript caller "should" have already validated — because a
|
|
33
|
+
* plain-JS caller, or a value that merely satisfies the type at
|
|
34
|
+
* compile time without actually conforming at runtime, is exactly
|
|
35
|
+
* the case a check that "passes while asserting nothing" would fail
|
|
36
|
+
* to catch. This is a deliberate difference from `voice`'s own
|
|
37
|
+
* `checkCopy`, which does NOT self-validate its `VoiceRecord`
|
|
38
|
+
* argument (see that function's own doc comment) — this package
|
|
39
|
+
* chooses the stricter behavior for its own entry point because it
|
|
40
|
+
* is the thing this whole package's design brief was written to
|
|
41
|
+
* distrust in itself.
|
|
42
|
+
* 3. `report.complete` MEANS "did this run check everything it could
|
|
43
|
+
* have", never "did everything pass" — the same non-negotiable
|
|
44
|
+
* distinction `voice`'s own `VoiceCheckReport.complete` draws. A
|
|
45
|
+
* `CopyRecord` with real voice violations in it can still be
|
|
46
|
+
* `complete: true` (every entry got checked; some of them failed);
|
|
47
|
+
* a `CopyRecord` this function refused to check at all is always
|
|
48
|
+
* `complete: false`, no matter how "clean" an empty `findings`
|
|
49
|
+
* array might otherwise look.
|
|
50
|
+
*
|
|
51
|
+
* Pure, no I/O: `checkCopyRecord` never reads a file — that is
|
|
52
|
+
* `registry.ts`'s job — and never resolves a `factRef` against anything,
|
|
53
|
+
* for the same reason `voice`'s `checkCopy` does not: see
|
|
54
|
+
* `@clossys/writer/voice`'s README, "The `factRef` seam".
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import {
|
|
58
|
+
checkCopy,
|
|
59
|
+
validateVoiceRecordShape,
|
|
60
|
+
type VoiceCheckReport,
|
|
61
|
+
type VoiceCheckWaiver,
|
|
62
|
+
type VoiceFinding,
|
|
63
|
+
type VoiceRecord,
|
|
64
|
+
} from "./voice/index.js";
|
|
65
|
+
import { validateCopyRecordShape } from "./schema.js";
|
|
66
|
+
import type { CopyEntryId, CopyRecord } from "./types.js";
|
|
67
|
+
|
|
68
|
+
/** One entry `checkCopyRecord` did NOT run through `voice`'s `checkCopy` this run, and why. */
|
|
69
|
+
export interface CopyEntrySkip {
|
|
70
|
+
entryId: CopyEntryId;
|
|
71
|
+
/**
|
|
72
|
+
* Human-readable reason. In practice always because the `CopyRecord` or
|
|
73
|
+
* `VoiceRecord` given to this call failed its own shape validation —
|
|
74
|
+
* see `report.findings` for the specific violation(s). An entry that
|
|
75
|
+
* passes `validateCopyRecordShape` (id well-formed and unique, `text`
|
|
76
|
+
* and `context` non-empty, every declared placeholder present) is
|
|
77
|
+
* always run, never skipped for a reason internal to the entry itself;
|
|
78
|
+
* `voice`'s own `checkCopy` has its own, separately-reported notion of
|
|
79
|
+
* an incomplete run (see `CopyEntryCheckResult.report.skipped`) for a
|
|
80
|
+
* dimension it could not evaluate against a structurally valid entry.
|
|
81
|
+
*/
|
|
82
|
+
reason: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** One entry `checkCopyRecord` DID run through `voice`'s `checkCopy`, and the resulting report. */
|
|
86
|
+
export interface CopyEntryCheckResult {
|
|
87
|
+
entryId: CopyEntryId;
|
|
88
|
+
/**
|
|
89
|
+
* `voice`'s own `checkCopy` report for this entry's `text`, unmodified.
|
|
90
|
+
* Its own `.complete`/`.skipped` describe whether `checkCopy` itself
|
|
91
|
+
* could evaluate every dimension against this ONE entry — a separate
|
|
92
|
+
* question from whether `checkCopyRecord` ran this entry at all.
|
|
93
|
+
*/
|
|
94
|
+
report: VoiceCheckReport;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** One `VoiceFinding` from one entry's `checkCopy` run, flattened into `report.findings` and tagged with the entry it came from. */
|
|
98
|
+
export interface CopyRecordFinding extends VoiceFinding {
|
|
99
|
+
/** The entry this finding is about, or `undefined` for a record-level finding not specific to any one entry (an invalid `CopyRecord`/`VoiceRecord`, or zero entries). */
|
|
100
|
+
entryId?: CopyEntryId;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A `CopyRecordFinding` that was waived, with the waiver that covered it — mirrors `voice`'s `WaivedVoiceFinding`. */
|
|
104
|
+
export interface CopyRecordWaivedFinding extends CopyRecordFinding {
|
|
105
|
+
waiver: VoiceCheckWaiver;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface CopyRecordCheckOptions {
|
|
109
|
+
/**
|
|
110
|
+
* Waivers applied uniformly to EVERY entry's `checkCopy` call — the
|
|
111
|
+
* same `VoiceCheckWaiver[]` shape `voice`'s own `checkCopy` accepts.
|
|
112
|
+
* There is no per-entry waiver targeting in this version: a waiver
|
|
113
|
+
* scoped to one entry's finding still matches the same `rule`+`path`
|
|
114
|
+
* pair in every other entry's report too. A caller that genuinely needs
|
|
115
|
+
* per-entry waiver scoping can call `voice`'s `checkCopy` directly,
|
|
116
|
+
* per entry, with its own waiver list — this package's own `checker.ts`
|
|
117
|
+
* does exactly that under the hood.
|
|
118
|
+
*/
|
|
119
|
+
waivers?: VoiceCheckWaiver[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface CopyRecordCheckReport {
|
|
123
|
+
recordId: string;
|
|
124
|
+
/** Every entry actually run through `checkCopy`, with its own `VoiceCheckReport`. */
|
|
125
|
+
checked: CopyEntryCheckResult[];
|
|
126
|
+
/** Every entry NOT run, by id, and why. Empty does not by itself mean "nothing was skipped for a bad reason" — see `report.complete`, which is `false` whenever this run could not be trusted enough to check every entry, including the cases where this is legitimately `[]` (zero entries; see `checkedCount`). */
|
|
127
|
+
skipped: CopyEntrySkip[];
|
|
128
|
+
/** `checked.length`, restated as its own field so a caller does not have to inspect the array to get this number — see this file's top-of-file "no silent narrowing" note. */
|
|
129
|
+
checkedCount: number;
|
|
130
|
+
/** `skipped.length`, restated as its own field for the same reason as `checkedCount`. */
|
|
131
|
+
skippedCount: number;
|
|
132
|
+
/**
|
|
133
|
+
* Every non-waived finding this run produced, flattened across every
|
|
134
|
+
* checked entry AND any record-level problem (an invalid `CopyRecord`,
|
|
135
|
+
* an invalid `VoiceRecord`, or zero entries), each entry-sourced
|
|
136
|
+
* finding tagged with the `entryId` it came from. This is the one field
|
|
137
|
+
* a caller checking "is anything wrong" should read — it is populated
|
|
138
|
+
* in every failure case this function knows about, never left empty to
|
|
139
|
+
* imply a clean run when the run in fact could not be trusted (see
|
|
140
|
+
* `.complete`).
|
|
141
|
+
*/
|
|
142
|
+
findings: CopyRecordFinding[];
|
|
143
|
+
/** Every finding covered by a waiver, across every checked entry, each with the waiver that covered it — mirrors `voice`'s `VoiceCheckReport.waived`. */
|
|
144
|
+
waived: CopyRecordWaivedFinding[];
|
|
145
|
+
/**
|
|
146
|
+
* `true` exactly when: the given `CopyRecord` was structurally valid,
|
|
147
|
+
* the given `VoiceRecord` was structurally valid, the record had at
|
|
148
|
+
* least one entry, and every entry was actually run (`skipped` is
|
|
149
|
+
* empty). `false` in every other case. Independent of `findings` —
|
|
150
|
+
* see this file's top-of-file note #3. Read this field, not an empty
|
|
151
|
+
* `findings` array, to decide whether a run can be trusted as "checked
|
|
152
|
+
* everything, found nothing".
|
|
153
|
+
*/
|
|
154
|
+
complete: boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function recordLevelFailure(
|
|
158
|
+
recordId: string,
|
|
159
|
+
findings: CopyRecordFinding[],
|
|
160
|
+
skipped: CopyEntrySkip[],
|
|
161
|
+
): CopyRecordCheckReport {
|
|
162
|
+
return {
|
|
163
|
+
recordId,
|
|
164
|
+
checked: [],
|
|
165
|
+
skipped,
|
|
166
|
+
checkedCount: 0,
|
|
167
|
+
skippedCount: skipped.length,
|
|
168
|
+
findings,
|
|
169
|
+
waived: [],
|
|
170
|
+
complete: false,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Best-effort per-entry skip attribution for a `CopyRecord` whose own
|
|
176
|
+
* shape is invalid. `record.entries` is typed as `CopyEntry[]`, but the
|
|
177
|
+
* whole point of the branch that calls this is that the type cannot be
|
|
178
|
+
* trusted at runtime — so this reads defensively, using whatever id it
|
|
179
|
+
* can find and falling back to a positional label (`"$3"`, meaning index
|
|
180
|
+
* 3) for an entry too malformed to have a usable id at all. Never throws.
|
|
181
|
+
*/
|
|
182
|
+
function bestEffortSkipList(entriesValue: unknown, reason: string): CopyEntrySkip[] {
|
|
183
|
+
if (!Array.isArray(entriesValue)) return [];
|
|
184
|
+
return entriesValue.map((entry, i) => {
|
|
185
|
+
const hasUsableId =
|
|
186
|
+
typeof entry === "object" &&
|
|
187
|
+
entry !== null &&
|
|
188
|
+
!Array.isArray(entry) &&
|
|
189
|
+
typeof (entry as Record<string, unknown>).id === "string" &&
|
|
190
|
+
((entry as Record<string, unknown>).id as string).length > 0;
|
|
191
|
+
const entryId = hasUsableId ? ((entry as Record<string, unknown>).id as string) : `$${i}`;
|
|
192
|
+
return { entryId, reason };
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Checks every entry in `record` against `voiceRecord`, via `voice`'s own
|
|
198
|
+
* `checkCopy`. See this file's top-of-file doc comment for the fail-
|
|
199
|
+
* closed behavior on an invalid `record`, an invalid `voiceRecord`, and a
|
|
200
|
+
* `record` with zero entries.
|
|
201
|
+
*
|
|
202
|
+
* Throws a plain `Error` (not a finding) for caller-input problems that
|
|
203
|
+
* are not this function's job to interpret as data: `record` or
|
|
204
|
+
* `voiceRecord` not an object at all. This mirrors `voice`'s own
|
|
205
|
+
* `checkCopy`, which treats a malformed call the same way — a hard
|
|
206
|
+
* upfront rejection, not a soft finding that could be mistaken for
|
|
207
|
+
* something the checker discovered about the record itself. A `record`
|
|
208
|
+
* or `voiceRecord` that IS an object but fails shape validation is,
|
|
209
|
+
* unlike a non-object, a real finding (`report.findings`), not a thrown
|
|
210
|
+
* error — see note #2 above for why this function validates both itself
|
|
211
|
+
* rather than trusting its own type signature.
|
|
212
|
+
*/
|
|
213
|
+
export function checkCopyRecord(
|
|
214
|
+
record: CopyRecord,
|
|
215
|
+
voiceRecord: VoiceRecord,
|
|
216
|
+
options: CopyRecordCheckOptions = {},
|
|
217
|
+
): CopyRecordCheckReport {
|
|
218
|
+
if (record === null || typeof record !== "object") {
|
|
219
|
+
throw new TypeError("checkCopyRecord: record must be a CopyRecord object");
|
|
220
|
+
}
|
|
221
|
+
if (voiceRecord === null || typeof voiceRecord !== "object") {
|
|
222
|
+
throw new TypeError("checkCopyRecord: voiceRecord must be a VoiceRecord object");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const recordIdForReport = isNonEmptyStringField(record, "id") ? (record as { id: string }).id : "(unknown)";
|
|
226
|
+
|
|
227
|
+
// --- fail closed: the CopyRecord itself must be well-formed --------------
|
|
228
|
+
const recordShapeFindings = validateCopyRecordShape(record);
|
|
229
|
+
if (recordShapeFindings.length > 0) {
|
|
230
|
+
const findings: CopyRecordFinding[] = recordShapeFindings.map((f) => ({ ...f }));
|
|
231
|
+
const entriesValue = (record as unknown as Record<string, unknown>).entries;
|
|
232
|
+
return recordLevelFailure(recordIdForReport, findings, bestEffortSkipList(entriesValue, "record-shape-invalid"));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// --- fail closed: the VoiceRecord itself must be well-formed --------------
|
|
236
|
+
// Unlike voice's own checkCopy, this function does not trust its typed
|
|
237
|
+
// input — see this file's top-of-file note #2 for why.
|
|
238
|
+
const voiceShapeFindings = validateVoiceRecordShape(voiceRecord);
|
|
239
|
+
if (voiceShapeFindings.length > 0) {
|
|
240
|
+
const findings: CopyRecordFinding[] = voiceShapeFindings.map((f) => ({ ...f }));
|
|
241
|
+
// record is now known well-formed (previous branch would have returned
|
|
242
|
+
// otherwise), so every entry has a real, unique id to attribute a skip to.
|
|
243
|
+
const skipped: CopyEntrySkip[] = record.entries.map((entry) => ({
|
|
244
|
+
entryId: entry.id,
|
|
245
|
+
reason: "voice-record-invalid",
|
|
246
|
+
}));
|
|
247
|
+
return recordLevelFailure(recordIdForReport, findings, skipped);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- fail closed: zero entries is nothing to check, not a clean pass -----
|
|
251
|
+
if (record.entries.length === 0) {
|
|
252
|
+
const findings: CopyRecordFinding[] = [
|
|
253
|
+
{
|
|
254
|
+
rule: "record:no-entries",
|
|
255
|
+
severity: "error",
|
|
256
|
+
message: `CopyRecord "${recordIdForReport}" has zero entries — there is nothing for checkCopyRecord to check.`,
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
return recordLevelFailure(recordIdForReport, findings, []);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- the real work: run every entry through voice's checkCopy ------------
|
|
263
|
+
const checked: CopyEntryCheckResult[] = [];
|
|
264
|
+
const findings: CopyRecordFinding[] = [];
|
|
265
|
+
const waived: CopyRecordWaivedFinding[] = [];
|
|
266
|
+
|
|
267
|
+
for (const entry of record.entries) {
|
|
268
|
+
const entryReport = checkCopy(voiceRecord, entry.text, { waivers: options.waivers });
|
|
269
|
+
checked.push({ entryId: entry.id, report: entryReport });
|
|
270
|
+
for (const finding of entryReport.findings) {
|
|
271
|
+
findings.push({ ...finding, entryId: entry.id });
|
|
272
|
+
}
|
|
273
|
+
for (const w of entryReport.waived) {
|
|
274
|
+
waived.push({ ...w, entryId: entry.id });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
recordId: recordIdForReport,
|
|
280
|
+
checked,
|
|
281
|
+
skipped: [],
|
|
282
|
+
checkedCount: checked.length,
|
|
283
|
+
skippedCount: 0,
|
|
284
|
+
findings,
|
|
285
|
+
waived,
|
|
286
|
+
complete: true,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function isNonEmptyStringField(value: object, key: string): boolean {
|
|
291
|
+
const v = (value as Record<string, unknown>)[key];
|
|
292
|
+
return typeof v === "string" && v.length > 0;
|
|
293
|
+
}
|