@xera-ai/core 0.11.1 → 0.11.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xera-ai/core",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "zod": "4.4.3",
34
- "@xera-ai/web": "^0.11.1",
35
- "@xera-ai/http": "^0.11.1",
34
+ "@xera-ai/web": "^0.11.3",
35
+ "@xera-ai/http": "^0.11.3",
36
36
  "@playwright/test": "1.60.0",
37
37
  "dotenv": "^16.0.0",
38
38
  "fflate": "0.8.3",
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, unlinkSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { z } from 'zod';
4
4
  import { appendEvents, deriveSnapshot, loadAllEvents } from './store';
@@ -50,6 +50,22 @@ export async function enrichTicket(
50
50
  ticketId: string,
51
51
  opts: EnrichOptions,
52
52
  ): Promise<EnrichResult> {
53
+ // Check the graph snapshot first so a missing ticket surfaces as the
54
+ // actionable "fetch it first" error instead of a confusing
55
+ // "enrichment-input.json not found" — the input file may live under
56
+ // .xera/<CANDIDATE>/, a directory that doesn't exist until the
57
+ // candidate has been fetched.
58
+ const snapshot = deriveSnapshot(loadAllEvents(repoRoot));
59
+ if (!snapshot.tickets[ticketId]) {
60
+ throw new Error(
61
+ `ticket ${ticketId} not in graph; fetch it first with \`/xera-fetch ${ticketId}\``,
62
+ );
63
+ }
64
+
65
+ if (snapshot.tickets[ticketId]!.enrichedAt && !opts.force) {
66
+ return { ticketId, similarCount: 0, enrichedAt: snapshot.tickets[ticketId]!.enrichedAt! };
67
+ }
68
+
53
69
  const inputPath = join(repoRoot, '.xera', ticketId, 'enrichment-input.json');
54
70
  if (!existsSync(inputPath)) {
55
71
  throw new Error(`enrichment-input.json not found at ${inputPath}`);
@@ -61,15 +77,6 @@ export async function enrichTicket(
61
77
  throw new Error(`invalid enrichment-input.json: ${parsed.error.message}`);
62
78
  }
63
79
 
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
80
  const validated = parsed.data.similar
74
81
  .map((s) => ({ ...s, confidence: Math.max(0, Math.min(1, s.confidence)) }))
75
82
  .filter((s) => s.confidence >= MIN_CONFIDENCE)
@@ -99,5 +106,13 @@ export async function enrichTicket(
99
106
 
100
107
  appendEvents(repoRoot, events, { skill: 'graph-enrich', ticketId });
101
108
 
109
+ // Consumed — remove so a stale file can't accidentally re-drive enrich
110
+ // on a later invocation outside the /xera-report skill flow.
111
+ try {
112
+ unlinkSync(inputPath);
113
+ } catch {
114
+ // Best-effort cleanup; ignore if already gone.
115
+ }
116
+
102
117
  return { ticketId, similarCount: validated.length, enrichedAt };
103
118
  }
@@ -328,7 +328,7 @@ export function renderHtml(input: RenderHtmlInput): string {
328
328
  return template
329
329
  .replace('{{CSS}}', () => css)
330
330
  .replace('{{STATS}}', () => statsHuman)
331
- .replace('{{GENERATED_AT}}', () => input.generatedAt)
331
+ .replace(/\{\{GENERATED_AT\}\}/g, () => input.generatedAt)
332
332
  .replace('{{VIS_NETWORK_JS}}', () => visNetwork)
333
333
  .replace('{{GRAPH_DATA}}', () => graphJson)
334
334
  .replace('{{INTERACTION_JS}}', () => js)
@@ -5,7 +5,6 @@
5
5
  <button data-subtab="trend">Trend</button>
6
6
  </nav>
7
7
  <div data-subpanel="map" class="active">
8
- <p class="subpanel-hint">Area nodes are colored by status. Red = UNCOVERED, amber = STALE, green = COVERED. Other nodes neutral.</p>
9
8
  <main id="coverage-map-canvas"></main>
10
9
  </div>
11
10
  <div data-subpanel="list" hidden>
@@ -17,4 +16,14 @@
17
16
  <p class="subpanel-hint">UNCOVERED + STALE area count over time (one point per day, latest snapshot wins).</p>
18
17
  <div id="coverage-trend-svg"></div>
19
18
  </div>
19
+ <aside id="cov-drawer" class="cov-drawer hidden" aria-hidden="true">
20
+ <header class="cov-drawer-head">
21
+ <div class="cov-drawer-head-text">
22
+ <span id="cov-drawer-status" class="cov-drawer-status"></span>
23
+ <h3 id="cov-drawer-title"></h3>
24
+ </div>
25
+ <button id="cov-drawer-close" aria-label="Close" type="button">&times;</button>
26
+ </header>
27
+ <div class="cov-drawer-body" id="cov-drawer-body"></div>
28
+ </aside>
20
29
  </section>