@xera-ai/core 0.3.0 → 0.4.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.
- 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 +71 -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 +8351 -369
- 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-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/cost.d.ts +21 -0
- package/dist/graph/cost.d.ts.map +1 -0
- package/dist/graph/index.d.ts +8 -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 +177 -0
- package/dist/graph/schema.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 +339 -318
- package/package.json +19 -14
- 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-query.ts +43 -0
- package/src/bin-internal/graph-record-script.ts +191 -0
- package/src/bin-internal/graph-record.ts +243 -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 +8 -0
- package/src/bin-internal/verify-prompts.ts +1 -0
- package/src/config/schema.ts +6 -6
- package/src/graph/cost.ts +59 -0
- package/src/graph/index.ts +15 -0
- package/src/graph/paths.ts +27 -0
- package/src/graph/schema.ts +135 -0
- package/src/graph/store.ts +231 -0
- package/src/graph/types.ts +174 -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.0",
|
|
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/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,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
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { parse as parseYaml } from 'yaml';
|
|
5
|
+
import { appendEvents } from '../graph/store';
|
|
6
|
+
import type {
|
|
7
|
+
EdgeDiscoveredPayload,
|
|
8
|
+
Event,
|
|
9
|
+
PomPromotedPayload,
|
|
10
|
+
RunClassifiedPayload,
|
|
11
|
+
RunCompletedPayload,
|
|
12
|
+
TicketFetchedPayload,
|
|
13
|
+
} from '../graph/types';
|
|
14
|
+
import { SCHEMA_VERSION } from '../graph/types';
|
|
15
|
+
import { ulid } from '../graph/ulid';
|
|
16
|
+
|
|
17
|
+
function nowIso(): string {
|
|
18
|
+
return new Date().toISOString();
|
|
19
|
+
}
|
|
20
|
+
function sha1(s: string): string {
|
|
21
|
+
return createHash('sha1').update(s).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
function scenarioId(ticket: string, name: string): string {
|
|
24
|
+
return sha1(`${ticket}:${name.trim().toLowerCase().replace(/\s+/g, ' ')}`);
|
|
25
|
+
}
|
|
26
|
+
function pomId(filePath: string): string {
|
|
27
|
+
return sha1(basename(filePath));
|
|
28
|
+
}
|
|
29
|
+
function makeEvent<T extends Event['type']>(
|
|
30
|
+
actor: string,
|
|
31
|
+
type: T,
|
|
32
|
+
payload: Extract<Event, { type: T }>['payload'],
|
|
33
|
+
): Event {
|
|
34
|
+
return {
|
|
35
|
+
event_id: ulid(),
|
|
36
|
+
schema_version: SCHEMA_VERSION,
|
|
37
|
+
ts: nowIso(),
|
|
38
|
+
actor,
|
|
39
|
+
type,
|
|
40
|
+
payload,
|
|
41
|
+
} as Event;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface StoryFrontmatter {
|
|
45
|
+
ticketId: string;
|
|
46
|
+
summary: string;
|
|
47
|
+
storyHash: string;
|
|
48
|
+
acceptanceCriteria?: string[];
|
|
49
|
+
linked_issues?: Array<{
|
|
50
|
+
ticketId: string;
|
|
51
|
+
relation: 'blocks' | 'duplicates' | 'relates' | 'supersedes';
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readStoryFrontmatter(repoRoot: string, ticket: string): StoryFrontmatter | null {
|
|
56
|
+
const path = join(repoRoot, '.xera', ticket, 'story.md');
|
|
57
|
+
if (!existsSync(path)) return null;
|
|
58
|
+
const raw = readFileSync(path, 'utf8');
|
|
59
|
+
const m = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
60
|
+
if (!m) return null;
|
|
61
|
+
return parseYaml(m[1]!) as StoryFrontmatter;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readGraphInput(repoRoot: string, ticket: string): { modifiesAreas: string[] } {
|
|
65
|
+
const path = join(repoRoot, '.xera', ticket, 'graph-input.json');
|
|
66
|
+
if (!existsSync(path)) return { modifiesAreas: [] };
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
69
|
+
} catch {
|
|
70
|
+
return { modifiesAreas: [] };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function recordFetch(repoRoot: string, ticket: string): Promise<number> {
|
|
75
|
+
const fm = readStoryFrontmatter(repoRoot, ticket);
|
|
76
|
+
if (!fm) {
|
|
77
|
+
console.error(`[graph-record fetch] story.md not found for ${ticket}`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
const { modifiesAreas } = readGraphInput(repoRoot, ticket);
|
|
81
|
+
const events: Event[] = [];
|
|
82
|
+
const fetchedPayload: TicketFetchedPayload = {
|
|
83
|
+
ticketId: fm.ticketId,
|
|
84
|
+
summary: fm.summary,
|
|
85
|
+
ac: fm.acceptanceCriteria ?? [],
|
|
86
|
+
jiraLinks: fm.linked_issues ?? [],
|
|
87
|
+
storyHash: fm.storyHash,
|
|
88
|
+
modifiesAreas,
|
|
89
|
+
};
|
|
90
|
+
events.push(makeEvent('xera-fetch', 'ticket.fetched', fetchedPayload));
|
|
91
|
+
for (const link of fm.linked_issues ?? []) {
|
|
92
|
+
const p: EdgeDiscoveredPayload = {
|
|
93
|
+
kind: 'jira-linked',
|
|
94
|
+
from: fm.ticketId,
|
|
95
|
+
to: link.ticketId,
|
|
96
|
+
source: `jira:${link.relation}`,
|
|
97
|
+
};
|
|
98
|
+
events.push(makeEvent('xera-fetch', 'edge.discovered', p));
|
|
99
|
+
}
|
|
100
|
+
for (const area of modifiesAreas) {
|
|
101
|
+
const p: EdgeDiscoveredPayload = {
|
|
102
|
+
kind: 'modifies',
|
|
103
|
+
from: fm.ticketId,
|
|
104
|
+
to: area,
|
|
105
|
+
source: 'extract-areas',
|
|
106
|
+
};
|
|
107
|
+
events.push(makeEvent('xera-fetch', 'edge.discovered', p));
|
|
108
|
+
}
|
|
109
|
+
appendEvents(repoRoot, events, { skill: 'xera-fetch', ticketId: ticket });
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function recordScript(repoRoot: string, ticket: string): Promise<number> {
|
|
114
|
+
const { recordScriptImpl } = await import('./graph-record-script');
|
|
115
|
+
return recordScriptImpl(repoRoot, ticket);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function recordExec(repoRoot: string, ticket: string, runId: string): Promise<number> {
|
|
119
|
+
const reporterPath = join(repoRoot, '.xera', ticket, 'runs', runId, 'reporter.json');
|
|
120
|
+
if (!existsSync(reporterPath)) {
|
|
121
|
+
console.error(`[graph-record exec] reporter.json missing`);
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
const data = JSON.parse(readFileSync(reporterPath, 'utf8')) as {
|
|
125
|
+
scenarios: Array<{ name: string; status: 'pass' | 'fail'; runtime: number; traceId?: string }>;
|
|
126
|
+
};
|
|
127
|
+
const events: Event[] = [];
|
|
128
|
+
for (const s of data.scenarios) {
|
|
129
|
+
const p: RunCompletedPayload = {
|
|
130
|
+
scenarioId: scenarioId(ticket, s.name),
|
|
131
|
+
ticketId: ticket,
|
|
132
|
+
runId,
|
|
133
|
+
status: s.status,
|
|
134
|
+
runtime: s.runtime,
|
|
135
|
+
};
|
|
136
|
+
if (s.traceId) p.traceId = s.traceId;
|
|
137
|
+
events.push(makeEvent('xera-exec', 'run.completed', p));
|
|
138
|
+
}
|
|
139
|
+
appendEvents(repoRoot, events, { skill: 'xera-exec', ticketId: ticket });
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function recordClassify(repoRoot: string, ticket: string, runId: string): Promise<number> {
|
|
144
|
+
const classifyPath = join(repoRoot, '.xera', ticket, 'runs', runId, 'classifier-output.json');
|
|
145
|
+
if (!existsSync(classifyPath)) {
|
|
146
|
+
console.error(`[graph-record classify] classifier-output.json missing`);
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
const data = JSON.parse(readFileSync(classifyPath, 'utf8')) as {
|
|
150
|
+
scenarios: Array<{ name: string; class: string; confidence: 'low' | 'medium' | 'high' }>;
|
|
151
|
+
};
|
|
152
|
+
const events: Event[] = [];
|
|
153
|
+
for (const s of data.scenarios) {
|
|
154
|
+
const p: RunClassifiedPayload = {
|
|
155
|
+
scenarioId: scenarioId(ticket, s.name),
|
|
156
|
+
runId,
|
|
157
|
+
classification: s.class as RunClassifiedPayload['classification'],
|
|
158
|
+
confidence: s.confidence,
|
|
159
|
+
};
|
|
160
|
+
events.push(makeEvent('xera-report', 'run.classified', p));
|
|
161
|
+
}
|
|
162
|
+
appendEvents(repoRoot, events, { skill: 'xera-report', ticketId: ticket });
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function recordPromote(repoRoot: string, args: Map<string, string>): Promise<number> {
|
|
167
|
+
const from = args.get('--from');
|
|
168
|
+
const to = args.get('--to');
|
|
169
|
+
const pomIdArg = args.get('--pom-id');
|
|
170
|
+
if (!from || !to) {
|
|
171
|
+
console.error(`[graph-record promote] --from and --to required`);
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
const id = pomIdArg ?? pomId(from);
|
|
175
|
+
const p: PomPromotedPayload = { pomId: id, fromPath: from, toPath: to };
|
|
176
|
+
const e = makeEvent('xera-promote', 'pom.promoted', p);
|
|
177
|
+
appendEvents(repoRoot, [e], { skill: 'xera-promote', ticketId: 'shared' });
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseFlags(args: string[]): Map<string, string> {
|
|
182
|
+
const m = new Map<string, string>();
|
|
183
|
+
for (let i = 0; i < args.length; i++) {
|
|
184
|
+
if (args[i]!.startsWith('--')) {
|
|
185
|
+
m.set(args[i]!, args[i + 1] ?? '');
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return m;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function graphRecordCmd(argv: string[]): Promise<number> {
|
|
193
|
+
const [action, ...rest] = argv;
|
|
194
|
+
if (!action) {
|
|
195
|
+
console.error(`Usage: xera-internal graph-record <fetch|script|exec|classify|promote> [args]`);
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
const repoRoot = process.cwd();
|
|
199
|
+
switch (action) {
|
|
200
|
+
case 'fetch': {
|
|
201
|
+
const ticket = rest[0];
|
|
202
|
+
if (!ticket) {
|
|
203
|
+
console.error('ticket required');
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
return recordFetch(repoRoot, ticket);
|
|
207
|
+
}
|
|
208
|
+
case 'script': {
|
|
209
|
+
const ticket = rest[0];
|
|
210
|
+
if (!ticket) {
|
|
211
|
+
console.error('ticket required');
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
return recordScript(repoRoot, ticket);
|
|
215
|
+
}
|
|
216
|
+
case 'exec': {
|
|
217
|
+
const ticket = rest[0];
|
|
218
|
+
const flags = parseFlags(rest);
|
|
219
|
+
const runId = flags.get('--run-id');
|
|
220
|
+
if (!ticket || !runId) {
|
|
221
|
+
console.error('ticket + --run-id required');
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
return recordExec(repoRoot, ticket, runId);
|
|
225
|
+
}
|
|
226
|
+
case 'classify': {
|
|
227
|
+
const ticket = rest[0];
|
|
228
|
+
const flags = parseFlags(rest);
|
|
229
|
+
const runId = flags.get('--run-id');
|
|
230
|
+
if (!ticket || !runId) {
|
|
231
|
+
console.error('ticket + --run-id required');
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
return recordClassify(repoRoot, ticket, runId);
|
|
235
|
+
}
|
|
236
|
+
case 'promote': {
|
|
237
|
+
return recordPromote(repoRoot, parseFlags(rest));
|
|
238
|
+
}
|
|
239
|
+
default:
|
|
240
|
+
console.error(`Unknown action: ${action}`);
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { deriveSnapshot, isSnapshotStale, loadAllEvents, writeSnapshot } from '../graph/store';
|
|
2
|
+
|
|
3
|
+
export async function graphSnapshotCmd(argv: string[]): Promise<number> {
|
|
4
|
+
const check = argv.includes('--check');
|
|
5
|
+
const noRebuild = argv.includes('--no-rebuild');
|
|
6
|
+
const repoRoot = process.cwd();
|
|
7
|
+
const stale = isSnapshotStale(repoRoot);
|
|
8
|
+
if (check) {
|
|
9
|
+
if (!stale) return 0;
|
|
10
|
+
if (noRebuild) {
|
|
11
|
+
console.error('[graph-snapshot] stale');
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
// fall through to rebuild
|
|
15
|
+
}
|
|
16
|
+
const events = loadAllEvents(repoRoot);
|
|
17
|
+
const snap = deriveSnapshot(events);
|
|
18
|
+
writeSnapshot(repoRoot, snap);
|
|
19
|
+
if (check && stale) {
|
|
20
|
+
console.log(`[graph-snapshot] rebuilt (${events.length} events)`);
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { scrubFreeText } from '@xera-ai/web';
|
|
4
4
|
import { unzipSync } from 'fflate';
|
|
@@ -4,6 +4,10 @@ import { evalPrepareCmd } from './eval-prepare';
|
|
|
4
4
|
import { evalReportCmd } from './eval-report';
|
|
5
5
|
import { execCmd } from './exec';
|
|
6
6
|
import { fetchCmd } from './fetch';
|
|
7
|
+
import { graphBackfillCmd } from './graph-backfill';
|
|
8
|
+
import { graphQueryCmd } from './graph-query';
|
|
9
|
+
import { graphRecordCmd } from './graph-record';
|
|
10
|
+
import { graphSnapshotCmd } from './graph-snapshot';
|
|
7
11
|
import { healPrepareCmd } from './heal-prepare';
|
|
8
12
|
import { lintCmd } from './lint';
|
|
9
13
|
import { normalizeCmd } from './normalize';
|
|
@@ -23,6 +27,10 @@ const COMMANDS: Record<string, (argv: string[]) => Promise<number>> = {
|
|
|
23
27
|
'eval-report': evalReportCmd,
|
|
24
28
|
exec: execCmd,
|
|
25
29
|
fetch: fetchCmd,
|
|
30
|
+
'graph-backfill': graphBackfillCmd,
|
|
31
|
+
'graph-query': graphQueryCmd,
|
|
32
|
+
'graph-record': graphRecordCmd,
|
|
33
|
+
'graph-snapshot': graphSnapshotCmd,
|
|
26
34
|
'heal-prepare': healPrepareCmd,
|
|
27
35
|
lint: lintCmd,
|
|
28
36
|
normalize: normalizeCmd,
|