@xera-ai/core 0.1.7 → 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.
Files changed (131) hide show
  1. package/bin/internal.ts +1 -0
  2. package/dist/adapter/types.d.ts +1 -1
  3. package/dist/adapter/types.d.ts.map +1 -1
  4. package/dist/artifact/meta.d.ts +2 -28
  5. package/dist/artifact/meta.d.ts.map +1 -1
  6. package/dist/artifact/status.d.ts +49 -74
  7. package/dist/artifact/status.d.ts.map +1 -1
  8. package/dist/auth/key.d.ts.map +1 -1
  9. package/dist/auth/refresh.d.ts.map +1 -1
  10. package/dist/auth/state.d.ts +5 -14
  11. package/dist/auth/state.d.ts.map +1 -1
  12. package/dist/bin/internal.js +10037 -746
  13. package/dist/bin-internal/doctor.d.ts +5 -0
  14. package/dist/bin-internal/doctor.d.ts.map +1 -0
  15. package/dist/bin-internal/eval-deterministic.d.ts +5 -0
  16. package/dist/bin-internal/eval-deterministic.d.ts.map +1 -0
  17. package/dist/bin-internal/eval-prepare.d.ts +7 -0
  18. package/dist/bin-internal/eval-prepare.d.ts.map +1 -0
  19. package/dist/bin-internal/eval-report.d.ts +5 -0
  20. package/dist/bin-internal/eval-report.d.ts.map +1 -0
  21. package/dist/bin-internal/exec.d.ts.map +1 -1
  22. package/dist/bin-internal/fetch.d.ts.map +1 -1
  23. package/dist/bin-internal/graph-backfill.d.ts +2 -0
  24. package/dist/bin-internal/graph-backfill.d.ts.map +1 -0
  25. package/dist/bin-internal/graph-query.d.ts +2 -0
  26. package/dist/bin-internal/graph-query.d.ts.map +1 -0
  27. package/dist/bin-internal/graph-record-script.d.ts +2 -0
  28. package/dist/bin-internal/graph-record-script.d.ts.map +1 -0
  29. package/dist/bin-internal/graph-record.d.ts +3 -0
  30. package/dist/bin-internal/graph-record.d.ts.map +1 -0
  31. package/dist/bin-internal/graph-snapshot.d.ts +2 -0
  32. package/dist/bin-internal/graph-snapshot.d.ts.map +1 -0
  33. package/dist/bin-internal/heal-prepare.d.ts +19 -0
  34. package/dist/bin-internal/heal-prepare.d.ts.map +1 -0
  35. package/dist/bin-internal/index.d.ts.map +1 -1
  36. package/dist/bin-internal/lint.d.ts.map +1 -1
  37. package/dist/bin-internal/normalize.d.ts.map +1 -1
  38. package/dist/bin-internal/post.d.ts.map +1 -1
  39. package/dist/bin-internal/status-cmd.d.ts.map +1 -1
  40. package/dist/bin-internal/typecheck.d.ts.map +1 -1
  41. package/dist/bin-internal/unlock.d.ts.map +1 -1
  42. package/dist/bin-internal/validate-feature.d.ts.map +1 -1
  43. package/dist/bin-internal/verify-prompts.d.ts +7 -0
  44. package/dist/bin-internal/verify-prompts.d.ts.map +1 -0
  45. package/dist/classifier/aggregate.d.ts.map +1 -1
  46. package/dist/config/define.d.ts.map +1 -1
  47. package/dist/config/load.d.ts.map +1 -1
  48. package/dist/config/schema.d.ts +38 -298
  49. package/dist/config/schema.d.ts.map +1 -1
  50. package/dist/eval/paths.d.ts +15 -0
  51. package/dist/eval/paths.d.ts.map +1 -0
  52. package/dist/eval/run-id.d.ts +6 -0
  53. package/dist/eval/run-id.d.ts.map +1 -0
  54. package/dist/eval/types.d.ts +203 -0
  55. package/dist/eval/types.d.ts.map +1 -0
  56. package/dist/graph/cost.d.ts +21 -0
  57. package/dist/graph/cost.d.ts.map +1 -0
  58. package/dist/graph/index.d.ts +8 -0
  59. package/dist/graph/index.d.ts.map +1 -0
  60. package/dist/graph/paths.d.ts +10 -0
  61. package/dist/graph/paths.d.ts.map +1 -0
  62. package/dist/graph/schema.d.ts +177 -0
  63. package/dist/graph/schema.d.ts.map +1 -0
  64. package/dist/graph/store.d.ts +14 -0
  65. package/dist/graph/store.d.ts.map +1 -0
  66. package/dist/graph/types.d.ts +151 -0
  67. package/dist/graph/types.d.ts.map +1 -0
  68. package/dist/graph/ulid.d.ts +2 -0
  69. package/dist/graph/ulid.d.ts.map +1 -0
  70. package/dist/index.d.ts +11 -11
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/jira/client.d.ts.map +1 -1
  73. package/dist/jira/fields.d.ts.map +1 -1
  74. package/dist/jira/rest-backend.d.ts.map +1 -1
  75. package/dist/reporter/jira-comment.d.ts.map +1 -1
  76. package/dist/reporter/status-writer.d.ts.map +1 -1
  77. package/dist/src/index.js +349 -321
  78. package/package.json +19 -13
  79. package/src/adapter/types.ts +5 -2
  80. package/src/artifact/meta.ts +1 -1
  81. package/src/artifact/status.ts +1 -1
  82. package/src/auth/encrypt.ts +2 -2
  83. package/src/auth/key.ts +1 -2
  84. package/src/auth/refresh.ts +5 -1
  85. package/src/auth/state.ts +2 -2
  86. package/src/bin-internal/doctor.ts +169 -0
  87. package/src/bin-internal/eval-deterministic.ts +149 -0
  88. package/src/bin-internal/eval-prepare.ts +214 -0
  89. package/src/bin-internal/eval-report.ts +177 -0
  90. package/src/bin-internal/exec.ts +28 -15
  91. package/src/bin-internal/fetch.ts +21 -10
  92. package/src/bin-internal/graph-backfill.ts +43 -0
  93. package/src/bin-internal/graph-query.ts +43 -0
  94. package/src/bin-internal/graph-record-script.ts +191 -0
  95. package/src/bin-internal/graph-record.ts +243 -0
  96. package/src/bin-internal/graph-snapshot.ts +23 -0
  97. package/src/bin-internal/heal-prepare.ts +230 -0
  98. package/src/bin-internal/index.ts +33 -11
  99. package/src/bin-internal/lint.ts +11 -4
  100. package/src/bin-internal/normalize.ts +23 -9
  101. package/src/bin-internal/post.ts +10 -4
  102. package/src/bin-internal/report.ts +3 -3
  103. package/src/bin-internal/status-cmd.ts +11 -3
  104. package/src/bin-internal/typecheck.ts +9 -3
  105. package/src/bin-internal/unlock.ts +12 -4
  106. package/src/bin-internal/validate-feature.ts +14 -5
  107. package/src/bin-internal/verify-prompts.ts +60 -0
  108. package/src/classifier/aggregate.ts +13 -6
  109. package/src/config/define.ts +3 -1
  110. package/src/config/load.ts +1 -1
  111. package/src/config/schema.ts +43 -37
  112. package/src/eval/paths.ts +32 -0
  113. package/src/eval/run-id.ts +30 -0
  114. package/src/eval/types.ts +101 -0
  115. package/src/graph/cost.ts +59 -0
  116. package/src/graph/index.ts +15 -0
  117. package/src/graph/paths.ts +27 -0
  118. package/src/graph/schema.ts +135 -0
  119. package/src/graph/store.ts +231 -0
  120. package/src/graph/types.ts +174 -0
  121. package/src/graph/ulid.ts +58 -0
  122. package/src/index.ts +11 -11
  123. package/src/jira/client.ts +4 -2
  124. package/src/jira/fields.ts +4 -2
  125. package/src/jira/mcp-backend.ts +1 -1
  126. package/src/jira/rest-backend.ts +18 -6
  127. package/src/jira/retry.ts +2 -2
  128. package/src/lock/file-lock.ts +2 -2
  129. package/src/logging/ndjson-logger.ts +2 -2
  130. package/src/reporter/jira-comment.ts +13 -7
  131. package/src/reporter/status-writer.ts +2 -2
@@ -3,11 +3,19 @@ import { readStatus } from '../artifact/status';
3
3
 
4
4
  export async function statusCmd(argv: string[]): Promise<number> {
5
5
  const ticket = argv[0];
6
- if (!ticket) { console.error('[xera:status] usage: status <TICKET>'); return 1; }
6
+ if (!ticket) {
7
+ console.error('[xera:status] usage: status <TICKET>');
8
+ return 1;
9
+ }
7
10
  const paths = resolveArtifactPaths(process.cwd(), ticket);
8
11
  const s = readStatus(paths.statusPath);
9
- if (!s) { console.log(`[xera:status] no status yet for ${ticket}`); return 0; }
10
- console.log(`${ticket}: ${s.result} (${s.classification}, conf=${s.confidence}) ${s.scenarios.passed}/${s.scenarios.total} passed, last run ${s.lastRun}`);
12
+ if (!s) {
13
+ console.log(`[xera:status] no status yet for ${ticket}`);
14
+ return 0;
15
+ }
16
+ console.log(
17
+ `${ticket}: ${s.result} (${s.classification}, conf=${s.confidence}) — ${s.scenarios.passed}/${s.scenarios.total} passed, last run ${s.lastRun}`,
18
+ );
11
19
  for (const h of s.history.slice(0, 5)) console.log(` ${h.ts} ${h.result} ${h.class}`);
12
20
  return 0;
13
21
  }
@@ -1,12 +1,18 @@
1
- import { resolveArtifactPaths } from '../artifact/paths';
2
1
  import { typecheckTicket } from '@xera-ai/web';
2
+ import { resolveArtifactPaths } from '../artifact/paths';
3
3
 
4
4
  export async function typecheckCmd(argv: string[]): Promise<number> {
5
5
  const ticket = argv[0];
6
- if (!ticket) { console.error('[xera:typecheck] usage: typecheck <TICKET>'); return 1; }
6
+ if (!ticket) {
7
+ console.error('[xera:typecheck] usage: typecheck <TICKET>');
8
+ return 1;
9
+ }
7
10
  const paths = resolveArtifactPaths(process.cwd(), ticket);
8
11
  const r = await typecheckTicket(paths.ticketDir);
9
- if (r.ok) { console.log('[xera:typecheck] ok'); return 0; }
12
+ if (r.ok) {
13
+ console.log('[xera:typecheck] ok');
14
+ return 0;
15
+ }
10
16
  for (const e of r.errors) console.error(`[xera:typecheck] ${e}`);
11
17
  return 2;
12
18
  }
@@ -1,15 +1,23 @@
1
1
  import { resolveArtifactPaths } from '../artifact/paths';
2
- import { isLockStale, readLock, forceUnlock } from '../lock/file-lock';
2
+ import { forceUnlock, isLockStale, readLock } from '../lock/file-lock';
3
3
 
4
4
  export async function unlockCmd(argv: string[]): Promise<number> {
5
5
  const ticket = argv[0];
6
- if (!ticket) { console.error('[xera:unlock] usage: unlock <TICKET> [--force]'); return 1; }
6
+ if (!ticket) {
7
+ console.error('[xera:unlock] usage: unlock <TICKET> [--force]');
8
+ return 1;
9
+ }
7
10
  const paths = resolveArtifactPaths(process.cwd(), ticket);
8
11
  const lock = readLock(paths.lockPath);
9
- if (!lock) { console.log(`[xera:unlock] no lock for ${ticket}`); return 0; }
12
+ if (!lock) {
13
+ console.log(`[xera:unlock] no lock for ${ticket}`);
14
+ return 0;
15
+ }
10
16
  const force = argv.includes('--force');
11
17
  if (!force && !isLockStale(paths.lockPath)) {
12
- console.error(`[xera:unlock] lock is held by PID ${lock.pid} on ${lock.hostname} (active). Pass --force to override.`);
18
+ console.error(
19
+ `[xera:unlock] lock is held by PID ${lock.pid} on ${lock.hostname} (active). Pass --force to override.`,
20
+ );
13
21
  return 1;
14
22
  }
15
23
  forceUnlock(paths.lockPath);
@@ -1,14 +1,23 @@
1
- import { readFileSync, existsSync } from 'node:fs';
2
- import { resolveArtifactPaths } from '../artifact/paths';
1
+ import { existsSync, readFileSync } from 'node:fs';
3
2
  import { validateGherkin } from '@xera-ai/web';
3
+ import { resolveArtifactPaths } from '../artifact/paths';
4
4
 
5
5
  export async function validateFeatureCmd(argv: string[]): Promise<number> {
6
6
  const ticket = argv[0];
7
- if (!ticket) { console.error('[xera:validate-feature] usage: validate-feature <TICKET>'); return 1; }
7
+ if (!ticket) {
8
+ console.error('[xera:validate-feature] usage: validate-feature <TICKET>');
9
+ return 1;
10
+ }
8
11
  const paths = resolveArtifactPaths(process.cwd(), ticket);
9
- if (!existsSync(paths.featurePath)) { console.error(`[xera:validate-feature] missing ${paths.featurePath}`); return 1; }
12
+ if (!existsSync(paths.featurePath)) {
13
+ console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
14
+ return 1;
15
+ }
10
16
  const r = validateGherkin(readFileSync(paths.featurePath, 'utf8'));
11
- if (r.ok) { console.log('[xera:validate-feature] ok'); return 0; }
17
+ if (r.ok) {
18
+ console.log('[xera:validate-feature] ok');
19
+ return 0;
20
+ }
12
21
  for (const e of r.errors) console.error(`[xera:validate-feature] line ${e.line}: ${e.message}`);
13
22
  return 2;
14
23
  }
@@ -0,0 +1,60 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export interface CheckResult {
5
+ ok: boolean;
6
+ message: string;
7
+ }
8
+
9
+ const IN_SCOPE_PROMPTS = [
10
+ 'feature-from-story.md',
11
+ 'script-from-feature.md',
12
+ 'heal-locator.md',
13
+ 'extract-areas.md',
14
+ ] as const;
15
+
16
+ const REQUIRED_SECTION_HEADING = '## Handling untrusted input';
17
+
18
+ const REQUIRED_KEYWORDS = ['UNTRUSTED', 'injection-follow', '<XR_'] as const;
19
+
20
+ export function verifyPrompts(repoRoot: string): CheckResult[] {
21
+ const promptsDir = join(repoRoot, 'packages/prompts');
22
+ const results: CheckResult[] = [];
23
+ for (const filename of IN_SCOPE_PROMPTS) {
24
+ const path = join(promptsDir, filename);
25
+ if (!existsSync(path)) {
26
+ results.push({
27
+ ok: false,
28
+ message: `${filename}: file missing at packages/prompts/${filename}`,
29
+ });
30
+ continue;
31
+ }
32
+ const text = readFileSync(path, 'utf8');
33
+ if (!text.includes(REQUIRED_SECTION_HEADING)) {
34
+ results.push({
35
+ ok: false,
36
+ message: `${filename}: missing required section "${REQUIRED_SECTION_HEADING}"`,
37
+ });
38
+ continue;
39
+ }
40
+ for (const keyword of REQUIRED_KEYWORDS) {
41
+ if (!text.includes(keyword)) {
42
+ results.push({
43
+ ok: false,
44
+ message: `${filename}: missing required keyword "${keyword}" (expected in "${REQUIRED_SECTION_HEADING}" section)`,
45
+ });
46
+ }
47
+ }
48
+ }
49
+ return results;
50
+ }
51
+
52
+ export async function verifyPromptsCmd(_argv: string[]): Promise<number> {
53
+ const results = verifyPrompts(process.cwd());
54
+ if (results.length === 0) {
55
+ console.log('[xera:verify-prompts] ok');
56
+ return 0;
57
+ }
58
+ for (const r of results) console.error(`[xera:verify-prompts] ${r.message}`);
59
+ return 1;
60
+ }
@@ -1,7 +1,11 @@
1
- import type { ClassifyOutput, ScenarioClassification, Confidence } from './types';
1
+ import type { ClassifyOutput, Confidence, ScenarioClassification } from './types';
2
2
 
3
3
  const CLASS_PRIORITY: Array<ClassifyOutput['overall']> = [
4
- 'REAL_BUG', 'TEST_BUG', 'SELECTOR_DRIFT', 'FLAKY', 'PASS',
4
+ 'REAL_BUG',
5
+ 'TEST_BUG',
6
+ 'SELECTOR_DRIFT',
7
+ 'FLAKY',
8
+ 'PASS',
5
9
  ];
6
10
 
7
11
  const CONF_RANK: Record<Confidence, number> = { low: 1, medium: 2, high: 3 };
@@ -10,16 +14,19 @@ export function aggregateScenarios(scenarios: ScenarioClassification[]): Classif
10
14
  if (scenarios.length === 0) {
11
15
  return { overall: 'PASS', overallConfidence: 'low', scenarios: [] };
12
16
  }
13
- if (scenarios.every(s => s.outcome === 'PASS')) {
17
+ if (scenarios.every((s) => s.outcome === 'PASS')) {
14
18
  return { overall: 'PASS', overallConfidence: 'high', scenarios };
15
19
  }
16
20
  let chosen: ClassifyOutput['overall'] = 'PASS';
17
21
  for (const cls of CLASS_PRIORITY) {
18
- if (scenarios.some(s => s.class === cls)) { chosen = cls; break; }
22
+ if (scenarios.some((s) => s.class === cls)) {
23
+ chosen = cls;
24
+ break;
25
+ }
19
26
  }
20
- const matching = scenarios.filter(s => s.class === chosen);
27
+ const matching = scenarios.filter((s) => s.class === chosen);
21
28
  const minConf = matching.reduce<Confidence>(
22
- (acc, s) => CONF_RANK[s.confidence] < CONF_RANK[acc] ? s.confidence : acc,
29
+ (acc, s) => (CONF_RANK[s.confidence] < CONF_RANK[acc] ? s.confidence : acc),
23
30
  'high',
24
31
  );
25
32
  return { overall: chosen, overallConfidence: minConf, scenarios };
@@ -1,2 +1,4 @@
1
1
  import type { XeraConfig } from './schema';
2
- export function defineConfig(config: XeraConfig): XeraConfig { return config; }
2
+ export function defineConfig(config: XeraConfig): XeraConfig {
3
+ return config;
4
+ }
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
- import { XeraConfigSchema, type XeraConfig } from './schema';
4
+ import { type XeraConfig, XeraConfigSchema } from './schema';
5
5
 
6
6
  export async function loadConfig(cwd: string): Promise<XeraConfig> {
7
7
  const path = join(cwd, 'xera.config.ts');
@@ -13,21 +13,23 @@ const AuthSchema = z.object({
13
13
  roles: z.record(z.string(), AuthRoleSchema).default({}),
14
14
  });
15
15
 
16
- const WebSchema = z.object({
17
- baseUrl: z.record(z.string(), z.string().url()).refine(m => Object.keys(m).length > 0, {
18
- message: 'baseUrl must have at least one environment',
19
- }),
20
- defaultEnv: z.string(),
21
- auth: AuthSchema.default({}),
22
- testData: z
23
- .object({
24
- users: z.record(z.string(), z.object({ fromAuth: z.string() })).default({}),
25
- })
26
- .default({ users: {} }),
27
- }).refine(w => w.baseUrl[w.defaultEnv] !== undefined, {
28
- message: 'defaultEnv must exist in baseUrl map',
29
- path: ['defaultEnv'],
30
- });
16
+ const WebSchema = z
17
+ .object({
18
+ baseUrl: z.record(z.string(), z.string().url()).refine((m) => Object.keys(m).length > 0, {
19
+ message: 'baseUrl must have at least one environment',
20
+ }),
21
+ defaultEnv: z.string(),
22
+ auth: AuthSchema.prefault({}),
23
+ testData: z
24
+ .object({
25
+ users: z.record(z.string(), z.object({ fromAuth: z.string() })).default({}),
26
+ })
27
+ .prefault({}),
28
+ })
29
+ .refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
30
+ message: 'defaultEnv must exist in baseUrl map',
31
+ path: ['defaultEnv'],
32
+ });
31
33
 
32
34
  const JiraSchema = z.object({
33
35
  baseUrl: z.string().url(),
@@ -39,29 +41,33 @@ const JiraSchema = z.object({
39
41
  }),
40
42
  });
41
43
 
42
- const AISchema = z.object({
43
- livePageSnapshot: z.boolean().default(true),
44
- confidenceThreshold: z.enum(['low', 'medium', 'high']).default('medium'),
45
- maxRetries: z
46
- .object({
47
- typecheck: z.number().int().min(0).max(5).default(2),
48
- lint: z.number().int().min(0).max(5).default(2),
49
- validateFeature: z.number().int().min(0).max(5).default(2),
50
- })
51
- .default({}),
52
- }).default({});
44
+ const AISchema = z
45
+ .object({
46
+ livePageSnapshot: z.boolean().default(true),
47
+ confidenceThreshold: z.enum(['low', 'medium', 'high']).default('medium'),
48
+ maxRetries: z
49
+ .object({
50
+ typecheck: z.number().int().min(0).max(5).default(2),
51
+ lint: z.number().int().min(0).max(5).default(2),
52
+ validateFeature: z.number().int().min(0).max(5).default(2),
53
+ })
54
+ .prefault({}),
55
+ })
56
+ .prefault({});
53
57
 
54
- const ReportingSchema = z.object({
55
- language: z.enum(['en', 'vi']).default('en'),
56
- postToJira: z.boolean().default(true),
57
- transition: z
58
- .object({
59
- onPass: z.string().nullable().default(null),
60
- onFail: z.string().nullable().default(null),
61
- })
62
- .default({}),
63
- artifactLinks: z.enum(['git', 'local']).default('git'),
64
- }).default({});
58
+ const ReportingSchema = z
59
+ .object({
60
+ language: z.enum(['en', 'vi']).default('en'),
61
+ postToJira: z.boolean().default(true),
62
+ transition: z
63
+ .object({
64
+ onPass: z.string().nullable().default(null),
65
+ onFail: z.string().nullable().default(null),
66
+ })
67
+ .prefault({}),
68
+ artifactLinks: z.enum(['git', 'local']).default('git'),
69
+ })
70
+ .prefault({});
65
71
 
66
72
  export const XeraConfigSchema = z.object({
67
73
  jira: JiraSchema,
@@ -0,0 +1,32 @@
1
+ import { join } from 'node:path';
2
+
3
+ export interface EvalPaths {
4
+ root: string;
5
+ manifest: string;
6
+ lock: string;
7
+ deterministicScores: string;
8
+ judgeScores: string;
9
+ report: string;
10
+ summary: string;
11
+ inputsDir: string;
12
+ actualDir: string;
13
+ ticketInputsDir(ticket: string): string;
14
+ ticketActualDir(ticket: string): string;
15
+ }
16
+
17
+ export function resolveEvalPaths(cwd: string, runId: string): EvalPaths {
18
+ const root = join(cwd, '.xera', 'eval', runId);
19
+ return {
20
+ root,
21
+ manifest: join(root, 'manifest.json'),
22
+ lock: join(root, '.lock'),
23
+ deterministicScores: join(root, 'deterministic-scores.json'),
24
+ judgeScores: join(root, 'judge-scores.json'),
25
+ report: join(root, 'report.md'),
26
+ summary: join(root, 'summary.json'),
27
+ inputsDir: join(root, 'inputs'),
28
+ actualDir: join(root, 'actual'),
29
+ ticketInputsDir: (ticket: string) => join(root, 'inputs', ticket),
30
+ ticketActualDir: (ticket: string) => join(root, 'actual', ticket),
31
+ };
32
+ }
@@ -0,0 +1,30 @@
1
+ import { execSync } from 'node:child_process';
2
+
3
+ export interface RunIdOpts {
4
+ getGitSha?: () => string | null;
5
+ now?: () => Date;
6
+ }
7
+
8
+ function defaultGetGitSha(): string | null {
9
+ try {
10
+ return execSync('git rev-parse HEAD', { stdio: ['ignore', 'pipe', 'ignore'] })
11
+ .toString()
12
+ .trim();
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ function pad(n: number): string {
19
+ return n.toString().padStart(2, '0');
20
+ }
21
+
22
+ export function generateRunId(opts: RunIdOpts = {}): string {
23
+ const getGitSha = opts.getGitSha ?? defaultGetGitSha;
24
+ const now = (opts.now ?? (() => new Date()))();
25
+ const date = `${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}`;
26
+ const time = `${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}`;
27
+ const sha = getGitSha();
28
+ const short = sha ? sha.slice(0, 7) : 'nogit';
29
+ return `${date}-${time}-${short}`;
30
+ }
@@ -0,0 +1,101 @@
1
+ import { z } from 'zod';
2
+
3
+ export const STAGES = ['feature-from-story', 'script-from-feature', 'diagnose-failure'] as const;
4
+ export const StageSchema = z.enum(STAGES);
5
+ export type Stage = z.infer<typeof StageSchema>;
6
+
7
+ export const VerdictSchema = z.enum(['PASS', 'FAIL', 'NA']);
8
+ export type Verdict = z.infer<typeof VerdictSchema>;
9
+
10
+ export const PromptVersionsSchema = z.object({
11
+ 'feature-from-story': z.string(),
12
+ 'script-from-feature': z.string(),
13
+ 'diagnose-failure': z.string(),
14
+ 'eval-rubric': z.string(),
15
+ });
16
+ export type PromptVersions = z.infer<typeof PromptVersionsSchema>;
17
+
18
+ export const ManifestSchema = z.object({
19
+ run_id: z.string(),
20
+ started_at: z.string(),
21
+ git_sha: z.string(),
22
+ tickets: z.array(z.string()).min(1),
23
+ stages: z.array(StageSchema).min(1),
24
+ ticket_stages: z.record(z.string(), z.array(StageSchema).min(1)),
25
+ prompt_versions: PromptVersionsSchema,
26
+ flags: z.object({
27
+ force: z.boolean(),
28
+ only_prompt: StageSchema.nullable(),
29
+ only_ticket: z.string().nullable(),
30
+ judge_only: z.boolean(),
31
+ }),
32
+ });
33
+ export type Manifest = z.infer<typeof ManifestSchema>;
34
+
35
+ export const DimensionSchema = z.object({
36
+ name: z.string(),
37
+ verdict: VerdictSchema,
38
+ notes: z.string(),
39
+ });
40
+ export type Dimension = z.infer<typeof DimensionSchema>;
41
+
42
+ export const JudgmentSchema = z.object({
43
+ stage: StageSchema,
44
+ ticket: z.string(),
45
+ dimensions: z.array(DimensionSchema).min(1),
46
+ });
47
+ export type Judgment = z.infer<typeof JudgmentSchema>;
48
+
49
+ export const JudgeScoresSchema = z.object({
50
+ run_id: z.string(),
51
+ judgments: z.array(JudgmentSchema),
52
+ });
53
+ export type JudgeScores = z.infer<typeof JudgeScoresSchema>;
54
+
55
+ export const DeterministicEntrySchema = z.object({
56
+ ticket: z.string(),
57
+ stage: StageSchema,
58
+ passed: z.boolean(),
59
+ checks: z.array(z.string()),
60
+ error: z.string().optional(),
61
+ });
62
+ export type DeterministicEntry = z.infer<typeof DeterministicEntrySchema>;
63
+
64
+ export const DeterministicScoresSchema = z.object({
65
+ run_id: z.string(),
66
+ entries: z.array(DeterministicEntrySchema),
67
+ });
68
+ export type DeterministicScores = z.infer<typeof DeterministicScoresSchema>;
69
+
70
+ export const ResultSchema = z.object({
71
+ ticket: z.string(),
72
+ stage: StageSchema,
73
+ deterministic: z.object({
74
+ passed: z.boolean(),
75
+ checks: z.array(z.string()),
76
+ error: z.string().optional(),
77
+ }),
78
+ judge: z
79
+ .object({
80
+ passed: z.boolean(),
81
+ dimensions: z.array(DimensionSchema),
82
+ score: z.number().min(0).max(1),
83
+ })
84
+ .nullable(),
85
+ skipped: z.boolean().optional(),
86
+ });
87
+ export type Result = z.infer<typeof ResultSchema>;
88
+
89
+ export const SummarySchema = z.object({
90
+ run_id: z.string(),
91
+ git_sha: z.string(),
92
+ prompt_versions: PromptVersionsSchema,
93
+ results: z.array(ResultSchema),
94
+ overall: z.object({
95
+ passed: z.number().int().nonnegative(),
96
+ failed: z.number().int().nonnegative(),
97
+ total: z.number().int().nonnegative(),
98
+ score: z.number().min(0).max(1),
99
+ }),
100
+ });
101
+ export type Summary = z.infer<typeof SummarySchema>;
@@ -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
+ }
@@ -0,0 +1,15 @@
1
+ export type { CostSummary, LlmCallLog } from './cost';
2
+ export { logLlmCall, summarizeCost } from './cost';
3
+ export { currentYyyyMm, graphPaths } from './paths';
4
+ export { EventSchema, safeParseEvent } from './schema';
5
+ export {
6
+ appendEvents,
7
+ computeEventsHash,
8
+ deriveSnapshot,
9
+ isSnapshotStale,
10
+ loadAllEvents,
11
+ loadSnapshot,
12
+ writeSnapshot,
13
+ } from './store';
14
+ export * from './types';
15
+ export { ulid } from './ulid';
@@ -0,0 +1,27 @@
1
+ import { join } from 'node:path';
2
+
3
+ export interface GraphPaths {
4
+ eventsDir: string;
5
+ snapshotFile: string;
6
+ costLog: string;
7
+ eventsMonthDir(yyyyMm: string): string;
8
+ eventFile(ulid: string, skill: string, ticketId: string, yyyyMm: string): string;
9
+ }
10
+
11
+ export function graphPaths(repoRoot: string): GraphPaths {
12
+ const eventsDir = join(repoRoot, '.xera/graph/events');
13
+ return {
14
+ eventsDir,
15
+ snapshotFile: join(repoRoot, '.xera/graph/snapshot.json'),
16
+ costLog: join(repoRoot, '.xera/cost-log.jsonl'),
17
+ eventsMonthDir: (yyyyMm) => join(eventsDir, yyyyMm),
18
+ eventFile: (ulid, skill, ticketId, yyyyMm) =>
19
+ join(eventsDir, yyyyMm, `${ulid}-${skill}-${ticketId}.jsonl`),
20
+ };
21
+ }
22
+
23
+ export function currentYyyyMm(now: Date = new Date()): string {
24
+ const y = now.getUTCFullYear();
25
+ const m = String(now.getUTCMonth() + 1).padStart(2, '0');
26
+ return `${y}-${m}`;
27
+ }