@sdods/db 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +17 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/col.d.ts +32 -0
  4. package/dist/col.js +58 -0
  5. package/dist/create-db.d.ts +14 -0
  6. package/dist/create-db.js +58 -0
  7. package/dist/driver.d.ts +10 -0
  8. package/dist/driver.js +16 -0
  9. package/dist/ids.d.ts +2 -0
  10. package/dist/ids.js +2 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.js +22 -0
  13. package/dist/ingest/attachments.d.ts +40 -0
  14. package/dist/ingest/attachments.js +154 -0
  15. package/dist/ingest/cucumber-ingest.d.ts +40 -0
  16. package/dist/ingest/cucumber-ingest.js +543 -0
  17. package/dist/ingest/finalize.d.ts +28 -0
  18. package/dist/ingest/finalize.js +132 -0
  19. package/dist/ingest/index.d.ts +26 -0
  20. package/dist/ingest/index.js +193 -0
  21. package/dist/ingest/message-index.d.ts +64 -0
  22. package/dist/ingest/message-index.js +146 -0
  23. package/dist/ingest/parse-ndjson.d.ts +10 -0
  24. package/dist/ingest/parse-ndjson.js +35 -0
  25. package/dist/ingest/pw-json-ingest.d.ts +14 -0
  26. package/dist/ingest/pw-json-ingest.js +259 -0
  27. package/dist/ingest/runner-json-ingest.d.ts +14 -0
  28. package/dist/ingest/runner-json-ingest.js +259 -0
  29. package/dist/ingest/types.d.ts +62 -0
  30. package/dist/ingest/types.js +89 -0
  31. package/dist/insights.d.ts +62 -0
  32. package/dist/insights.js +184 -0
  33. package/dist/migrate.d.ts +30 -0
  34. package/dist/migrate.js +114 -0
  35. package/dist/migrations/0001_platform_init.d.ts +5 -0
  36. package/dist/migrations/0001_platform_init.js +48 -0
  37. package/dist/migrations/0002_results.d.ts +5 -0
  38. package/dist/migrations/0002_results.js +124 -0
  39. package/dist/migrations/0003_improvement.d.ts +5 -0
  40. package/dist/migrations/0003_improvement.js +56 -0
  41. package/dist/migrations/0004_test_data.d.ts +5 -0
  42. package/dist/migrations/0004_test_data.js +48 -0
  43. package/dist/migrations/0005_integrations_agents_audit.d.ts +5 -0
  44. package/dist/migrations/0005_integrations_agents_audit.js +100 -0
  45. package/dist/migrations/0006_schedules.d.ts +5 -0
  46. package/dist/migrations/0006_schedules.js +34 -0
  47. package/dist/migrations/0007_hierarchy.d.ts +5 -0
  48. package/dist/migrations/0007_hierarchy.js +76 -0
  49. package/dist/migrations/0008_runner_naming.d.ts +14 -0
  50. package/dist/migrations/0008_runner_naming.js +35 -0
  51. package/dist/migrations/helpers.d.ts +15 -0
  52. package/dist/migrations/helpers.js +30 -0
  53. package/dist/migrations/index.d.ts +6 -0
  54. package/dist/migrations/index.js +22 -0
  55. package/dist/repos/auth.d.ts +109 -0
  56. package/dist/repos/auth.js +258 -0
  57. package/dist/repos/hierarchy.d.ts +151 -0
  58. package/dist/repos/hierarchy.js +465 -0
  59. package/dist/repos/improvement.d.ts +84 -0
  60. package/dist/repos/improvement.js +242 -0
  61. package/dist/repos/index.d.ts +8 -0
  62. package/dist/repos/index.js +8 -0
  63. package/dist/repos/platform.d.ts +250 -0
  64. package/dist/repos/platform.js +429 -0
  65. package/dist/repos/projects.d.ts +87 -0
  66. package/dist/repos/projects.js +118 -0
  67. package/dist/repos/runs.d.ts +306 -0
  68. package/dist/repos/runs.js +321 -0
  69. package/dist/repos/test-data.d.ts +84 -0
  70. package/dist/repos/test-data.js +291 -0
  71. package/dist/schema.d.ts +514 -0
  72. package/dist/schema.js +37 -0
  73. package/dist/scopes.d.ts +2 -0
  74. package/dist/scopes.js +2 -0
  75. package/dist/switch/export.d.ts +15 -0
  76. package/dist/switch/export.js +67 -0
  77. package/dist/switch/import.d.ts +16 -0
  78. package/dist/switch/import.js +106 -0
  79. package/dist/switch/index.d.ts +5 -0
  80. package/dist/switch/index.js +5 -0
  81. package/dist/switch/prune.d.ts +19 -0
  82. package/dist/switch/prune.js +50 -0
  83. package/dist/switch/switch.d.ts +34 -0
  84. package/dist/switch/switch.js +133 -0
  85. package/dist/test-data.d.ts +22 -0
  86. package/dist/test-data.js +79 -0
  87. package/package.json +50 -0
@@ -0,0 +1,132 @@
1
+ import { enc, nowIso, readBool, readJson, readTs } from '../col.js';
2
+ import { bumpLocatorStats, recomputeFlakyStats } from '../repos/improvement.js';
3
+ import { emptyTotals } from './types.js';
4
+ /**
5
+ * Derive scenario outcomes from attempts (count scenarios, not attempts), run totals/status,
6
+ * flaky stats windows and locator stats. Safe to re-run.
7
+ */
8
+ export async function finalizeRun(input) {
9
+ const { adb, runId, projectId } = input;
10
+ const { db, driver } = adb;
11
+ const scenarios = await db
12
+ .selectFrom('scenarios')
13
+ .selectAll()
14
+ .where('run_id', '=', runId)
15
+ .execute();
16
+ const attempts = await db
17
+ .selectFrom('scenario_attempts')
18
+ .selectAll()
19
+ .where('run_id', '=', runId)
20
+ .execute();
21
+ const heals = await db
22
+ .selectFrom('heal_events')
23
+ .select(['scenario_id'])
24
+ .where('run_id', '=', runId)
25
+ .execute();
26
+ const healedScenarios = new Set(heals.map((h) => h.scenario_id).filter(Boolean));
27
+ const totals = emptyTotals();
28
+ let minStart = null;
29
+ let maxEnd = null;
30
+ for (const s of scenarios) {
31
+ const mine = attempts
32
+ .filter((a) => a.scenario_id === s.id)
33
+ .sort((a, b) => a.attempt - b.attempt);
34
+ const last = mine[mine.length - 1];
35
+ const finalStatus = last?.status ?? s.status ?? 'unknown';
36
+ const earlierFailed = mine
37
+ .slice(0, -1)
38
+ .some((a) => a.status === 'failed' || a.status === 'timedOut');
39
+ const flaky = mine.length > 1 && finalStatus === 'passed' && earlierFailed;
40
+ const startedAt = mine[0] ? readTs(mine[0].started_at) : readTs(s.started_at);
41
+ const finishedAt = last ? readTs(last.finished_at) : readTs(s.finished_at);
42
+ await db
43
+ .updateTable('scenarios')
44
+ .set({
45
+ status: finalStatus,
46
+ attempts_count: mine.length,
47
+ flaky: enc.bool(driver, flaky),
48
+ duration_ms: last?.duration_ms ?? s.duration_ms ?? null,
49
+ error_message: finalStatus === 'passed' ? null : (last?.error_message ?? s.error_message ?? null),
50
+ error_stack: finalStatus === 'passed' ? null : (last?.error_stack ?? s.error_stack ?? null),
51
+ started_at: startedAt,
52
+ finished_at: finishedAt,
53
+ updated_at: nowIso(),
54
+ })
55
+ .where('id', '=', s.id)
56
+ .execute();
57
+ totals.total++;
58
+ if (flaky)
59
+ totals.flaky++;
60
+ else if (finalStatus === 'passed')
61
+ totals.passed++;
62
+ else if (finalStatus === 'failed')
63
+ totals.failed++;
64
+ else if (finalStatus === 'timedOut')
65
+ totals.timedOut++;
66
+ else
67
+ totals.skipped++;
68
+ if (healedScenarios.has(s.id))
69
+ totals.healed++;
70
+ if (startedAt)
71
+ minStart =
72
+ minStart === null ? Date.parse(startedAt) : Math.min(minStart, Date.parse(startedAt));
73
+ if (finishedAt)
74
+ maxEnd = maxEnd === null ? Date.parse(finishedAt) : Math.max(maxEnd, Date.parse(finishedAt));
75
+ }
76
+ const started = input.runStartedAt ? Date.parse(input.runStartedAt) : minStart;
77
+ const finished = input.runFinishedAt ? Date.parse(input.runFinishedAt) : maxEnd;
78
+ totals.durationMs =
79
+ started !== null && finished !== null && started !== undefined && finished !== undefined
80
+ ? Math.max(0, finished - started)
81
+ : 0;
82
+ const status = totals.failed + totals.timedOut > 0 ? 'failed' : totals.total > 0 ? 'passed' : 'error';
83
+ const existing = await db
84
+ .selectFrom('runs')
85
+ .select(['totals_json'])
86
+ .where('id', '=', runId)
87
+ .executeTakeFirst();
88
+ const prev = readJson(existing?.totals_json) ?? {};
89
+ const files = new Set([
90
+ ...(Array.isArray(prev.ingested_files) ? prev.ingested_files : []),
91
+ ...input.ingestedFiles,
92
+ ]);
93
+ await db
94
+ .updateTable('runs')
95
+ .set({
96
+ status,
97
+ totals_json: enc.json({
98
+ ...prev,
99
+ ...totals,
100
+ ingested_files: [...files],
101
+ ingestedAt: nowIso(),
102
+ }),
103
+ shards_ingested: files.size,
104
+ started_at: started !== null && started !== undefined ? new Date(started).toISOString() : null,
105
+ finished_at: finished !== null && finished !== undefined ? new Date(finished).toISOString() : null,
106
+ duration_ms: totals.durationMs,
107
+ exit_code: input.exitCode ?? (status === 'passed' ? 0 : 1),
108
+ updated_at: nowIso(),
109
+ })
110
+ .where('id', '=', runId)
111
+ .execute();
112
+ // flaky stats over the window for every scenario key touched by this run
113
+ const keys = new Set(scenarios.map((s) => `${s.fingerprint}|${s.runner_project}`));
114
+ for (const key of keys) {
115
+ const [fingerprint, runnerProject] = key.split('|');
116
+ await recomputeFlakyStats(db, driver, { projectId, fingerprint, runnerProject });
117
+ }
118
+ // locator stats
119
+ for (const [selector, c] of input.locatorCounters) {
120
+ await bumpLocatorStats(db, projectId, {
121
+ selector,
122
+ fails: c.fails,
123
+ heals: c.heals,
124
+ uses: c.uses,
125
+ lastStrategy: c.lastStrategy ?? null,
126
+ suggestedSelector: c.suggested ?? null,
127
+ });
128
+ }
129
+ void readBool;
130
+ return { totals, status };
131
+ }
132
+ //# sourceMappingURL=finalize.js.map
@@ -0,0 +1,26 @@
1
+ import type { RunManifest } from '@sdods/contracts/types';
2
+ import type { SdodsDb } from '../create-db.js';
3
+ import type { IngestResult, IngestRunOptions } from './types.js';
4
+ export * from './types.js';
5
+ export * from './parse-ndjson.js';
6
+ export * from './message-index.js';
7
+ export * from './attachments.js';
8
+ export * from './cucumber-ingest.js';
9
+ export * from './runner-json-ingest.js';
10
+ export * from './finalize.js';
11
+ /** Read `<runDir>/run.json` when present. */
12
+ export declare function readManifest(runDir: string): RunManifest | null;
13
+ /**
14
+ * Discover result files inside a run directory. Run directories outlive releases, so the
15
+ * pre-rename `pw-results*.json` name is still accepted alongside `runner-results*.json`.
16
+ */
17
+ export declare function discoverRunFiles(runDir: string): {
18
+ ndjson: string[];
19
+ runnerJson: string[];
20
+ };
21
+ /**
22
+ * Ingest one run (all its NDJSON shards and runner JSON files) into the database.
23
+ * Idempotent: natural keys converge on re-run; `replace` wipes the run's children first.
24
+ */
25
+ export declare function ingestRun(adb: SdodsDb, opts: IngestRunOptions): Promise<IngestResult>;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,193 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { basename, join, resolve } from 'node:path';
3
+ import { runFiles } from '@sdods/contracts/names';
4
+ import { readJson } from '../col.js';
5
+ import { ensureProject, getProjectBySlug } from '../repos/projects.js';
6
+ import { deleteRunChildren, upsertRun } from '../repos/runs.js';
7
+ import { IngestSession } from './cucumber-ingest.js';
8
+ import { finalizeRun } from './finalize.js';
9
+ import { fileSha256, parseNdjson } from './parse-ndjson.js';
10
+ import { ingestRunnerJson } from './runner-json-ingest.js';
11
+ export * from './types.js';
12
+ export * from './parse-ndjson.js';
13
+ export * from './message-index.js';
14
+ export * from './attachments.js';
15
+ export * from './cucumber-ingest.js';
16
+ export * from './runner-json-ingest.js';
17
+ export * from './finalize.js';
18
+ /** Read `<runDir>/run.json` when present. */
19
+ export function readManifest(runDir) {
20
+ const file = join(runDir, runFiles.manifest);
21
+ if (!existsSync(file))
22
+ return null;
23
+ try {
24
+ return JSON.parse(readFileSync(file, 'utf8'));
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ /**
31
+ * Discover result files inside a run directory. Run directories outlive releases, so the
32
+ * pre-rename `pw-results*.json` name is still accepted alongside `runner-results*.json`.
33
+ */
34
+ export function discoverRunFiles(runDir) {
35
+ const ndjson = [];
36
+ const runnerJson = [];
37
+ if (!existsSync(runDir))
38
+ return { ndjson, runnerJson };
39
+ for (const f of readdirSync(runDir)) {
40
+ if (/^messages(\.shard-\d+)?\.ndjson$/.test(f))
41
+ ndjson.push(join(runDir, f));
42
+ if (/^(runner|pw)-results(\.shard-\d+)?\.json$/.test(f))
43
+ runnerJson.push(join(runDir, f));
44
+ }
45
+ return { ndjson: ndjson.sort(), runnerJson: runnerJson.sort() };
46
+ }
47
+ /**
48
+ * Ingest one run (all its NDJSON shards and runner JSON files) into the database.
49
+ * Idempotent: natural keys converge on re-run; `replace` wipes the run's children first.
50
+ */
51
+ export async function ingestRun(adb, opts) {
52
+ const artifactsRoot = resolve(opts.artifactsRoot);
53
+ const runDir = join(artifactsRoot, opts.runId);
54
+ const manifest = opts.manifest === undefined ? readManifest(runDir) : opts.manifest;
55
+ const projectSlug = manifest?.projectSlug ?? opts.projectSlug;
56
+ if (!projectSlug)
57
+ throw new Error('ingestRun needs a project slug (manifest run.json or --project).');
58
+ const discovered = discoverRunFiles(runDir);
59
+ const ndjsonPaths = (opts.ndjsonPaths?.length ? opts.ndjsonPaths : discovered.ndjson).map((p) => resolve(p));
60
+ const runnerJsonPaths = (opts.runnerJsonPaths?.length ? opts.runnerJsonPaths : discovered.runnerJson).map((p) => resolve(p));
61
+ if (ndjsonPaths.length === 0 && runnerJsonPaths.length === 0)
62
+ throw new Error(`No messages*.ndjson or runner-results*.json found for run ${opts.runId} under ${runDir}.`);
63
+ const { db, driver } = adb;
64
+ const project = (await getProjectBySlug(db, projectSlug)) ?? {
65
+ id: await ensureProject(db, driver, projectSlug),
66
+ workspaceId: null,
67
+ };
68
+ const ctx = {
69
+ adb,
70
+ runId: opts.runId,
71
+ projectId: project.id,
72
+ projectSlug,
73
+ artifactsRoot,
74
+ runDir,
75
+ workspaceId: project.workspaceId ?? null,
76
+ };
77
+ const existingRun = await db
78
+ .selectFrom('runs')
79
+ .select(['totals_json'])
80
+ .where('id', '=', opts.runId)
81
+ .executeTakeFirst();
82
+ const prevTotals = readJson(existingRun?.totals_json) ?? {};
83
+ const alreadyIngested = new Set(Array.isArray(prevTotals.ingested_files) ? prevTotals.ingested_files : []);
84
+ await upsertRun(db, driver, {
85
+ id: opts.runId,
86
+ projectId: project.id,
87
+ workspaceId: ctx.workspaceId,
88
+ envName: manifest?.env ?? opts.env ?? 'unknown',
89
+ trigger: manifest?.trigger ?? opts.trigger ?? 'cli',
90
+ status: 'running',
91
+ suiteTag: manifest?.suiteTag ?? null,
92
+ tagsExpr: manifest?.tagsExpr ?? null,
93
+ layers: manifest?.layers ?? [],
94
+ browsers: manifest?.browsers ?? [],
95
+ shardTotal: manifest?.shardTotal ?? null,
96
+ gitSha: manifest?.git?.sha ?? null,
97
+ gitBranch: manifest?.git?.branch ?? null,
98
+ ciProvider: manifest?.ci?.provider ?? null,
99
+ ciRunId: manifest?.ci?.runId ?? null,
100
+ ciUrl: manifest?.ci?.url ?? null,
101
+ command: manifest?.command ?? null,
102
+ startedAt: manifest?.startedAt ?? null,
103
+ artifactsDir: runDir,
104
+ htmlReportRel: existsSync(join(runDir, runFiles.htmlReport)) ? runFiles.htmlReport : null,
105
+ exitCode: manifest?.exitCode ?? null,
106
+ process: manifest?.process ?? null,
107
+ });
108
+ if (opts.replace)
109
+ await deleteRunChildren(db, opts.runId);
110
+ const locatorCounters = new Map();
111
+ const filesIngested = [];
112
+ const filesSkipped = [];
113
+ let parseErrors = 0;
114
+ let runStartedAt = manifest?.startedAt ?? null;
115
+ let runFinishedAt = manifest?.finishedAt ?? null;
116
+ const counts = { scenarios: 0, attempts: 0, steps: 0, artifacts: 0, healEvents: 0 };
117
+ for (const file of ndjsonPaths) {
118
+ const sha = fileSha256(file);
119
+ const key = `${basename(file)}@${sha}`;
120
+ if (alreadyIngested.has(key) && !opts.replace) {
121
+ filesSkipped.push(file);
122
+ continue;
123
+ }
124
+ const session = new IngestSession(ctx);
125
+ const stats = { lines: 0, parsed: 0, skipped: 0 };
126
+ for await (const env of parseNdjson(file, stats))
127
+ await session.consume(env);
128
+ parseErrors += stats.skipped;
129
+ for (const [sel, c] of session.locatorCounters) {
130
+ const prev = locatorCounters.get(sel) ?? { fails: 0, heals: 0, uses: 0 };
131
+ locatorCounters.set(sel, {
132
+ fails: prev.fails + c.fails,
133
+ heals: prev.heals + c.heals,
134
+ uses: prev.uses + c.uses,
135
+ lastStrategy: c.lastStrategy ?? prev.lastStrategy,
136
+ suggested: c.suggested ?? prev.suggested,
137
+ });
138
+ }
139
+ counts.scenarios += session.counts.scenarios;
140
+ counts.attempts += session.counts.attempts;
141
+ counts.steps += session.counts.steps;
142
+ counts.artifacts += session.counts.artifacts;
143
+ counts.healEvents += session.counts.healEvents;
144
+ if (session.runStartedAt && (!runStartedAt || session.runStartedAt < runStartedAt))
145
+ runStartedAt = session.runStartedAt;
146
+ if (session.runFinishedAt && (!runFinishedAt || session.runFinishedAt > runFinishedAt))
147
+ runFinishedAt = session.runFinishedAt;
148
+ filesIngested.push(key);
149
+ }
150
+ for (const file of runnerJsonPaths) {
151
+ const sha = fileSha256(file);
152
+ const key = `${basename(file)}@${sha}`;
153
+ if (alreadyIngested.has(key) && !opts.replace) {
154
+ filesSkipped.push(file);
155
+ continue;
156
+ }
157
+ const r = await ingestRunnerJson(ctx, file, locatorCounters);
158
+ counts.scenarios += r.scenarios;
159
+ counts.attempts += r.attempts;
160
+ counts.steps += r.steps;
161
+ counts.artifacts += r.artifacts;
162
+ if (r.startedAt && (!runStartedAt || r.startedAt < runStartedAt))
163
+ runStartedAt = r.startedAt;
164
+ if (r.finishedAt && (!runFinishedAt || r.finishedAt > runFinishedAt))
165
+ runFinishedAt = r.finishedAt;
166
+ filesIngested.push(key);
167
+ }
168
+ const { totals, status } = await finalizeRun({
169
+ adb,
170
+ runId: opts.runId,
171
+ projectId: project.id,
172
+ locatorCounters,
173
+ runStartedAt,
174
+ runFinishedAt,
175
+ ingestedFiles: filesIngested,
176
+ exitCode: manifest?.exitCode ?? null,
177
+ });
178
+ return {
179
+ runId: opts.runId,
180
+ projectSlug,
181
+ totals,
182
+ status,
183
+ scenarios: counts.scenarios,
184
+ attempts: counts.attempts,
185
+ steps: counts.steps,
186
+ artifacts: counts.artifacts,
187
+ healEvents: counts.healEvents,
188
+ filesIngested,
189
+ filesSkipped,
190
+ parseErrors,
191
+ };
192
+ }
193
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * In-memory index of the static cucumber envelopes (gherkinDocument, pickle, stepDefinition, hook,
3
+ * testCase) that the dynamic ones (testCaseStarted, testStepFinished, attachment …) refer to.
4
+ */
5
+ export interface GherkinStepInfo {
6
+ keyword: string;
7
+ line: number;
8
+ }
9
+ export interface PickleInfo {
10
+ id: string;
11
+ uri: string;
12
+ name: string;
13
+ tags: string[];
14
+ steps: Array<{
15
+ id: string;
16
+ text: string;
17
+ type?: string;
18
+ astNodeIds: string[];
19
+ argument?: unknown;
20
+ }>;
21
+ astNodeIds: string[];
22
+ }
23
+ export interface TestCaseInfo {
24
+ id: string;
25
+ pickleId: string;
26
+ testSteps: Array<{
27
+ id: string;
28
+ pickleStepId?: string;
29
+ hookId?: string;
30
+ stepDefinitionIds?: string[];
31
+ }>;
32
+ }
33
+ export declare class MessageIndex {
34
+ readonly docs: Map<string, any>;
35
+ readonly pickles: Map<string, PickleInfo>;
36
+ readonly testCases: Map<string, TestCaseInfo>;
37
+ readonly stepDefs: Map<string, any>;
38
+ readonly hooks: Map<string, any>;
39
+ /** ast node id → step keyword/line (from gherkin documents) */
40
+ private readonly astSteps;
41
+ /** ast node id (examples table row) → { index (1-based), examplesTitle } */
42
+ private readonly astExampleRows;
43
+ /** ast node id (scenario) → { name, line } */
44
+ private readonly astScenarios;
45
+ private readonly featureNameByUri;
46
+ addDoc(doc: any): void;
47
+ addPickle(p: any): void;
48
+ addStepDef(sd: any): void;
49
+ addHook(h: any): void;
50
+ addTestCase(tc: any): void;
51
+ featureName(uri: string): string | null;
52
+ stepKeyword(pickleStep: {
53
+ astNodeIds: string[];
54
+ }): string | null;
55
+ /** 1-based Examples row index for an outline pickle, null for plain scenarios. */
56
+ exampleIndex(pickle: PickleInfo): number | null;
57
+ scenarioLine(pickle: PickleInfo): number | null;
58
+ /** Base scenario name (without example values substituted) */
59
+ scenarioTitle(pickle: PickleInfo): string;
60
+ hookType(hookId: string): 'before' | 'after' | null;
61
+ hookName(hookId: string): string;
62
+ definitionLocation(ids: string[] | undefined): string | null;
63
+ }
64
+ //# sourceMappingURL=message-index.d.ts.map
@@ -0,0 +1,146 @@
1
+ export class MessageIndex {
2
+ docs = new Map();
3
+ pickles = new Map();
4
+ testCases = new Map();
5
+ stepDefs = new Map();
6
+ hooks = new Map();
7
+ /** ast node id → step keyword/line (from gherkin documents) */
8
+ astSteps = new Map();
9
+ /** ast node id (examples table row) → { index (1-based), examplesTitle } */
10
+ astExampleRows = new Map();
11
+ /** ast node id (scenario) → { name, line } */
12
+ astScenarios = new Map();
13
+ featureNameByUri = new Map();
14
+ addDoc(doc) {
15
+ if (!doc?.uri)
16
+ return;
17
+ this.docs.set(doc.uri, doc);
18
+ const feature = doc.feature;
19
+ if (!feature)
20
+ return;
21
+ this.featureNameByUri.set(doc.uri, feature.name ?? '');
22
+ const walkChildren = (children) => {
23
+ for (const child of children ?? []) {
24
+ if (child.rule)
25
+ walkChildren(child.rule.children);
26
+ const node = child.background ?? child.scenario;
27
+ if (!node)
28
+ continue;
29
+ if (child.scenario) {
30
+ this.astScenarios.set(node.id, {
31
+ name: node.name ?? '',
32
+ line: node.location?.line ?? 0,
33
+ tags: (node.tags ?? []).map((t) => t.name),
34
+ });
35
+ }
36
+ for (const step of node.steps ?? []) {
37
+ this.astSteps.set(step.id, {
38
+ keyword: String(step.keyword ?? '').trim(),
39
+ line: step.location?.line ?? 0,
40
+ });
41
+ }
42
+ let rowIndex = 0;
43
+ for (const ex of node.examples ?? []) {
44
+ for (const row of ex.tableBody ?? []) {
45
+ rowIndex++;
46
+ this.astExampleRows.set(row.id, { index: rowIndex, line: row.location?.line ?? 0 });
47
+ }
48
+ }
49
+ }
50
+ };
51
+ walkChildren(feature.children);
52
+ }
53
+ addPickle(p) {
54
+ if (!p?.id)
55
+ return;
56
+ this.pickles.set(p.id, {
57
+ id: p.id,
58
+ uri: p.uri,
59
+ name: p.name ?? '',
60
+ tags: (p.tags ?? []).map((t) => t.name),
61
+ steps: (p.steps ?? []).map((s) => ({
62
+ id: s.id,
63
+ text: s.text ?? '',
64
+ type: s.type,
65
+ astNodeIds: s.astNodeIds ?? [],
66
+ argument: s.argument,
67
+ })),
68
+ astNodeIds: p.astNodeIds ?? [],
69
+ });
70
+ }
71
+ addStepDef(sd) {
72
+ if (sd?.id)
73
+ this.stepDefs.set(sd.id, sd);
74
+ }
75
+ addHook(h) {
76
+ if (h?.id)
77
+ this.hooks.set(h.id, h);
78
+ }
79
+ addTestCase(tc) {
80
+ if (!tc?.id)
81
+ return;
82
+ this.testCases.set(tc.id, { id: tc.id, pickleId: tc.pickleId, testSteps: tc.testSteps ?? [] });
83
+ }
84
+ featureName(uri) {
85
+ return this.featureNameByUri.get(uri) ?? null;
86
+ }
87
+ stepKeyword(pickleStep) {
88
+ for (const id of pickleStep.astNodeIds) {
89
+ const s = this.astSteps.get(id);
90
+ if (s)
91
+ return s.keyword;
92
+ }
93
+ return null;
94
+ }
95
+ /** 1-based Examples row index for an outline pickle, null for plain scenarios. */
96
+ exampleIndex(pickle) {
97
+ for (const id of pickle.astNodeIds) {
98
+ const row = this.astExampleRows.get(id);
99
+ if (row)
100
+ return row.index;
101
+ }
102
+ return null;
103
+ }
104
+ scenarioLine(pickle) {
105
+ for (const id of pickle.astNodeIds) {
106
+ const sc = this.astScenarios.get(id);
107
+ if (sc)
108
+ return sc.line;
109
+ }
110
+ return null;
111
+ }
112
+ /** Base scenario name (without example values substituted) */
113
+ scenarioTitle(pickle) {
114
+ for (const id of pickle.astNodeIds) {
115
+ const sc = this.astScenarios.get(id);
116
+ if (sc)
117
+ return sc.name || pickle.name;
118
+ }
119
+ return pickle.name;
120
+ }
121
+ hookType(hookId) {
122
+ const h = this.hooks.get(hookId);
123
+ if (!h)
124
+ return null;
125
+ const t = String(h.type ?? h.name ?? '').toLowerCase();
126
+ if (t.includes('after'))
127
+ return 'after';
128
+ if (t.includes('before'))
129
+ return 'before';
130
+ return null;
131
+ }
132
+ hookName(hookId) {
133
+ const h = this.hooks.get(hookId);
134
+ return h?.name ?? h?.sourceReference?.uri ?? 'hook';
135
+ }
136
+ definitionLocation(ids) {
137
+ for (const id of ids ?? []) {
138
+ const sd = this.stepDefs.get(id);
139
+ const ref = sd?.sourceReference;
140
+ if (ref?.uri)
141
+ return `${ref.uri}:${ref.location?.line ?? 0}`;
142
+ }
143
+ return null;
144
+ }
145
+ }
146
+ //# sourceMappingURL=message-index.js.map
@@ -0,0 +1,10 @@
1
+ export type Envelope = Record<string, any>;
2
+ export interface ParseStats {
3
+ lines: number;
4
+ parsed: number;
5
+ skipped: number;
6
+ }
7
+ /** Tolerant NDJSON reader: blank and corrupt lines are counted and skipped, never fatal. */
8
+ export declare function parseNdjson(file: string, stats?: ParseStats): AsyncGenerator<Envelope>;
9
+ export declare function fileSha256(file: string): string;
10
+ //# sourceMappingURL=parse-ndjson.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createReadStream, readFileSync } from 'node:fs';
3
+ import { createInterface } from 'node:readline';
4
+ /** Tolerant NDJSON reader: blank and corrupt lines are counted and skipped, never fatal. */
5
+ export async function* parseNdjson(file, stats) {
6
+ const rl = createInterface({
7
+ input: createReadStream(file, { encoding: 'utf8' }),
8
+ crlfDelay: Infinity,
9
+ });
10
+ for await (const raw of rl) {
11
+ if (stats)
12
+ stats.lines++;
13
+ const line = raw.trim();
14
+ if (!line)
15
+ continue;
16
+ try {
17
+ const env = JSON.parse(line);
18
+ if (env && typeof env === 'object') {
19
+ if (stats)
20
+ stats.parsed++;
21
+ yield env;
22
+ }
23
+ else if (stats)
24
+ stats.skipped++;
25
+ }
26
+ catch {
27
+ if (stats)
28
+ stats.skipped++;
29
+ }
30
+ }
31
+ }
32
+ export function fileSha256(file) {
33
+ return createHash('sha256').update(readFileSync(file)).digest('hex');
34
+ }
35
+ //# sourceMappingURL=parse-ndjson.js.map
@@ -0,0 +1,14 @@
1
+ import type { LocatorCounter } from './finalize.js';
2
+ import { type IngestContext } from './types.js';
3
+ export interface PwJsonCounts {
4
+ scenarios: number;
5
+ attempts: number;
6
+ steps: number;
7
+ artifacts: number;
8
+ }
9
+ /** Ingest a Playwright `json` reporter file (recorded layer / plain specs) into the same tables. */
10
+ export declare function ingestPwJson(ctx: IngestContext, file: string, locatorCounters: Map<string, LocatorCounter>): Promise<PwJsonCounts & {
11
+ startedAt: string | null;
12
+ finishedAt: string | null;
13
+ }>;
14
+ //# sourceMappingURL=pw-json-ingest.d.ts.map