@xera-ai/core 0.4.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.
Files changed (37) hide show
  1. package/dist/artifact/status.d.ts +4 -0
  2. package/dist/artifact/status.d.ts.map +1 -1
  3. package/dist/bin/internal.js +321 -69
  4. package/dist/bin-internal/graph-enrich.d.ts +2 -0
  5. package/dist/bin-internal/graph-enrich.d.ts.map +1 -0
  6. package/dist/bin-internal/graph-record.d.ts.map +1 -1
  7. package/dist/bin-internal/index.d.ts.map +1 -1
  8. package/dist/bin-internal/report.d.ts.map +1 -1
  9. package/dist/bin-internal/verify-prompts.d.ts.map +1 -1
  10. package/dist/classifier/aggregate.d.ts.map +1 -1
  11. package/dist/graph/classify.d.ts +42 -0
  12. package/dist/graph/classify.d.ts.map +1 -0
  13. package/dist/graph/enrich.d.ts +10 -0
  14. package/dist/graph/enrich.d.ts.map +1 -0
  15. package/dist/graph/index.d.ts +5 -0
  16. package/dist/graph/index.d.ts.map +1 -1
  17. package/dist/graph/schema.d.ts +3 -0
  18. package/dist/graph/schema.d.ts.map +1 -1
  19. package/dist/graph/similarity.d.ts +3 -0
  20. package/dist/graph/similarity.d.ts.map +1 -0
  21. package/dist/graph/types.d.ts +1 -1
  22. package/dist/graph/types.d.ts.map +1 -1
  23. package/dist/src/index.js +8 -1
  24. package/package.json +1 -1
  25. package/src/artifact/status.ts +8 -1
  26. package/src/bin-internal/graph-enrich.ts +28 -0
  27. package/src/bin-internal/graph-record.ts +45 -1
  28. package/src/bin-internal/index.ts +2 -0
  29. package/src/bin-internal/report.ts +63 -5
  30. package/src/bin-internal/verify-prompts.ts +2 -0
  31. package/src/classifier/aggregate.ts +1 -0
  32. package/src/graph/classify.ts +126 -0
  33. package/src/graph/enrich.ts +103 -0
  34. package/src/graph/index.ts +15 -0
  35. package/src/graph/schema.ts +8 -1
  36. package/src/graph/similarity.ts +43 -0
  37. package/src/graph/types.ts +7 -2
@@ -0,0 +1,103 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { appendEvents, deriveSnapshot, loadAllEvents } from './store';
5
+ import type { EdgeDiscoveredPayload, Event, TicketEnrichedPayload } from './types';
6
+ import { SCHEMA_VERSION } from './types';
7
+ import { ulid } from './ulid';
8
+
9
+ const MAX_SIMILAR_EDGES = 10;
10
+ const MIN_CONFIDENCE = 0.7;
11
+
12
+ const SimilarEntrySchema = z.object({
13
+ ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
14
+ confidence: z.number(),
15
+ reason: z.string(),
16
+ });
17
+
18
+ const EnrichmentInputSchema = z.object({
19
+ similar: z.array(SimilarEntrySchema),
20
+ });
21
+
22
+ export interface EnrichOptions {
23
+ force?: boolean;
24
+ }
25
+
26
+ export interface EnrichResult {
27
+ ticketId: string;
28
+ similarCount: number;
29
+ enrichedAt: string;
30
+ }
31
+
32
+ const nowIso = () => new Date().toISOString();
33
+
34
+ const mk = <T extends Event['type']>(
35
+ actor: string,
36
+ type: T,
37
+ payload: Extract<Event, { type: T }>['payload'],
38
+ ): Event =>
39
+ ({
40
+ event_id: ulid(),
41
+ schema_version: SCHEMA_VERSION,
42
+ ts: nowIso(),
43
+ actor,
44
+ type,
45
+ payload,
46
+ }) as Event;
47
+
48
+ export async function enrichTicket(
49
+ repoRoot: string,
50
+ ticketId: string,
51
+ opts: EnrichOptions,
52
+ ): Promise<EnrichResult> {
53
+ const inputPath = join(repoRoot, '.xera', ticketId, 'enrichment-input.json');
54
+ if (!existsSync(inputPath)) {
55
+ throw new Error(`enrichment-input.json not found at ${inputPath}`);
56
+ }
57
+
58
+ const raw = JSON.parse(readFileSync(inputPath, 'utf8'));
59
+ const parsed = EnrichmentInputSchema.safeParse(raw);
60
+ if (!parsed.success) {
61
+ throw new Error(`invalid enrichment-input.json: ${parsed.error.message}`);
62
+ }
63
+
64
+ const snapshot = deriveSnapshot(loadAllEvents(repoRoot));
65
+ if (!snapshot.tickets[ticketId]) {
66
+ throw new Error(`ticket ${ticketId} not in graph; run /xera-fetch first`);
67
+ }
68
+
69
+ if (snapshot.tickets[ticketId]!.enrichedAt && !opts.force) {
70
+ return { ticketId, similarCount: 0, enrichedAt: snapshot.tickets[ticketId]!.enrichedAt! };
71
+ }
72
+
73
+ const validated = parsed.data.similar
74
+ .map((s) => ({ ...s, confidence: Math.max(0, Math.min(1, s.confidence)) }))
75
+ .filter((s) => s.confidence >= MIN_CONFIDENCE)
76
+ .filter((s) => snapshot.tickets[s.ticketId] !== undefined)
77
+ .filter((s) => s.ticketId !== ticketId)
78
+ .slice(0, MAX_SIMILAR_EDGES);
79
+
80
+ const events: Event[] = [];
81
+ for (const s of validated) {
82
+ const payload: EdgeDiscoveredPayload = {
83
+ kind: 'similar',
84
+ from: ticketId,
85
+ to: s.ticketId,
86
+ confidence: s.confidence,
87
+ source: `llm-similarity:${s.reason.slice(0, 80)}`,
88
+ };
89
+ events.push(mk('graph-enrich', 'edge.discovered', payload));
90
+ }
91
+
92
+ const enrichedAt = nowIso();
93
+ const enrichedPayload: TicketEnrichedPayload = {
94
+ ticketId,
95
+ enrichedAt,
96
+ similarCount: validated.length,
97
+ };
98
+ events.push(mk('graph-enrich', 'ticket.enriched', enrichedPayload));
99
+
100
+ appendEvents(repoRoot, events, { skill: 'graph-enrich', ticketId });
101
+
102
+ return { ticketId, similarCount: validated.length, enrichedAt };
103
+ }
@@ -1,7 +1,22 @@
1
+ export type {
2
+ CandidateEvidence,
3
+ ClassifyEvidence,
4
+ ClassifyInput,
5
+ ClassifyOutput,
6
+ DecideOutdated,
7
+ OutdatedDecision,
8
+ } from './classify';
9
+ export {
10
+ enhanceClassification,
11
+ findCandidateTickets,
12
+ } from './classify';
1
13
  export type { CostSummary, LlmCallLog } from './cost';
2
14
  export { logLlmCall, summarizeCost } from './cost';
15
+ export type { EnrichOptions, EnrichResult } from './enrich';
16
+ export { enrichTicket } from './enrich';
3
17
  export { currentYyyyMm, graphPaths } from './paths';
4
18
  export { EventSchema, safeParseEvent } from './schema';
19
+ export { buildSimilarityPrompt } from './similarity';
5
20
  export {
6
21
  appendEvents,
7
22
  computeEventsHash,
@@ -71,7 +71,14 @@ const runCompleted = z
71
71
  })
72
72
  .passthrough();
73
73
 
74
- const classification = z.enum(['REAL_BUG', 'TEST_BUG', 'SELECTOR_DRIFT', 'FLAKY', 'PASS']);
74
+ const classification = z.enum([
75
+ 'REAL_BUG',
76
+ 'TEST_BUG',
77
+ 'SELECTOR_DRIFT',
78
+ 'FLAKY',
79
+ 'PASS',
80
+ 'TEST_OUTDATED',
81
+ ]);
75
82
 
76
83
  const runClassified = z
77
84
  .object({
@@ -0,0 +1,43 @@
1
+ import type { TicketNode } from './types';
2
+
3
+ const MAX_CANDIDATES = 50;
4
+
5
+ export function buildSimilarityPrompt(target: TicketNode, candidates: TicketNode[]): string {
6
+ const window = candidates.slice(0, MAX_CANDIDATES);
7
+ const candidateBlock = window
8
+ .map((t, i) => {
9
+ const ac = t.ac.length > 0 ? `\n AC: ${t.ac.slice(0, 3).join(' | ')}` : '';
10
+ return `${i + 1}. ${t.id} — ${t.summary}${ac}`;
11
+ })
12
+ .join('\n');
13
+
14
+ const targetAc =
15
+ target.ac.length > 0 ? `\nAC:\n${target.ac.map((a) => ` - ${a}`).join('\n')}` : '';
16
+
17
+ return `You are evaluating whether a NEW ticket is semantically related to any prior tickets in this project's knowledge graph.
18
+
19
+ # NEW TICKET
20
+ ID: ${target.id}
21
+ Summary: ${target.summary}${targetAc}
22
+
23
+ # PRIOR TICKETS (most recent ${window.length} of ${candidates.length})
24
+ ${candidateBlock || '(none yet)'}
25
+
26
+ # Task
27
+ Output a JSON object with shape:
28
+
29
+ \`\`\`json
30
+ {
31
+ "similar": [
32
+ { "ticketId": "<JIRA-KEY>", "confidence": 0.0-1.0, "reason": "<one sentence>" }
33
+ ]
34
+ }
35
+ \`\`\`
36
+
37
+ # Rules
38
+ 1. Only include candidates with confidence ≥ 0.7. Below that, exclude.
39
+ 2. Confidence reflects semantic relatedness (same SUT area, same flow, complementary feature) — NOT just word overlap.
40
+ 3. Cap output at 10 entries even if more candidates pass the threshold; pick the highest-confidence ones.
41
+ 4. If NO candidates are related, return \`{ "similar": [] }\`. Do not invent relationships.
42
+ 5. Output JSON ONLY. No prose, no fences, no commentary.`;
43
+ }
@@ -6,8 +6,13 @@ export type Priority = 'p0' | 'p1' | 'p2';
6
6
  export type ScenarioStatus = 'pass' | 'fail';
7
7
  export type EdgeKind = 'tests' | 'uses' | 'covers' | 'modifies' | 'jira-linked' | 'similar' | 'ran';
8
8
 
9
- export type Classification = 'REAL_BUG' | 'TEST_BUG' | 'SELECTOR_DRIFT' | 'FLAKY' | 'PASS';
10
- // Note: TEST_OUTDATED is added in v0.6.1.
9
+ export type Classification =
10
+ | 'REAL_BUG'
11
+ | 'TEST_BUG'
12
+ | 'SELECTOR_DRIFT'
13
+ | 'FLAKY'
14
+ | 'PASS'
15
+ | 'TEST_OUTDATED';
11
16
 
12
17
  export interface TicketFetchedPayload {
13
18
  ticketId: string;