@everystack/cli 0.3.3 → 0.3.5

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.
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Component model — a faithful, neutral representation of `.tsx` source (Brick 5a-B).
3
+ *
4
+ * This is the shared foundation, NOT a private detail of the auditor. The UI-bloat
5
+ * checks consume it; so will the 5b frontend Module contract (checking screens +
6
+ * components against a contract). Its reusability comes from *fidelity*: it
7
+ * represents what a component actually IS — name, location, JSX tree, hooks, props,
8
+ * imports, inline style literals — not an audit-shaped projection of it. It knows
9
+ * nothing about `AuditFinding`, checks, budgets, or fixes. The dependency points one
10
+ * way: the auditor imports this model; this model imports nothing from the auditor.
11
+ *
12
+ * Syntactic only (no type checker): everything here is read off the AST, so it needs
13
+ * a single `createSourceFile` per file — no `tsconfig` resolution, no `Program`, fast
14
+ * and pure. `parseComponents(file, text)` is the pure core (no filesystem);
15
+ * `discoverComponents(dir)` is the thin filesystem walk over it.
16
+ */
17
+
18
+ import ts from 'typescript';
19
+ import { readFileSync } from 'node:fs';
20
+ import { globSync } from 'glob';
21
+
22
+ /** A file-level import: `import def, { a, b }, * as ns from 'mod'`. */
23
+ export interface ImportRef {
24
+ from: string;
25
+ names: string[];
26
+ default?: string;
27
+ namespace?: string;
28
+ }
29
+
30
+ /** A structural JSX node — tag identity, the element's own attributes and inline
31
+ * style, and its nested element children (text and non-JSX expressions are not
32
+ * structure). Map/conditional-wrapped JSX is descended into, so genuine reuse (one
33
+ * `.map`) stays one node. */
34
+ export interface JsxNode {
35
+ tag: string;
36
+ /** 1-based line of this element. */
37
+ line: number;
38
+ /** Attribute names present on this element (e.g. ['style', 'onPress']). */
39
+ attrs: string[];
40
+ /** Raw inline `style={{…}}` object literal on this element, if any. */
41
+ style?: string;
42
+ children: JsxNode[];
43
+ }
44
+
45
+ /** An inline `style={{…}}` object literal and the line it sits on. */
46
+ export interface StyleLiteral {
47
+ text: string;
48
+ line: number;
49
+ }
50
+
51
+ /** A faithful model of one component. Source facts only — no audit concepts. */
52
+ export interface ComponentDescriptor {
53
+ name: string;
54
+ file: string;
55
+ /** 1-based line of the component declaration. */
56
+ line: number;
57
+ /** Line span of the component (for the size metric). */
58
+ lines: number;
59
+ jsx: JsxNode[];
60
+ hooks: string[];
61
+ props: string[];
62
+ imports: ImportRef[];
63
+ styles: StyleLiteral[];
64
+ }
65
+
66
+ const isJsx = (
67
+ n: ts.Node,
68
+ ): n is ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment =>
69
+ ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n);
70
+
71
+ /** True if any JSX appears anywhere in the subtree — the test for "is a component". */
72
+ function containsJsx(node: ts.Node): boolean {
73
+ let found = false;
74
+ const walk = (n: ts.Node): void => {
75
+ if (found) return;
76
+ if (isJsx(n)) {
77
+ found = true;
78
+ return;
79
+ }
80
+ ts.forEachChild(n, walk);
81
+ };
82
+ ts.forEachChild(node, walk);
83
+ return found;
84
+ }
85
+
86
+ function collectImports(sf: ts.SourceFile): ImportRef[] {
87
+ const out: ImportRef[] = [];
88
+ for (const stmt of sf.statements) {
89
+ if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
90
+ const ref: ImportRef = { from: stmt.moduleSpecifier.text, names: [] };
91
+ const clause = stmt.importClause;
92
+ if (clause?.name) ref.default = clause.name.text;
93
+ const bindings = clause?.namedBindings;
94
+ if (bindings) {
95
+ if (ts.isNamespaceImport(bindings)) ref.namespace = bindings.name.text;
96
+ else for (const el of bindings.elements) ref.names.push(el.name.text);
97
+ }
98
+ out.push(ref);
99
+ }
100
+ return out;
101
+ }
102
+
103
+ function tagName(n: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment): string {
104
+ if (ts.isJsxFragment(n)) return 'Fragment';
105
+ const tag = ts.isJsxElement(n) ? n.openingElement.tagName : n.tagName;
106
+ return tag.getText();
107
+ }
108
+
109
+ /** Read an element's attribute names and its inline `style={{…}}` literal. */
110
+ function readAttrs(
111
+ n: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment,
112
+ sf: ts.SourceFile,
113
+ ): { attrs: string[]; style?: string } {
114
+ if (ts.isJsxFragment(n)) return { attrs: [] };
115
+ const props = (ts.isJsxElement(n) ? n.openingElement : n).attributes.properties;
116
+ const attrs: string[] = [];
117
+ let style: string | undefined;
118
+ for (const p of props) {
119
+ if (!ts.isJsxAttribute(p) || !ts.isIdentifier(p.name)) continue;
120
+ attrs.push(p.name.text);
121
+ if (
122
+ p.name.text === 'style' &&
123
+ p.initializer &&
124
+ ts.isJsxExpression(p.initializer) &&
125
+ p.initializer.expression &&
126
+ ts.isObjectLiteralExpression(p.initializer.expression)
127
+ ) {
128
+ style = p.initializer.expression.getText(sf);
129
+ }
130
+ }
131
+ return { attrs, style };
132
+ }
133
+
134
+ /** Build a faithful node for one JSX element, with its attrs, style, line, children. */
135
+ function makeNode(
136
+ n: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment,
137
+ sf: ts.SourceFile,
138
+ ): JsxNode {
139
+ const { attrs, style } = readAttrs(n, sf);
140
+ const node: JsxNode = {
141
+ tag: tagName(n),
142
+ line: sf.getLineAndCharacterOfPosition(n.getStart(sf)).line + 1,
143
+ attrs,
144
+ children: jsxChildren(n, sf),
145
+ };
146
+ if (style !== undefined) node.style = style;
147
+ return node;
148
+ }
149
+
150
+ /** The element children of a JSX node, descending through expression containers
151
+ * ({cond && <X/>}, {items.map(=> <Y/>)}) so structure is captured but text is not. */
152
+ function jsxChildren(
153
+ n: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment,
154
+ sf: ts.SourceFile,
155
+ ): JsxNode[] {
156
+ if (ts.isJsxSelfClosingElement(n)) return [];
157
+ const kids: JsxNode[] = [];
158
+ const fromChild = (child: ts.Node): void => {
159
+ if (isJsx(child)) {
160
+ kids.push(makeNode(child, sf));
161
+ return; // don't descend past a JSX element — its own children handle that
162
+ }
163
+ // descend through JsxExpression / parens / map callbacks to find nested JSX
164
+ if (ts.isJsxText(child)) return;
165
+ ts.forEachChild(child, fromChild);
166
+ };
167
+ for (const child of n.children) fromChild(child);
168
+ return kids;
169
+ }
170
+
171
+ /** The outermost JSX nodes within a component body — its roots. */
172
+ function collectRoots(fn: ts.Node, sf: ts.SourceFile): JsxNode[] {
173
+ const roots: JsxNode[] = [];
174
+ const walk = (n: ts.Node): void => {
175
+ if (isJsx(n)) {
176
+ roots.push(makeNode(n, sf));
177
+ return; // stop — nested JSX is captured as children, not as another root
178
+ }
179
+ ts.forEachChild(n, walk);
180
+ };
181
+ ts.forEachChild(fn, walk);
182
+ return roots;
183
+ }
184
+
185
+ function collectHooks(fn: ts.Node): string[] {
186
+ const hooks: string[] = [];
187
+ const walk = (n: ts.Node): void => {
188
+ if (ts.isCallExpression(n)) {
189
+ const expr = n.expression;
190
+ const name = ts.isIdentifier(expr)
191
+ ? expr.text
192
+ : ts.isPropertyAccessExpression(expr)
193
+ ? expr.name.text
194
+ : '';
195
+ if (/^use[A-Z]/.test(name)) hooks.push(name);
196
+ }
197
+ ts.forEachChild(n, walk);
198
+ };
199
+ ts.forEachChild(fn, walk);
200
+ return hooks;
201
+ }
202
+
203
+ function collectProps(fn: ts.FunctionLikeDeclarationBase): string[] {
204
+ const param = fn.parameters[0];
205
+ if (!param) return [];
206
+ if (ts.isObjectBindingPattern(param.name)) {
207
+ return param.name.elements
208
+ .map((el) => (ts.isIdentifier(el.name) ? el.name.text : ''))
209
+ .filter(Boolean);
210
+ }
211
+ if (ts.isIdentifier(param.name)) return [param.name.text];
212
+ return [];
213
+ }
214
+
215
+ function collectStyles(fn: ts.Node, sf: ts.SourceFile): StyleLiteral[] {
216
+ const styles: StyleLiteral[] = [];
217
+ const walk = (n: ts.Node): void => {
218
+ if (
219
+ ts.isJsxAttribute(n) &&
220
+ ts.isIdentifier(n.name) &&
221
+ n.name.text === 'style' &&
222
+ n.initializer &&
223
+ ts.isJsxExpression(n.initializer) &&
224
+ n.initializer.expression &&
225
+ ts.isObjectLiteralExpression(n.initializer.expression)
226
+ ) {
227
+ const obj = n.initializer.expression;
228
+ styles.push({
229
+ text: obj.getText(sf),
230
+ line: sf.getLineAndCharacterOfPosition(obj.getStart(sf)).line + 1,
231
+ });
232
+ }
233
+ ts.forEachChild(n, walk);
234
+ };
235
+ ts.forEachChild(fn, walk);
236
+ return styles;
237
+ }
238
+
239
+ /**
240
+ * Pure core: parse one `.tsx` file's source text into faithful component
241
+ * descriptors. No filesystem, no type checker.
242
+ */
243
+ export function parseComponents(file: string, text: string): ComponentDescriptor[] {
244
+ const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
245
+ const imports = collectImports(sf);
246
+ const out: ComponentDescriptor[] = [];
247
+ const lineOf = (pos: number) => sf.getLineAndCharacterOfPosition(pos).line + 1;
248
+
249
+ const consider = (name: string, declNode: ts.Node, fn: ts.FunctionLikeDeclarationBase): void => {
250
+ if (!containsJsx(fn)) return;
251
+ const start = declNode.getStart(sf);
252
+ out.push({
253
+ name,
254
+ file,
255
+ line: lineOf(start),
256
+ lines: lineOf(declNode.getEnd()) - lineOf(start) + 1,
257
+ jsx: collectRoots(fn, sf),
258
+ hooks: collectHooks(fn),
259
+ props: collectProps(fn),
260
+ imports,
261
+ styles: collectStyles(fn, sf),
262
+ });
263
+ };
264
+
265
+ for (const node of sf.statements) {
266
+ if (ts.isFunctionDeclaration(node)) {
267
+ const name =
268
+ node.name?.text ??
269
+ (node.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ? 'default' : '');
270
+ if (name) consider(name, node, node);
271
+ } else if (ts.isVariableStatement(node)) {
272
+ for (const d of node.declarationList.declarations) {
273
+ if (
274
+ ts.isIdentifier(d.name) &&
275
+ d.initializer &&
276
+ (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))
277
+ ) {
278
+ consider(d.name.text, node, d.initializer);
279
+ }
280
+ }
281
+ } else if (ts.isExportAssignment(node)) {
282
+ const e = node.expression;
283
+ if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) consider('default', node, e);
284
+ }
285
+ }
286
+ return out;
287
+ }
288
+
289
+ /**
290
+ * Walk a directory for `.tsx` files (excluding node_modules) and parse each into
291
+ * component descriptors. The thin filesystem shell over `parseComponents`.
292
+ */
293
+ export function discoverComponents(dir: string): ComponentDescriptor[] {
294
+ const files = globSync('**/*.tsx', {
295
+ cwd: dir,
296
+ absolute: true,
297
+ ignore: ['**/node_modules/**', '**/.expo/**', '**/dist/**'],
298
+ });
299
+ const out: ComponentDescriptor[] = [];
300
+ for (const file of files) {
301
+ out.push(...parseComponents(file, readFileSync(file, 'utf8')));
302
+ }
303
+ return out;
304
+ }
package/src/cli/index.ts CHANGED
@@ -21,6 +21,11 @@ import { analyzeSSRCommand } from './commands/analyze.js';
21
21
  import { logsErrorsCommand, logsTailCommand, logsQueryCommand } from './commands/logs.js';
22
22
  import { secretsCommand } from './commands/secrets.js';
23
23
  import { securityAuditCommand } from './commands/security.js';
24
+ import { securityProbeCommand } from './commands/security-probe.js';
25
+ import { bundleAnalyzeCommand, bundleAuditCommand } from './commands/bundle.js';
26
+ import { auditLighthouseCommand } from './commands/lighthouse.js';
27
+ import { auditCommand } from './commands/audit.js';
28
+ import { uiAuditCommand } from './commands/ui-audit.js';
24
29
  import { fail } from './output.js';
25
30
 
26
31
  const args = process.argv.slice(2);
@@ -206,6 +211,30 @@ async function main() {
206
211
  case 'security:audit':
207
212
  await securityAuditCommand(flags);
208
213
  break;
214
+ case 'security:probe':
215
+ await securityProbeCommand(flags);
216
+ break;
217
+ case 'bundle:analyze': {
218
+ const positional = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
219
+ await bundleAnalyzeCommand(positional, flags);
220
+ break;
221
+ }
222
+ case 'audit':
223
+ await auditCommand(flags);
224
+ break;
225
+ case 'audit:lighthouse':
226
+ await auditLighthouseCommand(flags);
227
+ break;
228
+ case 'bundle:audit': {
229
+ const positional = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
230
+ await bundleAuditCommand(positional, flags);
231
+ break;
232
+ }
233
+ case 'ui:audit': {
234
+ const positional = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
235
+ await uiAuditCommand(positional, flags);
236
+ break;
237
+ }
209
238
  case 'secrets': {
210
239
  // secrets <subcommand> [positional...] [--flags]
211
240
  // Extract positional args: everything after subcommand that isn't a flag or flag value
@@ -262,6 +291,12 @@ Usage:
262
291
  everystack status [--stage <name>] [--hours <n>] Platform health: CDN, Lambda, rollup summary
263
292
  everystack security:audit [--dir <path>] [--config <file>] Static scan of migration SQL (pre-merge gate)
264
293
  everystack security:audit --stage <name> [--config <file>] Live catalog scan of a deployed stage (post-deploy)
294
+ everystack security:probe --stage <name> [--host <url>] Live black-box probe: introspects RLS/grants, then tests the deployed surface (RLS coverage, column exposure, injection, limit cap, embed depth)
295
+ everystack audit --stage <name> [--host <url>] [--dir <export>] [--lighthouse] Capstone: security:audit + security:probe + bundle:audit (+ optional lighthouse), combined report + exit code (CI gate); --dir adds the weight/size gate over a local export
296
+ everystack bundle:analyze [dir] Client web-bundle size + composition (run after \`expo export -p web --source-maps\`)
297
+ everystack audit:lighthouse [--stage <name>] [--host <url>] [--runs 3] [--path /] [--json out.json] Mobile Lighthouse (perf/a11y/bp/seo), median over N runs
298
+ everystack bundle:audit [url|--dir <export>] [--stage <name>] [--values-from <.env>] [--leak-only] [--max-file-mb N] [--max-total-mb N] [--max-js-mb N] [--max-data-mb N] Audit a bundle for leaked secrets AND weight (oversized assets, data that belongs in the DB, JS bloat) AND UI design-bloat (style repetition + hardcoded colors). A URL/stage weighs the remote bundle (assets sized via HEAD, no download); --dir scans a local export; --leak-only skips the weight gate
299
+ everystack ui:audit [dir] [--ui-repeat N] [--ui-color-repeat N] Source-side UI design-bloat: styles copy-pasted across call sites, hardcoded colors that should be tokens, raw primitives reimplementing a ui component, oversized components, duplicated JSX. Located + fixable; advisory, never gates
265
300
  everystack certs:generate [--output ./certs]
266
301
  everystack certs:configure [--input ./certs] [--keyid main]
267
302
  everystack cache:purge [--stage <name>] Bust all cached content (global epoch)
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Lighthouse audit — pure, Lighthouse-free parsing/aggregation logic for
3
+ * `everystack audit:lighthouse`.
4
+ *
5
+ * Why N runs: the category scores are stable but Performance swings run-to-run
6
+ * because LCP does under throttled-mobile simulation. A single run can read 92
7
+ * or 65. We run each host several times and report the DISTRIBUTION (median +
8
+ * min/max) — the median is the honest number, the spread shows how noisy LCP is.
9
+ *
10
+ * Measurement rule (in the command): warm the CloudFront edge first, then
11
+ * measure WITHOUT a cache-buster, so we score the real-user warm-HTML path, not
12
+ * a pessimistic cold-origin SSR render.
13
+ *
14
+ * Config-free: the target URL comes from the deployed SST outputs (or --host),
15
+ * never a hand-maintained env map.
16
+ */
17
+
18
+ export const CATEGORIES = ['performance', 'accessibility', 'best-practices', 'seo'] as const;
19
+ export const METRICS = [
20
+ 'first-contentful-paint',
21
+ 'largest-contentful-paint',
22
+ 'total-blocking-time',
23
+ 'cumulative-layout-shift',
24
+ 'speed-index',
25
+ ] as const;
26
+
27
+ export type Category = (typeof CATEGORIES)[number];
28
+ export type Metric = (typeof METRICS)[number];
29
+
30
+ export interface LhAudit {
31
+ title: string;
32
+ score: number | null;
33
+ numericValue?: number;
34
+ scoreDisplayMode?: string;
35
+ }
36
+
37
+ export interface LhReport {
38
+ categories: Record<string, { score: number; auditRefs: { id: string }[] }>;
39
+ audits: Record<string, LhAudit>;
40
+ }
41
+
42
+ export interface ExtractedRun {
43
+ scores: Record<string, number>; // category → 0..100
44
+ metrics: Record<string, number>; // metric → numericValue
45
+ imperfect: { id: string; title: string; cat: string }[];
46
+ }
47
+
48
+ export interface Aggregate {
49
+ scores: Record<string, number[]>;
50
+ metrics: Record<string, number[]>;
51
+ failing: { id: string; title: string; cat: string; count: number }[];
52
+ noindex: boolean;
53
+ }
54
+
55
+ export function median(xs: number[]): number {
56
+ const s = [...xs].sort((a, b) => a - b);
57
+ const m = Math.floor(s.length / 2);
58
+ return s.length % 2 ? s[m] : Math.round((s[m - 1] + s[m]) / 2);
59
+ }
60
+
61
+ /** Pull scores, metrics, and imperfect audits out of one Lighthouse report. */
62
+ export function extractRun(report: LhReport): ExtractedRun {
63
+ const scores: Record<string, number> = {};
64
+ const metrics: Record<string, number> = {};
65
+ const imperfect: { id: string; title: string; cat: string }[] = [];
66
+
67
+ for (const c of CATEGORIES) {
68
+ scores[c] = Math.round((report.categories[c]?.score ?? 0) * 100);
69
+ }
70
+ for (const m of METRICS) {
71
+ metrics[m] = report.audits[m]?.numericValue ?? 0;
72
+ }
73
+ for (const c of CATEGORIES) {
74
+ for (const ref of report.categories[c]?.auditRefs ?? []) {
75
+ const au = report.audits[ref.id];
76
+ if (au && au.score !== null && au.score < 1 && au.scoreDisplayMode !== 'informative') {
77
+ imperfect.push({ id: ref.id, title: au.title, cat: c });
78
+ }
79
+ }
80
+ }
81
+ return { scores, metrics, imperfect };
82
+ }
83
+
84
+ /** Aggregate the per-run extracts into score/metric distributions + flags. */
85
+ export function aggregate(runs: ExtractedRun[]): Aggregate {
86
+ const scores: Record<string, number[]> = Object.fromEntries(CATEGORIES.map((c) => [c, []]));
87
+ const metrics: Record<string, number[]> = Object.fromEntries(METRICS.map((m) => [m, []]));
88
+ const failingMap = new Map<string, { title: string; cat: string; count: number }>();
89
+ let noindex = false;
90
+
91
+ for (const run of runs) {
92
+ for (const c of CATEGORIES) scores[c].push(run.scores[c] ?? 0);
93
+ for (const m of METRICS) metrics[m].push(run.metrics[m] ?? 0);
94
+ for (const imp of run.imperfect) {
95
+ if (imp.id === 'is-crawlable') noindex = true;
96
+ const e = failingMap.get(imp.id) || { title: imp.title, cat: imp.cat, count: 0 };
97
+ e.count++;
98
+ failingMap.set(imp.id, e);
99
+ }
100
+ }
101
+
102
+ const failing = [...failingMap.entries()]
103
+ .map(([id, e]) => ({ id, ...e }))
104
+ .sort((a, b) => b.count - a.count);
105
+ return { scores, metrics, failing, noindex };
106
+ }
107
+
108
+ /** extractRun + aggregate over a set of raw reports. */
109
+ export function summarize(reports: LhReport[]): Aggregate {
110
+ return aggregate(reports.map(extractRun));
111
+ }
112
+
113
+ function fmtMs(v: number): string {
114
+ return v >= 1000 ? `${(v / 1000).toFixed(1)}s` : `${Math.round(v)}ms`;
115
+ }
116
+
117
+ export function formatLighthouseReport(url: string, runs: number, agg: Aggregate): string[] {
118
+ const lines: string[] = [];
119
+ lines.push('='.repeat(64));
120
+ lines.push(`${url} (${runs} run${runs === 1 ? '' : 's'})`);
121
+ lines.push('='.repeat(64));
122
+ lines.push('Category median runs');
123
+ for (const c of CATEGORIES) {
124
+ const xs = agg.scores[c];
125
+ const spread = xs.length > 1 ? ` [${Math.min(...xs)}–${Math.max(...xs)}]` : '';
126
+ lines.push(` ${c.padEnd(14)} ${String(median(xs)).padStart(3)} ${xs.join(' ')}${spread}`);
127
+ }
128
+ lines.push('');
129
+ lines.push('Metric median spread');
130
+ for (const m of METRICS) {
131
+ const xs = agg.metrics[m];
132
+ const cls = m === 'cumulative-layout-shift';
133
+ const f = cls ? (v: number) => v.toFixed(3) : fmtMs;
134
+ lines.push(` ${m.padEnd(24)} ${f(median(xs)).padStart(7)} ${f(Math.min(...xs))}–${f(Math.max(...xs))}`);
135
+ }
136
+ if (agg.failing.length) {
137
+ lines.push('');
138
+ lines.push('Imperfect audits (count = runs flagged):');
139
+ for (const e of agg.failing) {
140
+ const note =
141
+ e.id === 'is-crawlable'
142
+ ? ' ← expected when the stage is noindex (true SEO only on the indexable domain)'
143
+ : '';
144
+ lines.push(` [${e.cat}] ${e.id} (${e.count}/${runs}) — ${e.title}${note}`);
145
+ }
146
+ }
147
+ if (agg.noindex) {
148
+ lines.push('');
149
+ lines.push(
150
+ 'NOTE: is-crawlable fails because this stage is noindex (robots Disallow: /).',
151
+ );
152
+ lines.push(' That suppresses SEO ~20pts. Re-measure the indexable production domain for the true score.');
153
+ }
154
+ return lines;
155
+ }