@xera-ai/core 0.3.0 → 0.4.1
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/bin/internal.ts +1 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/artifact/hash.d.ts.map +1 -0
- package/dist/artifact/meta.d.ts +20 -0
- package/dist/artifact/meta.d.ts.map +1 -0
- package/dist/artifact/paths.d.ts.map +1 -0
- package/dist/artifact/status.d.ts +75 -0
- package/dist/artifact/status.d.ts.map +1 -0
- package/dist/auth/encrypt.d.ts.map +1 -0
- package/dist/auth/key.d.ts.map +1 -0
- package/dist/auth/refresh.d.ts.map +1 -0
- package/dist/{core/src/auth → auth}/state.d.ts +5 -14
- package/dist/auth/state.d.ts.map +1 -0
- package/dist/bin/internal.js +8607 -373
- package/dist/bin-internal/doctor.d.ts.map +1 -0
- package/dist/bin-internal/eval-deterministic.d.ts.map +1 -0
- package/dist/bin-internal/eval-prepare.d.ts.map +1 -0
- package/dist/bin-internal/eval-report.d.ts.map +1 -0
- package/dist/bin-internal/exec.d.ts.map +1 -0
- package/dist/bin-internal/fetch.d.ts.map +1 -0
- package/dist/bin-internal/graph-backfill.d.ts +2 -0
- package/dist/bin-internal/graph-backfill.d.ts.map +1 -0
- package/dist/bin-internal/graph-enrich.d.ts +2 -0
- package/dist/bin-internal/graph-enrich.d.ts.map +1 -0
- package/dist/bin-internal/graph-query.d.ts +2 -0
- package/dist/bin-internal/graph-query.d.ts.map +1 -0
- package/dist/bin-internal/graph-record-script.d.ts +2 -0
- package/dist/bin-internal/graph-record-script.d.ts.map +1 -0
- package/dist/bin-internal/graph-record.d.ts +3 -0
- package/dist/bin-internal/graph-record.d.ts.map +1 -0
- package/dist/bin-internal/graph-snapshot.d.ts +2 -0
- package/dist/bin-internal/graph-snapshot.d.ts.map +1 -0
- package/dist/bin-internal/heal-prepare.d.ts.map +1 -0
- package/dist/bin-internal/index.d.ts.map +1 -0
- package/dist/bin-internal/lint.d.ts.map +1 -0
- package/dist/bin-internal/normalize.d.ts.map +1 -0
- package/dist/bin-internal/post.d.ts.map +1 -0
- package/dist/bin-internal/promote.d.ts.map +1 -0
- package/dist/bin-internal/report.d.ts.map +1 -0
- package/dist/bin-internal/status-cmd.d.ts.map +1 -0
- package/dist/bin-internal/typecheck.d.ts.map +1 -0
- package/dist/bin-internal/unlock.d.ts.map +1 -0
- package/dist/bin-internal/validate-feature.d.ts.map +1 -0
- package/dist/bin-internal/verify-prompts.d.ts.map +1 -0
- package/dist/classifier/aggregate.d.ts.map +1 -0
- package/dist/classifier/history.d.ts.map +1 -0
- package/dist/classifier/types.d.ts.map +1 -0
- package/dist/config/define.d.ts.map +1 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/config/schema.d.ts +66 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/eval/paths.d.ts.map +1 -0
- package/dist/eval/run-id.d.ts.map +1 -0
- package/dist/eval/types.d.ts +203 -0
- package/dist/eval/types.d.ts.map +1 -0
- package/dist/graph/classify.d.ts +42 -0
- package/dist/graph/classify.d.ts.map +1 -0
- package/dist/graph/cost.d.ts +21 -0
- package/dist/graph/cost.d.ts.map +1 -0
- package/dist/graph/enrich.d.ts +10 -0
- package/dist/graph/enrich.d.ts.map +1 -0
- package/dist/graph/index.d.ts +13 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/paths.d.ts +10 -0
- package/dist/graph/paths.d.ts.map +1 -0
- package/dist/graph/schema.d.ts +180 -0
- package/dist/graph/schema.d.ts.map +1 -0
- package/dist/graph/similarity.d.ts +3 -0
- package/dist/graph/similarity.d.ts.map +1 -0
- package/dist/graph/store.d.ts +14 -0
- package/dist/graph/store.d.ts.map +1 -0
- package/dist/graph/types.d.ts +151 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/ulid.d.ts +2 -0
- package/dist/graph/ulid.d.ts.map +1 -0
- package/dist/{core/src/index.d.ts → index.d.ts} +11 -11
- package/dist/index.d.ts.map +1 -0
- package/dist/jira/client.d.ts.map +1 -0
- package/dist/jira/fields.d.ts.map +1 -0
- package/dist/jira/mcp-backend.d.ts.map +1 -0
- package/dist/jira/rest-backend.d.ts.map +1 -0
- package/dist/jira/retry.d.ts.map +1 -0
- package/dist/jira/types.d.ts.map +1 -0
- package/dist/lock/file-lock.d.ts.map +1 -0
- package/dist/logging/ndjson-logger.d.ts.map +1 -0
- package/dist/reporter/jira-comment.d.ts.map +1 -0
- package/dist/reporter/status-writer.d.ts.map +1 -0
- package/dist/src/index.js +346 -318
- package/package.json +19 -14
- package/src/artifact/status.ts +8 -1
- package/src/auth/refresh.ts +1 -0
- package/src/bin-internal/doctor.ts +37 -1
- package/src/bin-internal/eval-prepare.ts +1 -1
- package/src/bin-internal/graph-backfill.ts +43 -0
- package/src/bin-internal/graph-enrich.ts +28 -0
- package/src/bin-internal/graph-query.ts +43 -0
- package/src/bin-internal/graph-record-script.ts +191 -0
- package/src/bin-internal/graph-record.ts +287 -0
- package/src/bin-internal/graph-snapshot.ts +23 -0
- package/src/bin-internal/heal-prepare.ts +1 -1
- package/src/bin-internal/index.ts +10 -0
- package/src/bin-internal/report.ts +63 -5
- package/src/bin-internal/verify-prompts.ts +3 -0
- package/src/classifier/aggregate.ts +1 -0
- package/src/config/schema.ts +6 -6
- package/src/graph/classify.ts +126 -0
- package/src/graph/cost.ts +59 -0
- package/src/graph/enrich.ts +103 -0
- package/src/graph/index.ts +30 -0
- package/src/graph/paths.ts +27 -0
- package/src/graph/schema.ts +142 -0
- package/src/graph/similarity.ts +43 -0
- package/src/graph/store.ts +231 -0
- package/src/graph/types.ts +179 -0
- package/src/graph/ulid.ts +58 -0
- package/src/index.ts +11 -11
- package/src/jira/rest-backend.ts +1 -1
- package/src/reporter/status-writer.ts +1 -1
- package/dist/core/src/adapter/types.d.ts.map +0 -1
- package/dist/core/src/artifact/hash.d.ts.map +0 -1
- package/dist/core/src/artifact/meta.d.ts +0 -46
- package/dist/core/src/artifact/meta.d.ts.map +0 -1
- package/dist/core/src/artifact/paths.d.ts.map +0 -1
- package/dist/core/src/artifact/status.d.ts +0 -96
- package/dist/core/src/artifact/status.d.ts.map +0 -1
- package/dist/core/src/auth/encrypt.d.ts.map +0 -1
- package/dist/core/src/auth/key.d.ts.map +0 -1
- package/dist/core/src/auth/refresh.d.ts.map +0 -1
- package/dist/core/src/auth/state.d.ts.map +0 -1
- package/dist/core/src/bin-internal/doctor.d.ts.map +0 -1
- package/dist/core/src/bin-internal/eval-deterministic.d.ts.map +0 -1
- package/dist/core/src/bin-internal/eval-prepare.d.ts.map +0 -1
- package/dist/core/src/bin-internal/eval-report.d.ts.map +0 -1
- package/dist/core/src/bin-internal/exec.d.ts.map +0 -1
- package/dist/core/src/bin-internal/fetch.d.ts.map +0 -1
- package/dist/core/src/bin-internal/heal-prepare.d.ts.map +0 -1
- package/dist/core/src/bin-internal/index.d.ts.map +0 -1
- package/dist/core/src/bin-internal/lint.d.ts.map +0 -1
- package/dist/core/src/bin-internal/normalize.d.ts.map +0 -1
- package/dist/core/src/bin-internal/post.d.ts.map +0 -1
- package/dist/core/src/bin-internal/promote.d.ts.map +0 -1
- package/dist/core/src/bin-internal/report.d.ts.map +0 -1
- package/dist/core/src/bin-internal/status-cmd.d.ts.map +0 -1
- package/dist/core/src/bin-internal/typecheck.d.ts.map +0 -1
- package/dist/core/src/bin-internal/unlock.d.ts.map +0 -1
- package/dist/core/src/bin-internal/validate-feature.d.ts.map +0 -1
- package/dist/core/src/bin-internal/verify-prompts.d.ts.map +0 -1
- package/dist/core/src/classifier/aggregate.d.ts.map +0 -1
- package/dist/core/src/classifier/history.d.ts.map +0 -1
- package/dist/core/src/classifier/types.d.ts.map +0 -1
- package/dist/core/src/config/define.d.ts.map +0 -1
- package/dist/core/src/config/load.d.ts.map +0 -1
- package/dist/core/src/config/schema.d.ts +0 -326
- package/dist/core/src/config/schema.d.ts.map +0 -1
- package/dist/core/src/eval/paths.d.ts.map +0 -1
- package/dist/core/src/eval/run-id.d.ts.map +0 -1
- package/dist/core/src/eval/types.d.ts +0 -551
- package/dist/core/src/eval/types.d.ts.map +0 -1
- package/dist/core/src/index.d.ts.map +0 -1
- package/dist/core/src/jira/client.d.ts.map +0 -1
- package/dist/core/src/jira/fields.d.ts.map +0 -1
- package/dist/core/src/jira/mcp-backend.d.ts.map +0 -1
- package/dist/core/src/jira/rest-backend.d.ts.map +0 -1
- package/dist/core/src/jira/retry.d.ts.map +0 -1
- package/dist/core/src/jira/types.d.ts.map +0 -1
- package/dist/core/src/lock/file-lock.d.ts.map +0 -1
- package/dist/core/src/logging/ndjson-logger.d.ts.map +0 -1
- package/dist/core/src/reporter/jira-comment.d.ts.map +0 -1
- package/dist/core/src/reporter/status-writer.d.ts.map +0 -1
- package/dist/web/src/adapter.d.ts +0 -3
- package/dist/web/src/adapter.d.ts.map +0 -1
- package/dist/web/src/auth-setup/define.d.ts +0 -16
- package/dist/web/src/auth-setup/define.d.ts.map +0 -1
- package/dist/web/src/auth-setup/playwright-state.d.ts +0 -2
- package/dist/web/src/auth-setup/playwright-state.d.ts.map +0 -1
- package/dist/web/src/auth-setup/runner.d.ts +0 -12
- package/dist/web/src/auth-setup/runner.d.ts.map +0 -1
- package/dist/web/src/executor/index.d.ts +0 -18
- package/dist/web/src/executor/index.d.ts.map +0 -1
- package/dist/web/src/executor/playwright-args.d.ts +0 -7
- package/dist/web/src/executor/playwright-args.d.ts.map +0 -1
- package/dist/web/src/generator/gherkin-validate.d.ts +0 -9
- package/dist/web/src/generator/gherkin-validate.d.ts.map +0 -1
- package/dist/web/src/generator/lint.d.ts +0 -9
- package/dist/web/src/generator/lint.d.ts.map +0 -1
- package/dist/web/src/generator/pom-scan.d.ts +0 -6
- package/dist/web/src/generator/pom-scan.d.ts.map +0 -1
- package/dist/web/src/generator/promote.d.ts +0 -7
- package/dist/web/src/generator/promote.d.ts.map +0 -1
- package/dist/web/src/generator/selector-rules.d.ts +0 -10
- package/dist/web/src/generator/selector-rules.d.ts.map +0 -1
- package/dist/web/src/generator/typecheck.d.ts +0 -11
- package/dist/web/src/generator/typecheck.d.ts.map +0 -1
- package/dist/web/src/index.d.ts +0 -18
- package/dist/web/src/index.d.ts.map +0 -1
- package/dist/web/src/trace-normalizer/normalize.d.ts +0 -7
- package/dist/web/src/trace-normalizer/normalize.d.ts.map +0 -1
- package/dist/web/src/trace-normalizer/parse.d.ts +0 -37
- package/dist/web/src/trace-normalizer/parse.d.ts.map +0 -1
- package/dist/web/src/trace-normalizer/scrub-rules.d.ts +0 -12
- package/dist/web/src/trace-normalizer/scrub-rules.d.ts.map +0 -1
- package/dist/web/src/trace-normalizer/scrub.d.ts +0 -29
- package/dist/web/src/trace-normalizer/scrub.d.ts.map +0 -1
- package/dist/web/src/trace-normalizer/unzip.d.ts +0 -6
- package/dist/web/src/trace-normalizer/unzip.d.ts.map +0 -1
- /package/dist/{core/src/adapter → adapter}/types.d.ts +0 -0
- /package/dist/{core/src/artifact → artifact}/hash.d.ts +0 -0
- /package/dist/{core/src/artifact → artifact}/paths.d.ts +0 -0
- /package/dist/{core/src/auth → auth}/encrypt.d.ts +0 -0
- /package/dist/{core/src/auth → auth}/key.d.ts +0 -0
- /package/dist/{core/src/auth → auth}/refresh.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/doctor.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/eval-deterministic.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/eval-prepare.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/eval-report.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/exec.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/fetch.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/heal-prepare.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/index.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/lint.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/normalize.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/post.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/promote.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/report.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/status-cmd.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/typecheck.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/unlock.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/validate-feature.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/verify-prompts.d.ts +0 -0
- /package/dist/{core/src/classifier → classifier}/aggregate.d.ts +0 -0
- /package/dist/{core/src/classifier → classifier}/history.d.ts +0 -0
- /package/dist/{core/src/classifier → classifier}/types.d.ts +0 -0
- /package/dist/{core/src/config → config}/define.d.ts +0 -0
- /package/dist/{core/src/config → config}/load.d.ts +0 -0
- /package/dist/{core/src/eval → eval}/paths.d.ts +0 -0
- /package/dist/{core/src/eval → eval}/run-id.d.ts +0 -0
- /package/dist/{core/src/jira → jira}/client.d.ts +0 -0
- /package/dist/{core/src/jira → jira}/fields.d.ts +0 -0
- /package/dist/{core/src/jira → jira}/mcp-backend.d.ts +0 -0
- /package/dist/{core/src/jira → jira}/rest-backend.d.ts +0 -0
- /package/dist/{core/src/jira → jira}/retry.d.ts +0 -0
- /package/dist/{core/src/jira → jira}/types.d.ts +0 -0
- /package/dist/{core/src/lock → lock}/file-lock.d.ts +0 -0
- /package/dist/{core/src/logging → logging}/ndjson-logger.d.ts +0 -0
- /package/dist/{core/src/reporter → reporter}/jira-comment.d.ts +0 -0
- /package/dist/{core/src/reporter → reporter}/status-writer.d.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xera-ai/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -17,17 +17,22 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"bin": {
|
|
20
|
-
|
|
21
|
-
},
|
|
22
|
-
"files": [
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
20
|
+
"xera-internal": "./dist/bin/internal.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src",
|
|
25
|
+
"bin"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "bun build ./src/index.ts ./bin/internal.ts --outdir ./dist --target bun --external @playwright/test --external @xera-ai/web --external zod",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"zod": "4.4.3",
|
|
33
|
+
"@xera-ai/web": "^0.2.0",
|
|
34
|
+
"@playwright/test": "1.60.0",
|
|
35
|
+
"fflate": "0.8.3",
|
|
36
|
+
"yaml": "2.9.0"
|
|
37
|
+
}
|
|
33
38
|
}
|
package/src/artifact/status.ts
CHANGED
|
@@ -2,7 +2,14 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
|
-
const ClassificationEnum = z.enum([
|
|
5
|
+
const ClassificationEnum = z.enum([
|
|
6
|
+
'PASS',
|
|
7
|
+
'REAL_BUG',
|
|
8
|
+
'SELECTOR_DRIFT',
|
|
9
|
+
'FLAKY',
|
|
10
|
+
'TEST_BUG',
|
|
11
|
+
'TEST_OUTDATED',
|
|
12
|
+
]);
|
|
6
13
|
const ResultEnum = z.enum(['PASS', 'FAIL']);
|
|
7
14
|
const ConfidenceEnum = z.enum(['low', 'medium', 'high']);
|
|
8
15
|
|
package/src/auth/refresh.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import type { Stage } from '../eval/types';
|
|
4
|
+
import { summarizeCost } from '../graph/cost';
|
|
5
|
+
import { loadAllEvents } from '../graph/store';
|
|
4
6
|
import { verifyPrompts } from './verify-prompts';
|
|
5
7
|
|
|
6
8
|
export interface DoctorOpts {
|
|
@@ -124,6 +126,40 @@ export async function doctorCmd(_argv: string[], opts: DoctorOpts = {}): Promise
|
|
|
124
126
|
...checkPromptInjectionPreamble(repoRoot),
|
|
125
127
|
...checkRootScripts(repoRoot),
|
|
126
128
|
];
|
|
129
|
+
// Cost summary (past 7 days)
|
|
130
|
+
const cost = summarizeCost(repoRoot, 7);
|
|
131
|
+
if (cost.totalCalls > 0) {
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log('LLM cost (past 7 days):');
|
|
134
|
+
console.log(` Total calls: ${cost.totalCalls}`);
|
|
135
|
+
console.log(` Estimated: $${cost.totalUsd.toFixed(2)} USD`);
|
|
136
|
+
const top = Object.entries(cost.bySkill).sort((a, b) => b[1].usd - a[1].usd)[0];
|
|
137
|
+
if (top)
|
|
138
|
+
console.log(` Top skill: ${top[0]} (${top[1].calls} calls, $${top[1].usd.toFixed(2)})`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Backfill detection
|
|
142
|
+
const xeraDir = join(repoRoot, '.xera');
|
|
143
|
+
if (existsSync(xeraDir)) {
|
|
144
|
+
const ticketDirs = readdirSync(xeraDir, { withFileTypes: true }).filter(
|
|
145
|
+
(e) => e.isDirectory() && /^[A-Z]+-\d+$/.test(e.name),
|
|
146
|
+
);
|
|
147
|
+
if (ticketDirs.length > 0) {
|
|
148
|
+
const events = loadAllEvents(repoRoot);
|
|
149
|
+
const fetchedTickets = new Set(
|
|
150
|
+
events.filter((e) => e.type === 'ticket.fetched').map((e) => e.payload.ticketId),
|
|
151
|
+
);
|
|
152
|
+
const unbackfilled = ticketDirs.map((d) => d.name).filter((t) => !fetchedTickets.has(t));
|
|
153
|
+
if (unbackfilled.length > 0) {
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log(`⚠ Graph: ${unbackfilled.length} ticket(s) not yet in graph.`);
|
|
156
|
+
console.log(` These won't participate in v0.6.1+ features (TEST_OUTDATED, /xera-impact).`);
|
|
157
|
+
console.log(` Run: bun run xera:graph-backfill`);
|
|
158
|
+
console.log(` (Use --dry-run to preview.)`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
127
163
|
if (results.length === 0) {
|
|
128
164
|
console.log('[xera:doctor] ok');
|
|
129
165
|
return 0;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { recordScriptImpl } from './graph-record-script';
|
|
4
|
+
|
|
5
|
+
async function backfillTicket(repoRoot: string, ticket: string, dryRun: boolean): Promise<number> {
|
|
6
|
+
// 1) Synthesize ticket.fetched from story.md (use story.md mtime)
|
|
7
|
+
const storyPath = join(repoRoot, '.xera', ticket, 'story.md');
|
|
8
|
+
if (!existsSync(storyPath)) return 0;
|
|
9
|
+
|
|
10
|
+
const { recordFetch } = await import('./graph-record');
|
|
11
|
+
// Re-use the same code path; in dry-run we don't actually call appendEvents.
|
|
12
|
+
// For simplicity in v0.6.0, dry-run lists ticket count and returns.
|
|
13
|
+
if (dryRun) {
|
|
14
|
+
console.log(`[backfill dry-run] would backfill ${ticket}`);
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
// recordScriptImpl handles scenario/POM extraction.
|
|
18
|
+
await recordScriptImpl(repoRoot, ticket);
|
|
19
|
+
await recordFetch(repoRoot, ticket);
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function graphBackfillCmd(argv: string[]): Promise<number> {
|
|
24
|
+
const dryRun = argv.includes('--dry-run');
|
|
25
|
+
const repoRoot = process.cwd();
|
|
26
|
+
const xeraDir = join(repoRoot, '.xera');
|
|
27
|
+
if (!existsSync(xeraDir)) {
|
|
28
|
+
console.log('[backfill] no .xera/ directory');
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
const tickets: string[] = [];
|
|
32
|
+
for (const entry of readdirSync(xeraDir, { withFileTypes: true })) {
|
|
33
|
+
if (!entry.isDirectory()) continue;
|
|
34
|
+
if (entry.name === 'graph') continue;
|
|
35
|
+
if (entry.name.startsWith('.')) continue;
|
|
36
|
+
if (!/^[A-Z]+-\d+$/.test(entry.name)) continue;
|
|
37
|
+
tickets.push(entry.name);
|
|
38
|
+
}
|
|
39
|
+
console.log(`[backfill] found ${tickets.length} tickets`);
|
|
40
|
+
for (const t of tickets) await backfillTicket(repoRoot, t, dryRun);
|
|
41
|
+
console.log(`[backfill] done`);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { enrichTicket } from '../graph/enrich';
|
|
2
|
+
|
|
3
|
+
export async function graphEnrichCmd(argv: string[]): Promise<number> {
|
|
4
|
+
let ticket: string | undefined;
|
|
5
|
+
let force = false;
|
|
6
|
+
for (let i = 0; i < argv.length; i++) {
|
|
7
|
+
if (argv[i] === '--ticket') ticket = argv[++i];
|
|
8
|
+
else if (argv[i] === '--force') force = true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const repoRoot = process.cwd();
|
|
12
|
+
|
|
13
|
+
if (!ticket) {
|
|
14
|
+
console.error('[graph-enrich] usage: graph-enrich --ticket <id> [--force]');
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const result = await enrichTicket(repoRoot, ticket, { force });
|
|
20
|
+
console.log(
|
|
21
|
+
`[graph-enrich] ${ticket} enriched (${result.similarCount} similar edges, at ${result.enrichedAt})`,
|
|
22
|
+
);
|
|
23
|
+
return 0;
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error(`[graph-enrich] ${ticket} failed: ${(e as Error).message}`);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { deriveSnapshot, loadAllEvents } from '../graph/store';
|
|
2
|
+
import type { Snapshot } from '../graph/types';
|
|
3
|
+
|
|
4
|
+
function filterByTicket(snap: Snapshot, ticket: string): Snapshot {
|
|
5
|
+
const out: Snapshot = {
|
|
6
|
+
...snap,
|
|
7
|
+
tickets: snap.tickets[ticket] ? { [ticket]: snap.tickets[ticket]! } : {},
|
|
8
|
+
scenarios: Object.fromEntries(
|
|
9
|
+
Object.entries(snap.scenarios).filter(([, s]) => s.ticketId === ticket),
|
|
10
|
+
),
|
|
11
|
+
poms: Object.fromEntries(Object.entries(snap.poms).filter(([, p]) => p.ticketId === ticket)),
|
|
12
|
+
edges: snap.edges.filter((e) => e.from === ticket || e.to === ticket),
|
|
13
|
+
};
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function renderText(snap: Snapshot): string {
|
|
18
|
+
const out: string[] = [];
|
|
19
|
+
out.push(`Graph snapshot — ${snap.event_count} events`);
|
|
20
|
+
out.push(`Tickets: ${Object.keys(snap.tickets).length}`);
|
|
21
|
+
out.push(`Scenarios: ${Object.keys(snap.scenarios).length}`);
|
|
22
|
+
out.push(`POMs: ${Object.keys(snap.poms).length}`);
|
|
23
|
+
out.push(`Edges: ${snap.edges.length}`);
|
|
24
|
+
for (const t of Object.values(snap.tickets)) {
|
|
25
|
+
out.push(` ${t.id} — ${t.summary}`);
|
|
26
|
+
}
|
|
27
|
+
return out.join('\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function graphQueryCmd(argv: string[]): Promise<number> {
|
|
31
|
+
let ticket: string | undefined;
|
|
32
|
+
let format: 'text' | 'json' = 'text';
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
if (argv[i] === '--ticket') ticket = argv[++i];
|
|
35
|
+
else if (argv[i] === '--format') format = argv[++i] as 'text' | 'json';
|
|
36
|
+
}
|
|
37
|
+
const repoRoot = process.cwd();
|
|
38
|
+
let snap = deriveSnapshot(loadAllEvents(repoRoot));
|
|
39
|
+
if (ticket) snap = filterByTicket(snap, ticket);
|
|
40
|
+
if (format === 'json') process.stdout.write(JSON.stringify(snap, null, 2));
|
|
41
|
+
else process.stdout.write(renderText(snap));
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { appendEvents } from '../graph/store';
|
|
5
|
+
import type {
|
|
6
|
+
EdgeDiscoveredPayload,
|
|
7
|
+
Event,
|
|
8
|
+
PomGeneratedPayload,
|
|
9
|
+
ScenarioGeneratedPayload,
|
|
10
|
+
} from '../graph/types';
|
|
11
|
+
import { SCHEMA_VERSION } from '../graph/types';
|
|
12
|
+
import { ulid } from '../graph/ulid';
|
|
13
|
+
|
|
14
|
+
const sha1 = (s: string) => createHash('sha1').update(s).digest('hex');
|
|
15
|
+
const sId = (ticket: string, name: string) =>
|
|
16
|
+
sha1(`${ticket}:${name.trim().toLowerCase().replace(/\s+/g, ' ')}`);
|
|
17
|
+
const pId = (file: string) => sha1(basename(file));
|
|
18
|
+
const nowIso = () => new Date().toISOString();
|
|
19
|
+
const mk = <T extends Event['type']>(
|
|
20
|
+
actor: string,
|
|
21
|
+
type: T,
|
|
22
|
+
payload: Extract<Event, { type: T }>['payload'],
|
|
23
|
+
): Event =>
|
|
24
|
+
({
|
|
25
|
+
event_id: ulid(),
|
|
26
|
+
schema_version: SCHEMA_VERSION,
|
|
27
|
+
ts: nowIso(),
|
|
28
|
+
actor,
|
|
29
|
+
type,
|
|
30
|
+
payload,
|
|
31
|
+
}) as Event;
|
|
32
|
+
|
|
33
|
+
function parseFeature(
|
|
34
|
+
text: string,
|
|
35
|
+
): Array<{ name: string; priority: 'p0' | 'p1' | 'p2'; gherkin: string }> {
|
|
36
|
+
const scenarios: Array<{ name: string; priority: 'p0' | 'p1' | 'p2'; gherkin: string }> = [];
|
|
37
|
+
const lines = text.split('\n');
|
|
38
|
+
let currentTagPriority: 'p0' | 'p1' | 'p2' = 'p1';
|
|
39
|
+
let i = 0;
|
|
40
|
+
while (i < lines.length) {
|
|
41
|
+
const line = lines[i]!.trim();
|
|
42
|
+
if (line.startsWith('@')) {
|
|
43
|
+
const tag = line.slice(1).split(/\s+/)[0]!.toLowerCase();
|
|
44
|
+
if (tag === 'p0' || tag === 'p1' || tag === 'p2') currentTagPriority = tag;
|
|
45
|
+
i++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (line.startsWith('Scenario:') || line.startsWith('Scenario Outline:')) {
|
|
49
|
+
const name = line.replace(/^Scenario( Outline)?:\s*/, '');
|
|
50
|
+
const start = i;
|
|
51
|
+
i++;
|
|
52
|
+
while (
|
|
53
|
+
i < lines.length &&
|
|
54
|
+
!lines[i]!.trim().startsWith('Scenario') &&
|
|
55
|
+
!lines[i]!.trim().startsWith('@')
|
|
56
|
+
)
|
|
57
|
+
i++;
|
|
58
|
+
scenarios.push({
|
|
59
|
+
name,
|
|
60
|
+
priority: currentTagPriority,
|
|
61
|
+
gherkin: lines.slice(start, i).join('\n'),
|
|
62
|
+
});
|
|
63
|
+
currentTagPriority = 'p1';
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
return scenarios;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function listPomFiles(dir: string): string[] {
|
|
72
|
+
if (!existsSync(dir)) return [];
|
|
73
|
+
return readdirSync(dir)
|
|
74
|
+
.filter((f) => f.endsWith('.ts'))
|
|
75
|
+
.map((f) => join(dir, f));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function extractRoute(pomContent: string): string {
|
|
79
|
+
const m = pomContent.match(/goto\s*\(\s*['"]([^'"]+)['"]/);
|
|
80
|
+
return m ? m[1]! : '';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function extractLocators(pomContent: string): string[] {
|
|
84
|
+
const out: string[] = [];
|
|
85
|
+
const re = /\b(getByRole|getByLabel|getByText|getByTestId|locator)\s*\(\s*([^)]+)\)/g;
|
|
86
|
+
let m = re.exec(pomContent);
|
|
87
|
+
while (m !== null) {
|
|
88
|
+
out.push(`${m[1]}(${m[2]})`);
|
|
89
|
+
m = re.exec(pomContent);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function extractPomUsage(specContent: string): string[] {
|
|
95
|
+
const names = new Set<string>();
|
|
96
|
+
const re = /new\s+([A-Z][A-Za-z0-9]*Page)\s*\(/g;
|
|
97
|
+
let m = re.exec(specContent);
|
|
98
|
+
while (m !== null) {
|
|
99
|
+
names.add(m[1]!);
|
|
100
|
+
m = re.exec(specContent);
|
|
101
|
+
}
|
|
102
|
+
return [...names];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function recordScriptImpl(repoRoot: string, ticket: string): Promise<number> {
|
|
106
|
+
const ticketDir = join(repoRoot, '.xera', ticket);
|
|
107
|
+
const featurePath = join(ticketDir, 'feature', `${ticket}.feature`);
|
|
108
|
+
const specPath = join(ticketDir, 'tests', `${ticket}.spec.ts`);
|
|
109
|
+
const pomDir = join(ticketDir, 'poms');
|
|
110
|
+
|
|
111
|
+
if (!existsSync(featurePath)) {
|
|
112
|
+
console.error(`[graph-record script] feature missing`);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const featureText = readFileSync(featurePath, 'utf8');
|
|
117
|
+
const featureHash = sha1(featureText);
|
|
118
|
+
const scenarios = parseFeature(featureText);
|
|
119
|
+
|
|
120
|
+
const events: Event[] = [];
|
|
121
|
+
for (const s of scenarios) {
|
|
122
|
+
const id = sId(ticket, s.name);
|
|
123
|
+
const p: ScenarioGeneratedPayload = {
|
|
124
|
+
scenarioId: id,
|
|
125
|
+
ticketId: ticket,
|
|
126
|
+
name: s.name,
|
|
127
|
+
gherkin: s.gherkin,
|
|
128
|
+
priority: s.priority,
|
|
129
|
+
featureHash,
|
|
130
|
+
generatedAt: nowIso(),
|
|
131
|
+
};
|
|
132
|
+
events.push(mk('xera-script', 'scenario.generated', p));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pomFiles = listPomFiles(pomDir);
|
|
136
|
+
const pomNameToId = new Map<string, string>();
|
|
137
|
+
for (const pomFile of pomFiles) {
|
|
138
|
+
const content = readFileSync(pomFile, 'utf8');
|
|
139
|
+
const id = pId(pomFile);
|
|
140
|
+
const className = content.match(/export\s+class\s+([A-Z][A-Za-z0-9]*Page)/)?.[1] ?? '';
|
|
141
|
+
pomNameToId.set(className, id);
|
|
142
|
+
const pg: PomGeneratedPayload = {
|
|
143
|
+
pomId: id,
|
|
144
|
+
ticketId: ticket,
|
|
145
|
+
filePath: pomFile.replace(`${repoRoot}/`, ''),
|
|
146
|
+
route: extractRoute(content),
|
|
147
|
+
locators: extractLocators(content),
|
|
148
|
+
scope: 'local',
|
|
149
|
+
};
|
|
150
|
+
events.push(mk('xera-script', 'pom.generated', pg));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (existsSync(specPath)) {
|
|
154
|
+
const specContent = readFileSync(specPath, 'utf8');
|
|
155
|
+
const usedPoms = extractPomUsage(specContent);
|
|
156
|
+
for (const scenario of scenarios) {
|
|
157
|
+
const scId = sId(ticket, scenario.name);
|
|
158
|
+
for (const pomName of usedPoms) {
|
|
159
|
+
const pid = pomNameToId.get(pomName);
|
|
160
|
+
if (!pid) continue;
|
|
161
|
+
const ep: EdgeDiscoveredPayload = {
|
|
162
|
+
kind: 'uses',
|
|
163
|
+
from: scId,
|
|
164
|
+
to: pid,
|
|
165
|
+
source: 'xera-script',
|
|
166
|
+
};
|
|
167
|
+
events.push(mk('xera-script', 'edge.discovered', ep));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const [, id] of pomNameToId) {
|
|
173
|
+
const pom = events.find(
|
|
174
|
+
(e) => e.type === 'pom.generated' && (e.payload as PomGeneratedPayload).pomId === id,
|
|
175
|
+
);
|
|
176
|
+
if (!pom) continue;
|
|
177
|
+
const route = (pom.payload as PomGeneratedPayload).route;
|
|
178
|
+
if (!route) continue;
|
|
179
|
+
const slug =
|
|
180
|
+
route
|
|
181
|
+
.replace(/^\//, '')
|
|
182
|
+
.split('/')[0]!
|
|
183
|
+
.replace(/[^a-z0-9-]/gi, '-')
|
|
184
|
+
.toLowerCase() || 'root';
|
|
185
|
+
const ep: EdgeDiscoveredPayload = { kind: 'covers', from: id, to: slug, source: 'xera-script' };
|
|
186
|
+
events.push(mk('xera-script', 'edge.discovered', ep));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
appendEvents(repoRoot, events, { skill: 'xera-script', ticketId: ticket });
|
|
190
|
+
return 0;
|
|
191
|
+
}
|