@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
|
@@ -0,0 +1,287 @@
|
|
|
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
|
+
Classification,
|
|
8
|
+
ClassificationDisputedPayload,
|
|
9
|
+
EdgeDiscoveredPayload,
|
|
10
|
+
Event,
|
|
11
|
+
PomPromotedPayload,
|
|
12
|
+
RunClassifiedPayload,
|
|
13
|
+
RunCompletedPayload,
|
|
14
|
+
TicketFetchedPayload,
|
|
15
|
+
} from '../graph/types';
|
|
16
|
+
import { SCHEMA_VERSION } from '../graph/types';
|
|
17
|
+
import { ulid } from '../graph/ulid';
|
|
18
|
+
|
|
19
|
+
function nowIso(): string {
|
|
20
|
+
return new Date().toISOString();
|
|
21
|
+
}
|
|
22
|
+
function sha1(s: string): string {
|
|
23
|
+
return createHash('sha1').update(s).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
function scenarioId(ticket: string, name: string): string {
|
|
26
|
+
return sha1(`${ticket}:${name.trim().toLowerCase().replace(/\s+/g, ' ')}`);
|
|
27
|
+
}
|
|
28
|
+
function pomId(filePath: string): string {
|
|
29
|
+
return sha1(basename(filePath));
|
|
30
|
+
}
|
|
31
|
+
function makeEvent<T extends Event['type']>(
|
|
32
|
+
actor: string,
|
|
33
|
+
type: T,
|
|
34
|
+
payload: Extract<Event, { type: T }>['payload'],
|
|
35
|
+
): Event {
|
|
36
|
+
return {
|
|
37
|
+
event_id: ulid(),
|
|
38
|
+
schema_version: SCHEMA_VERSION,
|
|
39
|
+
ts: nowIso(),
|
|
40
|
+
actor,
|
|
41
|
+
type,
|
|
42
|
+
payload,
|
|
43
|
+
} as Event;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface StoryFrontmatter {
|
|
47
|
+
ticketId: string;
|
|
48
|
+
summary: string;
|
|
49
|
+
storyHash: string;
|
|
50
|
+
acceptanceCriteria?: string[];
|
|
51
|
+
linked_issues?: Array<{
|
|
52
|
+
ticketId: string;
|
|
53
|
+
relation: 'blocks' | 'duplicates' | 'relates' | 'supersedes';
|
|
54
|
+
}>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readStoryFrontmatter(repoRoot: string, ticket: string): StoryFrontmatter | null {
|
|
58
|
+
const path = join(repoRoot, '.xera', ticket, 'story.md');
|
|
59
|
+
if (!existsSync(path)) return null;
|
|
60
|
+
const raw = readFileSync(path, 'utf8');
|
|
61
|
+
const m = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
62
|
+
if (!m) return null;
|
|
63
|
+
return parseYaml(m[1]!) as StoryFrontmatter;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readGraphInput(repoRoot: string, ticket: string): { modifiesAreas: string[] } {
|
|
67
|
+
const path = join(repoRoot, '.xera', ticket, 'graph-input.json');
|
|
68
|
+
if (!existsSync(path)) return { modifiesAreas: [] };
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
71
|
+
} catch {
|
|
72
|
+
return { modifiesAreas: [] };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function recordFetch(repoRoot: string, ticket: string): Promise<number> {
|
|
77
|
+
const fm = readStoryFrontmatter(repoRoot, ticket);
|
|
78
|
+
if (!fm) {
|
|
79
|
+
console.error(`[graph-record fetch] story.md not found for ${ticket}`);
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
const { modifiesAreas } = readGraphInput(repoRoot, ticket);
|
|
83
|
+
const events: Event[] = [];
|
|
84
|
+
const fetchedPayload: TicketFetchedPayload = {
|
|
85
|
+
ticketId: fm.ticketId,
|
|
86
|
+
summary: fm.summary,
|
|
87
|
+
ac: fm.acceptanceCriteria ?? [],
|
|
88
|
+
jiraLinks: fm.linked_issues ?? [],
|
|
89
|
+
storyHash: fm.storyHash,
|
|
90
|
+
modifiesAreas,
|
|
91
|
+
};
|
|
92
|
+
events.push(makeEvent('xera-fetch', 'ticket.fetched', fetchedPayload));
|
|
93
|
+
for (const link of fm.linked_issues ?? []) {
|
|
94
|
+
const p: EdgeDiscoveredPayload = {
|
|
95
|
+
kind: 'jira-linked',
|
|
96
|
+
from: fm.ticketId,
|
|
97
|
+
to: link.ticketId,
|
|
98
|
+
source: `jira:${link.relation}`,
|
|
99
|
+
};
|
|
100
|
+
events.push(makeEvent('xera-fetch', 'edge.discovered', p));
|
|
101
|
+
}
|
|
102
|
+
for (const area of modifiesAreas) {
|
|
103
|
+
const p: EdgeDiscoveredPayload = {
|
|
104
|
+
kind: 'modifies',
|
|
105
|
+
from: fm.ticketId,
|
|
106
|
+
to: area,
|
|
107
|
+
source: 'extract-areas',
|
|
108
|
+
};
|
|
109
|
+
events.push(makeEvent('xera-fetch', 'edge.discovered', p));
|
|
110
|
+
}
|
|
111
|
+
appendEvents(repoRoot, events, { skill: 'xera-fetch', ticketId: ticket });
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function recordScript(repoRoot: string, ticket: string): Promise<number> {
|
|
116
|
+
const { recordScriptImpl } = await import('./graph-record-script');
|
|
117
|
+
return recordScriptImpl(repoRoot, ticket);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function recordExec(repoRoot: string, ticket: string, runId: string): Promise<number> {
|
|
121
|
+
const reporterPath = join(repoRoot, '.xera', ticket, 'runs', runId, 'reporter.json');
|
|
122
|
+
if (!existsSync(reporterPath)) {
|
|
123
|
+
console.error(`[graph-record exec] reporter.json missing`);
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
const data = JSON.parse(readFileSync(reporterPath, 'utf8')) as {
|
|
127
|
+
scenarios: Array<{ name: string; status: 'pass' | 'fail'; runtime: number; traceId?: string }>;
|
|
128
|
+
};
|
|
129
|
+
const events: Event[] = [];
|
|
130
|
+
for (const s of data.scenarios) {
|
|
131
|
+
const p: RunCompletedPayload = {
|
|
132
|
+
scenarioId: scenarioId(ticket, s.name),
|
|
133
|
+
ticketId: ticket,
|
|
134
|
+
runId,
|
|
135
|
+
status: s.status,
|
|
136
|
+
runtime: s.runtime,
|
|
137
|
+
};
|
|
138
|
+
if (s.traceId) p.traceId = s.traceId;
|
|
139
|
+
events.push(makeEvent('xera-exec', 'run.completed', p));
|
|
140
|
+
}
|
|
141
|
+
appendEvents(repoRoot, events, { skill: 'xera-exec', ticketId: ticket });
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function recordClassify(repoRoot: string, ticket: string, runId: string): Promise<number> {
|
|
146
|
+
const classifyPath = join(repoRoot, '.xera', ticket, 'runs', runId, 'classifier-output.json');
|
|
147
|
+
if (!existsSync(classifyPath)) {
|
|
148
|
+
console.error(`[graph-record classify] classifier-output.json missing`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
const data = JSON.parse(readFileSync(classifyPath, 'utf8')) as {
|
|
152
|
+
scenarios: Array<{ name: string; class: string; confidence: 'low' | 'medium' | 'high' }>;
|
|
153
|
+
};
|
|
154
|
+
const events: Event[] = [];
|
|
155
|
+
for (const s of data.scenarios) {
|
|
156
|
+
const p: RunClassifiedPayload = {
|
|
157
|
+
scenarioId: scenarioId(ticket, s.name),
|
|
158
|
+
runId,
|
|
159
|
+
classification: s.class as RunClassifiedPayload['classification'],
|
|
160
|
+
confidence: s.confidence,
|
|
161
|
+
};
|
|
162
|
+
events.push(makeEvent('xera-report', 'run.classified', p));
|
|
163
|
+
}
|
|
164
|
+
appendEvents(repoRoot, events, { skill: 'xera-report', ticketId: ticket });
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function recordPromote(repoRoot: string, args: Map<string, string>): Promise<number> {
|
|
169
|
+
const from = args.get('--from');
|
|
170
|
+
const to = args.get('--to');
|
|
171
|
+
const pomIdArg = args.get('--pom-id');
|
|
172
|
+
if (!from || !to) {
|
|
173
|
+
console.error(`[graph-record promote] --from and --to required`);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
const id = pomIdArg ?? pomId(from);
|
|
177
|
+
const p: PomPromotedPayload = { pomId: id, fromPath: from, toPath: to };
|
|
178
|
+
const e = makeEvent('xera-promote', 'pom.promoted', p);
|
|
179
|
+
appendEvents(repoRoot, [e], { skill: 'xera-promote', ticketId: 'shared' });
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseFlags(args: string[]): Map<string, string> {
|
|
184
|
+
const m = new Map<string, string>();
|
|
185
|
+
for (let i = 0; i < args.length; i++) {
|
|
186
|
+
if (args[i]!.startsWith('--')) {
|
|
187
|
+
m.set(args[i]!, args[i + 1] ?? '');
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return m;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function graphRecordCmd(argv: string[]): Promise<number> {
|
|
195
|
+
const [action, ...rest] = argv;
|
|
196
|
+
if (!action) {
|
|
197
|
+
console.error(
|
|
198
|
+
`Usage: xera-internal graph-record <fetch|script|exec|classify|promote|dispute> [args]`,
|
|
199
|
+
);
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
const repoRoot = process.cwd();
|
|
203
|
+
switch (action) {
|
|
204
|
+
case 'fetch': {
|
|
205
|
+
const ticket = rest[0];
|
|
206
|
+
if (!ticket) {
|
|
207
|
+
console.error('ticket required');
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
return recordFetch(repoRoot, ticket);
|
|
211
|
+
}
|
|
212
|
+
case 'script': {
|
|
213
|
+
const ticket = rest[0];
|
|
214
|
+
if (!ticket) {
|
|
215
|
+
console.error('ticket required');
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
218
|
+
return recordScript(repoRoot, ticket);
|
|
219
|
+
}
|
|
220
|
+
case 'exec': {
|
|
221
|
+
const ticket = rest[0];
|
|
222
|
+
const flags = parseFlags(rest);
|
|
223
|
+
const runId = flags.get('--run-id');
|
|
224
|
+
if (!ticket || !runId) {
|
|
225
|
+
console.error('ticket + --run-id required');
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
return recordExec(repoRoot, ticket, runId);
|
|
229
|
+
}
|
|
230
|
+
case 'classify': {
|
|
231
|
+
const ticket = rest[0];
|
|
232
|
+
const flags = parseFlags(rest);
|
|
233
|
+
const runId = flags.get('--run-id');
|
|
234
|
+
if (!ticket || !runId) {
|
|
235
|
+
console.error('ticket + --run-id required');
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
return recordClassify(repoRoot, ticket, runId);
|
|
239
|
+
}
|
|
240
|
+
case 'promote': {
|
|
241
|
+
return recordPromote(repoRoot, parseFlags(rest));
|
|
242
|
+
}
|
|
243
|
+
case 'dispute': {
|
|
244
|
+
const flags = parseFlags(rest);
|
|
245
|
+
const runId = flags.get('--run-id');
|
|
246
|
+
const scenarioIdArg = flags.get('--scenario-id');
|
|
247
|
+
const from = flags.get('--from');
|
|
248
|
+
const to = flags.get('--to');
|
|
249
|
+
const actor = flags.get('--actor');
|
|
250
|
+
const reason = flags.get('--reason');
|
|
251
|
+
if (!runId || !scenarioIdArg || !from || !to || !actor) {
|
|
252
|
+
console.error(
|
|
253
|
+
'[graph-record dispute] required: --run-id --scenario-id --from --to --actor [--reason]',
|
|
254
|
+
);
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
const validClass = [
|
|
258
|
+
'REAL_BUG',
|
|
259
|
+
'TEST_BUG',
|
|
260
|
+
'SELECTOR_DRIFT',
|
|
261
|
+
'FLAKY',
|
|
262
|
+
'PASS',
|
|
263
|
+
'TEST_OUTDATED',
|
|
264
|
+
];
|
|
265
|
+
if (!validClass.includes(from) || !validClass.includes(to)) {
|
|
266
|
+
console.error(
|
|
267
|
+
`[graph-record dispute] --from and --to must be one of: ${validClass.join(', ')}`,
|
|
268
|
+
);
|
|
269
|
+
return 1;
|
|
270
|
+
}
|
|
271
|
+
const payload: ClassificationDisputedPayload = {
|
|
272
|
+
runId,
|
|
273
|
+
scenarioId: scenarioIdArg,
|
|
274
|
+
originalClassification: from as Classification,
|
|
275
|
+
disputedTo: to as Classification,
|
|
276
|
+
qaActor: actor,
|
|
277
|
+
};
|
|
278
|
+
if (reason) payload.qaReason = reason;
|
|
279
|
+
const e = makeEvent('xera-report', 'classification.disputed', payload);
|
|
280
|
+
appendEvents(repoRoot, [e], { skill: 'xera-report', ticketId: scenarioIdArg.slice(0, 12) });
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
default:
|
|
284
|
+
console.error(`Unknown action: ${action}`);
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -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,11 @@ 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 { graphEnrichCmd } from './graph-enrich';
|
|
9
|
+
import { graphQueryCmd } from './graph-query';
|
|
10
|
+
import { graphRecordCmd } from './graph-record';
|
|
11
|
+
import { graphSnapshotCmd } from './graph-snapshot';
|
|
7
12
|
import { healPrepareCmd } from './heal-prepare';
|
|
8
13
|
import { lintCmd } from './lint';
|
|
9
14
|
import { normalizeCmd } from './normalize';
|
|
@@ -23,6 +28,11 @@ const COMMANDS: Record<string, (argv: string[]) => Promise<number>> = {
|
|
|
23
28
|
'eval-report': evalReportCmd,
|
|
24
29
|
exec: execCmd,
|
|
25
30
|
fetch: fetchCmd,
|
|
31
|
+
'graph-backfill': graphBackfillCmd,
|
|
32
|
+
'graph-enrich': graphEnrichCmd,
|
|
33
|
+
'graph-query': graphQueryCmd,
|
|
34
|
+
'graph-record': graphRecordCmd,
|
|
35
|
+
'graph-snapshot': graphSnapshotCmd,
|
|
26
36
|
'heal-prepare': healPrepareCmd,
|
|
27
37
|
lint: lintCmd,
|
|
28
38
|
normalize: normalizeCmd,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { resolveArtifactPaths } from '../artifact/paths';
|
|
4
4
|
import { aggregateScenarios } from '../classifier/aggregate';
|
|
5
5
|
import type { ScenarioClassification } from '../classifier/types';
|
|
6
|
+
import type { OutdatedDecision } from '../graph/classify';
|
|
7
|
+
import { enhanceClassification } from '../graph/classify';
|
|
8
|
+
import { deriveSnapshot, loadAllEvents } from '../graph/store';
|
|
6
9
|
import { buildJiraComment } from '../reporter/jira-comment';
|
|
7
10
|
import { writeStatusFromClassification } from '../reporter/status-writer';
|
|
8
11
|
|
|
@@ -23,20 +26,75 @@ export async function reportCmd(argv: string[]): Promise<number> {
|
|
|
23
26
|
const input = JSON.parse(readFileSync(inputArg.slice('--input='.length), 'utf8')) as ReportInput;
|
|
24
27
|
|
|
25
28
|
const aggregated = aggregateScenarios(input.scenarios);
|
|
29
|
+
|
|
30
|
+
// v0.6.1: TEST_OUTDATED enhancement.
|
|
31
|
+
// The /xera-report skill writes outdated-decisions.json BEFORE invoking this subcommand,
|
|
32
|
+
// containing { [scenarioId]: { classification, confidence, evidence } } for every
|
|
33
|
+
// failing scenario the skill ran the LLM on. We use those decisions directly via
|
|
34
|
+
// an injected resolver — no Claude call here.
|
|
35
|
+
const decisionsPath = join(paths.ticketDir, 'runs', input.runId, 'outdated-decisions.json');
|
|
36
|
+
const decisions: Record<string, OutdatedDecision> = existsSync(decisionsPath)
|
|
37
|
+
? (JSON.parse(readFileSync(decisionsPath, 'utf8')) as Record<string, OutdatedDecision>)
|
|
38
|
+
: {};
|
|
39
|
+
|
|
40
|
+
const graph = deriveSnapshot(loadAllEvents(process.cwd()));
|
|
41
|
+
|
|
42
|
+
// Build a lookup: normalized name → scenarioId (graph node id) for this ticket.
|
|
43
|
+
// This mirrors how graph-record-script.ts stores scenarios using sha1(ticket:name),
|
|
44
|
+
// but here we look up by the stored node id so both sha1-keyed and stub-keyed graphs work.
|
|
45
|
+
const normalizeScenarioName = (name: string) => name.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
46
|
+
|
|
47
|
+
const scenarioIdByName: Record<string, string> = {};
|
|
48
|
+
for (const [id, node] of Object.entries(graph.scenarios)) {
|
|
49
|
+
if (node.ticketId === ticket) {
|
|
50
|
+
scenarioIdByName[normalizeScenarioName(node.name)] = id;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const enhancedScenarios: ScenarioClassification[] = await Promise.all(
|
|
55
|
+
aggregated.scenarios.map(async (s) => {
|
|
56
|
+
if (s.outcome !== 'FAIL') return s;
|
|
57
|
+
const scenarioId = scenarioIdByName[normalizeScenarioName(s.name)];
|
|
58
|
+
if (!scenarioId) return s;
|
|
59
|
+
const decision = decisions[scenarioId];
|
|
60
|
+
const decideOutdated = async (): Promise<OutdatedDecision> =>
|
|
61
|
+
decision ?? {
|
|
62
|
+
classification: 'BUG' as const,
|
|
63
|
+
confidence: 0,
|
|
64
|
+
evidence: { reasoning: 'no LLM decision' },
|
|
65
|
+
};
|
|
66
|
+
const enhanced = await enhanceClassification(
|
|
67
|
+
{ scenarioId, traceClassification: s.class },
|
|
68
|
+
graph,
|
|
69
|
+
decideOutdated,
|
|
70
|
+
);
|
|
71
|
+
if (enhanced.classification !== s.class) {
|
|
72
|
+
return {
|
|
73
|
+
...s,
|
|
74
|
+
class: enhanced.classification,
|
|
75
|
+
rationale: `${s.rationale} | TEST_OUTDATED override (conf ${enhanced.confidence})`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return s;
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const reAggregated = aggregateScenarios(enhancedScenarios);
|
|
83
|
+
|
|
26
84
|
const ts = new Date().toISOString();
|
|
27
85
|
writeStatusFromClassification(paths.statusPath, {
|
|
28
86
|
ticket,
|
|
29
87
|
runTs: ts,
|
|
30
|
-
classification:
|
|
88
|
+
classification: reAggregated,
|
|
31
89
|
scenarioCounts: input.scenarioCounts,
|
|
32
90
|
});
|
|
33
91
|
|
|
34
92
|
const md = buildJiraComment({
|
|
35
93
|
ticket,
|
|
36
94
|
runId: input.runId,
|
|
37
|
-
overall:
|
|
38
|
-
overallConfidence:
|
|
39
|
-
scenarios:
|
|
95
|
+
overall: reAggregated.overall,
|
|
96
|
+
overallConfidence: reAggregated.overallConfidence,
|
|
97
|
+
scenarios: reAggregated.scenarios,
|
|
40
98
|
xeraVersion: '0.1.0',
|
|
41
99
|
promptsVersion: '1.0.0',
|
|
42
100
|
});
|
|
@@ -10,6 +10,9 @@ const IN_SCOPE_PROMPTS = [
|
|
|
10
10
|
'feature-from-story.md',
|
|
11
11
|
'script-from-feature.md',
|
|
12
12
|
'heal-locator.md',
|
|
13
|
+
'extract-areas.md',
|
|
14
|
+
'similarity-match.md',
|
|
15
|
+
'classify-outdated.md',
|
|
13
16
|
] as const;
|
|
14
17
|
|
|
15
18
|
const REQUIRED_SECTION_HEADING = '## Handling untrusted input';
|
package/src/config/schema.ts
CHANGED
|
@@ -19,12 +19,12 @@ const WebSchema = z
|
|
|
19
19
|
message: 'baseUrl must have at least one environment',
|
|
20
20
|
}),
|
|
21
21
|
defaultEnv: z.string(),
|
|
22
|
-
auth: AuthSchema.
|
|
22
|
+
auth: AuthSchema.prefault({}),
|
|
23
23
|
testData: z
|
|
24
24
|
.object({
|
|
25
25
|
users: z.record(z.string(), z.object({ fromAuth: z.string() })).default({}),
|
|
26
26
|
})
|
|
27
|
-
.
|
|
27
|
+
.prefault({}),
|
|
28
28
|
})
|
|
29
29
|
.refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
|
|
30
30
|
message: 'defaultEnv must exist in baseUrl map',
|
|
@@ -51,9 +51,9 @@ const AISchema = z
|
|
|
51
51
|
lint: z.number().int().min(0).max(5).default(2),
|
|
52
52
|
validateFeature: z.number().int().min(0).max(5).default(2),
|
|
53
53
|
})
|
|
54
|
-
.
|
|
54
|
+
.prefault({}),
|
|
55
55
|
})
|
|
56
|
-
.
|
|
56
|
+
.prefault({});
|
|
57
57
|
|
|
58
58
|
const ReportingSchema = z
|
|
59
59
|
.object({
|
|
@@ -64,10 +64,10 @@ const ReportingSchema = z
|
|
|
64
64
|
onPass: z.string().nullable().default(null),
|
|
65
65
|
onFail: z.string().nullable().default(null),
|
|
66
66
|
})
|
|
67
|
-
.
|
|
67
|
+
.prefault({}),
|
|
68
68
|
artifactLinks: z.enum(['git', 'local']).default('git'),
|
|
69
69
|
})
|
|
70
|
-
.
|
|
70
|
+
.prefault({});
|
|
71
71
|
|
|
72
72
|
export const XeraConfigSchema = z.object({
|
|
73
73
|
jira: JiraSchema,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Classification, ScenarioNode, Snapshot, TicketNode } from './types';
|
|
2
|
+
|
|
3
|
+
export interface ClassifyInput {
|
|
4
|
+
scenarioId: string;
|
|
5
|
+
traceClassification: Classification;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CandidateEvidence {
|
|
9
|
+
ticketId: string;
|
|
10
|
+
summary: string;
|
|
11
|
+
modifiedArea: string;
|
|
12
|
+
relevantAcRef?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ClassifyEvidence {
|
|
16
|
+
candidateTickets?: CandidateEvidence[];
|
|
17
|
+
reasoning?: string;
|
|
18
|
+
expectedByTest?: string;
|
|
19
|
+
actualInApp?: string;
|
|
20
|
+
proposedAction?: 'regenerate-scenario' | 'review-and-decide';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ClassifyOutput {
|
|
24
|
+
classification: Classification;
|
|
25
|
+
confidence: number;
|
|
26
|
+
evidence?: ClassifyEvidence;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface OutdatedDecision {
|
|
30
|
+
classification: 'TEST_OUTDATED' | 'BUG' | 'AMBIGUOUS';
|
|
31
|
+
confidence: number;
|
|
32
|
+
evidence: {
|
|
33
|
+
reasoning: string;
|
|
34
|
+
expectedByTest?: string;
|
|
35
|
+
actualInApp?: string;
|
|
36
|
+
relevantAcRef?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type DecideOutdated = (args: {
|
|
41
|
+
scenario: ScenarioNode;
|
|
42
|
+
candidates: TicketNode[];
|
|
43
|
+
}) => Promise<OutdatedDecision>;
|
|
44
|
+
|
|
45
|
+
const DEFAULT_THRESHOLD = 0.7;
|
|
46
|
+
const SHORT_CIRCUIT: Classification[] = ['FLAKY', 'PASS'];
|
|
47
|
+
|
|
48
|
+
export function findCandidateTickets(graph: Snapshot, scenario: ScenarioNode): TicketNode[] {
|
|
49
|
+
const poms = graph.edges
|
|
50
|
+
.filter((e) => e.kind === 'uses' && e.from === scenario.id)
|
|
51
|
+
.map((e) => e.to);
|
|
52
|
+
if (poms.length === 0) return [];
|
|
53
|
+
|
|
54
|
+
const areas = graph.edges
|
|
55
|
+
.filter((e) => e.kind === 'covers' && poms.includes(e.from))
|
|
56
|
+
.map((e) => e.to);
|
|
57
|
+
if (areas.length === 0) return [];
|
|
58
|
+
|
|
59
|
+
const ticketIds = graph.edges
|
|
60
|
+
.filter((e) => e.kind === 'modifies' && areas.includes(e.to))
|
|
61
|
+
.map((e) => e.from);
|
|
62
|
+
|
|
63
|
+
const seen = new Set<string>();
|
|
64
|
+
const out: TicketNode[] = [];
|
|
65
|
+
for (const id of ticketIds) {
|
|
66
|
+
if (seen.has(id)) continue;
|
|
67
|
+
seen.add(id);
|
|
68
|
+
if (id === scenario.ticketId) continue;
|
|
69
|
+
const t = graph.tickets[id];
|
|
70
|
+
if (!t) continue;
|
|
71
|
+
if (t.fetchedAt <= scenario.generatedAt) continue;
|
|
72
|
+
out.push(t);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function enhanceClassification(
|
|
78
|
+
input: ClassifyInput,
|
|
79
|
+
graph: Snapshot,
|
|
80
|
+
decideOutdated: DecideOutdated,
|
|
81
|
+
options: { threshold?: number } = {},
|
|
82
|
+
): Promise<ClassifyOutput> {
|
|
83
|
+
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
84
|
+
if (SHORT_CIRCUIT.includes(input.traceClassification)) {
|
|
85
|
+
return { classification: input.traceClassification, confidence: 1 };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const scenario = graph.scenarios[input.scenarioId];
|
|
89
|
+
if (!scenario) return { classification: input.traceClassification, confidence: 1 };
|
|
90
|
+
|
|
91
|
+
const candidates = findCandidateTickets(graph, scenario);
|
|
92
|
+
if (candidates.length === 0) {
|
|
93
|
+
return { classification: input.traceClassification, confidence: 1 };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const candidateEvidence: CandidateEvidence[] = candidates.map((t) => {
|
|
97
|
+
const area = graph.edges.find((e) => e.kind === 'modifies' && e.from === t.id)?.to ?? '';
|
|
98
|
+
const ev: CandidateEvidence = { ticketId: t.id, summary: t.summary, modifiedArea: area };
|
|
99
|
+
if (t.ac[0]) ev.relevantAcRef = t.ac[0];
|
|
100
|
+
return ev;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const decision = await decideOutdated({ scenario, candidates });
|
|
104
|
+
|
|
105
|
+
if (decision.classification === 'TEST_OUTDATED' && decision.confidence >= threshold) {
|
|
106
|
+
const evidence: ClassifyEvidence = {
|
|
107
|
+
candidateTickets: candidateEvidence,
|
|
108
|
+
reasoning: decision.evidence.reasoning,
|
|
109
|
+
proposedAction: 'regenerate-scenario',
|
|
110
|
+
};
|
|
111
|
+
if (decision.evidence.expectedByTest)
|
|
112
|
+
evidence.expectedByTest = decision.evidence.expectedByTest;
|
|
113
|
+
if (decision.evidence.actualInApp) evidence.actualInApp = decision.evidence.actualInApp;
|
|
114
|
+
return {
|
|
115
|
+
classification: 'TEST_OUTDATED',
|
|
116
|
+
confidence: decision.confidence,
|
|
117
|
+
evidence,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
classification: input.traceClassification,
|
|
123
|
+
confidence: 1,
|
|
124
|
+
evidence: { candidateTickets: candidateEvidence },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { graphPaths } from './paths';
|
|
4
|
+
|
|
5
|
+
export interface LlmCallLog {
|
|
6
|
+
ts?: string;
|
|
7
|
+
skill: string;
|
|
8
|
+
prompt: string;
|
|
9
|
+
tokensIn: number;
|
|
10
|
+
tokensOut: number;
|
|
11
|
+
model: string;
|
|
12
|
+
costUsd: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function logLlmCall(repoRoot: string, call: LlmCallLog): void {
|
|
16
|
+
const paths = graphPaths(repoRoot);
|
|
17
|
+
mkdirSync(dirname(paths.costLog), { recursive: true });
|
|
18
|
+
const record = {
|
|
19
|
+
ts: call.ts ?? new Date().toISOString(),
|
|
20
|
+
skill: call.skill,
|
|
21
|
+
prompt: call.prompt,
|
|
22
|
+
tokens_in: call.tokensIn,
|
|
23
|
+
tokens_out: call.tokensOut,
|
|
24
|
+
model: call.model,
|
|
25
|
+
cost_estimate_usd: call.costUsd,
|
|
26
|
+
};
|
|
27
|
+
appendFileSync(paths.costLog, `${JSON.stringify(record)}\n`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CostSummary {
|
|
31
|
+
totalCalls: number;
|
|
32
|
+
totalUsd: number;
|
|
33
|
+
bySkill: Record<string, { calls: number; usd: number }>;
|
|
34
|
+
windowDays: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function summarizeCost(repoRoot: string, daysBack: number): CostSummary {
|
|
38
|
+
const paths = graphPaths(repoRoot);
|
|
39
|
+
const result: CostSummary = { totalCalls: 0, totalUsd: 0, bySkill: {}, windowDays: daysBack };
|
|
40
|
+
if (!existsSync(paths.costLog)) return result;
|
|
41
|
+
const cutoff = Date.now() - daysBack * 86400 * 1000;
|
|
42
|
+
for (const line of readFileSync(paths.costLog, 'utf8').split('\n')) {
|
|
43
|
+
if (!line.trim()) continue;
|
|
44
|
+
let row: { ts: string; skill: string; cost_estimate_usd: number };
|
|
45
|
+
try {
|
|
46
|
+
row = JSON.parse(line);
|
|
47
|
+
} catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (Date.parse(row.ts) < cutoff) continue;
|
|
51
|
+
result.totalCalls++;
|
|
52
|
+
result.totalUsd += row.cost_estimate_usd;
|
|
53
|
+
if (!result.bySkill[row.skill]) result.bySkill[row.skill] = { calls: 0, usd: 0 };
|
|
54
|
+
const s = result.bySkill[row.skill]!;
|
|
55
|
+
s.calls++;
|
|
56
|
+
s.usd += row.cost_estimate_usd;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|