@xera-ai/core 0.1.7 → 0.3.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/dist/bin/internal.js +1932 -623
- package/dist/{adapter → core/src/adapter}/types.d.ts +1 -1
- package/dist/core/src/adapter/types.d.ts.map +1 -0
- package/dist/core/src/artifact/hash.d.ts.map +1 -0
- package/dist/core/src/artifact/meta.d.ts.map +1 -0
- package/dist/core/src/artifact/paths.d.ts.map +1 -0
- package/dist/core/src/artifact/status.d.ts.map +1 -0
- package/dist/core/src/auth/encrypt.d.ts.map +1 -0
- package/dist/core/src/auth/key.d.ts.map +1 -0
- package/dist/core/src/auth/refresh.d.ts.map +1 -0
- package/dist/core/src/auth/state.d.ts.map +1 -0
- package/dist/core/src/bin-internal/doctor.d.ts +5 -0
- package/dist/core/src/bin-internal/doctor.d.ts.map +1 -0
- package/dist/core/src/bin-internal/eval-deterministic.d.ts +5 -0
- package/dist/core/src/bin-internal/eval-deterministic.d.ts.map +1 -0
- package/dist/core/src/bin-internal/eval-prepare.d.ts +7 -0
- package/dist/core/src/bin-internal/eval-prepare.d.ts.map +1 -0
- package/dist/core/src/bin-internal/eval-report.d.ts +5 -0
- package/dist/core/src/bin-internal/eval-report.d.ts.map +1 -0
- package/dist/core/src/bin-internal/exec.d.ts.map +1 -0
- package/dist/core/src/bin-internal/fetch.d.ts.map +1 -0
- package/dist/core/src/bin-internal/heal-prepare.d.ts +19 -0
- package/dist/core/src/bin-internal/heal-prepare.d.ts.map +1 -0
- package/dist/core/src/bin-internal/index.d.ts.map +1 -0
- package/dist/core/src/bin-internal/lint.d.ts.map +1 -0
- package/dist/core/src/bin-internal/normalize.d.ts.map +1 -0
- package/dist/core/src/bin-internal/post.d.ts.map +1 -0
- package/dist/core/src/bin-internal/promote.d.ts.map +1 -0
- package/dist/core/src/bin-internal/report.d.ts.map +1 -0
- package/dist/core/src/bin-internal/status-cmd.d.ts.map +1 -0
- package/dist/core/src/bin-internal/typecheck.d.ts.map +1 -0
- package/dist/core/src/bin-internal/unlock.d.ts.map +1 -0
- package/dist/core/src/bin-internal/validate-feature.d.ts.map +1 -0
- package/dist/core/src/bin-internal/verify-prompts.d.ts +7 -0
- package/dist/core/src/bin-internal/verify-prompts.d.ts.map +1 -0
- package/dist/core/src/classifier/aggregate.d.ts.map +1 -0
- package/dist/core/src/classifier/history.d.ts.map +1 -0
- package/dist/core/src/classifier/types.d.ts.map +1 -0
- package/dist/core/src/config/define.d.ts.map +1 -0
- package/dist/core/src/config/load.d.ts.map +1 -0
- package/dist/{config → core/src/config}/schema.d.ts.map +1 -1
- package/dist/core/src/eval/paths.d.ts +15 -0
- package/dist/core/src/eval/paths.d.ts.map +1 -0
- package/dist/core/src/eval/run-id.d.ts +6 -0
- package/dist/core/src/eval/run-id.d.ts.map +1 -0
- package/dist/core/src/eval/types.d.ts +551 -0
- package/dist/core/src/eval/types.d.ts.map +1 -0
- package/dist/core/src/index.d.ts.map +1 -0
- package/dist/core/src/jira/client.d.ts.map +1 -0
- package/dist/core/src/jira/fields.d.ts.map +1 -0
- package/dist/core/src/jira/mcp-backend.d.ts.map +1 -0
- package/dist/core/src/jira/rest-backend.d.ts.map +1 -0
- package/dist/core/src/jira/retry.d.ts.map +1 -0
- package/dist/core/src/jira/types.d.ts.map +1 -0
- package/dist/core/src/lock/file-lock.d.ts.map +1 -0
- package/dist/core/src/logging/ndjson-logger.d.ts.map +1 -0
- package/dist/core/src/reporter/jira-comment.d.ts.map +1 -0
- package/dist/core/src/reporter/status-writer.d.ts.map +1 -0
- package/dist/src/index.js +19 -12
- package/dist/web/src/adapter.d.ts +3 -0
- package/dist/web/src/adapter.d.ts.map +1 -0
- package/dist/web/src/auth-setup/define.d.ts +16 -0
- package/dist/web/src/auth-setup/define.d.ts.map +1 -0
- package/dist/web/src/auth-setup/playwright-state.d.ts +2 -0
- package/dist/web/src/auth-setup/playwright-state.d.ts.map +1 -0
- package/dist/web/src/auth-setup/runner.d.ts +12 -0
- package/dist/web/src/auth-setup/runner.d.ts.map +1 -0
- package/dist/web/src/executor/index.d.ts +18 -0
- package/dist/web/src/executor/index.d.ts.map +1 -0
- package/dist/web/src/executor/playwright-args.d.ts +7 -0
- package/dist/web/src/executor/playwright-args.d.ts.map +1 -0
- package/dist/web/src/generator/gherkin-validate.d.ts +9 -0
- package/dist/web/src/generator/gherkin-validate.d.ts.map +1 -0
- package/dist/web/src/generator/lint.d.ts +9 -0
- package/dist/web/src/generator/lint.d.ts.map +1 -0
- package/dist/web/src/generator/pom-scan.d.ts +6 -0
- package/dist/web/src/generator/pom-scan.d.ts.map +1 -0
- package/dist/web/src/generator/promote.d.ts +7 -0
- package/dist/web/src/generator/promote.d.ts.map +1 -0
- package/dist/web/src/generator/selector-rules.d.ts +10 -0
- package/dist/web/src/generator/selector-rules.d.ts.map +1 -0
- package/dist/web/src/generator/typecheck.d.ts +11 -0
- package/dist/web/src/generator/typecheck.d.ts.map +1 -0
- package/dist/web/src/index.d.ts +18 -0
- package/dist/web/src/index.d.ts.map +1 -0
- package/dist/web/src/trace-normalizer/normalize.d.ts +7 -0
- package/dist/web/src/trace-normalizer/normalize.d.ts.map +1 -0
- package/dist/web/src/trace-normalizer/parse.d.ts +37 -0
- package/dist/web/src/trace-normalizer/parse.d.ts.map +1 -0
- package/dist/web/src/trace-normalizer/scrub-rules.d.ts +12 -0
- package/dist/web/src/trace-normalizer/scrub-rules.d.ts.map +1 -0
- package/dist/web/src/trace-normalizer/scrub.d.ts +29 -0
- package/dist/web/src/trace-normalizer/scrub.d.ts.map +1 -0
- package/dist/web/src/trace-normalizer/unzip.d.ts +6 -0
- package/dist/web/src/trace-normalizer/unzip.d.ts.map +1 -0
- package/package.json +3 -2
- package/src/adapter/types.ts +5 -2
- package/src/artifact/meta.ts +1 -1
- package/src/artifact/status.ts +1 -1
- package/src/auth/encrypt.ts +2 -2
- package/src/auth/key.ts +1 -2
- package/src/auth/refresh.ts +4 -1
- package/src/auth/state.ts +2 -2
- package/src/bin-internal/doctor.ts +133 -0
- package/src/bin-internal/eval-deterministic.ts +149 -0
- package/src/bin-internal/eval-prepare.ts +214 -0
- package/src/bin-internal/eval-report.ts +177 -0
- package/src/bin-internal/exec.ts +28 -15
- package/src/bin-internal/fetch.ts +21 -10
- package/src/bin-internal/heal-prepare.ts +230 -0
- package/src/bin-internal/index.ts +25 -11
- package/src/bin-internal/lint.ts +11 -4
- package/src/bin-internal/normalize.ts +23 -9
- package/src/bin-internal/post.ts +10 -4
- package/src/bin-internal/report.ts +3 -3
- package/src/bin-internal/status-cmd.ts +11 -3
- package/src/bin-internal/typecheck.ts +9 -3
- package/src/bin-internal/unlock.ts +12 -4
- package/src/bin-internal/validate-feature.ts +14 -5
- package/src/bin-internal/verify-prompts.ts +59 -0
- package/src/classifier/aggregate.ts +13 -6
- package/src/config/define.ts +3 -1
- package/src/config/load.ts +1 -1
- package/src/config/schema.ts +43 -37
- package/src/eval/paths.ts +32 -0
- package/src/eval/run-id.ts +30 -0
- package/src/eval/types.ts +101 -0
- package/src/jira/client.ts +4 -2
- package/src/jira/fields.ts +4 -2
- package/src/jira/mcp-backend.ts +1 -1
- package/src/jira/rest-backend.ts +17 -5
- package/src/jira/retry.ts +2 -2
- package/src/lock/file-lock.ts +2 -2
- package/src/logging/ndjson-logger.ts +2 -2
- package/src/reporter/jira-comment.ts +13 -7
- package/src/reporter/status-writer.ts +2 -2
- package/dist/adapter/types.d.ts.map +0 -1
- package/dist/artifact/hash.d.ts.map +0 -1
- package/dist/artifact/meta.d.ts.map +0 -1
- package/dist/artifact/paths.d.ts.map +0 -1
- package/dist/artifact/status.d.ts.map +0 -1
- package/dist/auth/encrypt.d.ts.map +0 -1
- package/dist/auth/key.d.ts.map +0 -1
- package/dist/auth/refresh.d.ts.map +0 -1
- package/dist/auth/state.d.ts.map +0 -1
- package/dist/bin-internal/exec.d.ts.map +0 -1
- package/dist/bin-internal/fetch.d.ts.map +0 -1
- package/dist/bin-internal/index.d.ts.map +0 -1
- package/dist/bin-internal/lint.d.ts.map +0 -1
- package/dist/bin-internal/normalize.d.ts.map +0 -1
- package/dist/bin-internal/post.d.ts.map +0 -1
- package/dist/bin-internal/promote.d.ts.map +0 -1
- package/dist/bin-internal/report.d.ts.map +0 -1
- package/dist/bin-internal/status-cmd.d.ts.map +0 -1
- package/dist/bin-internal/typecheck.d.ts.map +0 -1
- package/dist/bin-internal/unlock.d.ts.map +0 -1
- package/dist/bin-internal/validate-feature.d.ts.map +0 -1
- package/dist/classifier/aggregate.d.ts.map +0 -1
- package/dist/classifier/history.d.ts.map +0 -1
- package/dist/classifier/types.d.ts.map +0 -1
- package/dist/config/define.d.ts.map +0 -1
- package/dist/config/load.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/jira/client.d.ts.map +0 -1
- package/dist/jira/fields.d.ts.map +0 -1
- package/dist/jira/mcp-backend.d.ts.map +0 -1
- package/dist/jira/rest-backend.d.ts.map +0 -1
- package/dist/jira/retry.d.ts.map +0 -1
- package/dist/jira/types.d.ts.map +0 -1
- package/dist/lock/file-lock.d.ts.map +0 -1
- package/dist/logging/ndjson-logger.d.ts.map +0 -1
- package/dist/reporter/jira-comment.d.ts.map +0 -1
- package/dist/reporter/status-writer.d.ts.map +0 -1
- /package/dist/{artifact → core/src/artifact}/hash.d.ts +0 -0
- /package/dist/{artifact → core/src/artifact}/meta.d.ts +0 -0
- /package/dist/{artifact → core/src/artifact}/paths.d.ts +0 -0
- /package/dist/{artifact → core/src/artifact}/status.d.ts +0 -0
- /package/dist/{auth → core/src/auth}/encrypt.d.ts +0 -0
- /package/dist/{auth → core/src/auth}/key.d.ts +0 -0
- /package/dist/{auth → core/src/auth}/refresh.d.ts +0 -0
- /package/dist/{auth → core/src/auth}/state.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/exec.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/fetch.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/index.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/lint.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/normalize.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/post.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/promote.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/report.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/status-cmd.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/typecheck.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/unlock.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/validate-feature.d.ts +0 -0
- /package/dist/{classifier → core/src/classifier}/aggregate.d.ts +0 -0
- /package/dist/{classifier → core/src/classifier}/history.d.ts +0 -0
- /package/dist/{classifier → core/src/classifier}/types.d.ts +0 -0
- /package/dist/{config → core/src/config}/define.d.ts +0 -0
- /package/dist/{config → core/src/config}/load.d.ts +0 -0
- /package/dist/{config → core/src/config}/schema.d.ts +0 -0
- /package/dist/{index.d.ts → core/src/index.d.ts} +0 -0
- /package/dist/{jira → core/src/jira}/client.d.ts +0 -0
- /package/dist/{jira → core/src/jira}/fields.d.ts +0 -0
- /package/dist/{jira → core/src/jira}/mcp-backend.d.ts +0 -0
- /package/dist/{jira → core/src/jira}/rest-backend.d.ts +0 -0
- /package/dist/{jira → core/src/jira}/retry.d.ts +0 -0
- /package/dist/{jira → core/src/jira}/types.d.ts +0 -0
- /package/dist/{lock → core/src/lock}/file-lock.d.ts +0 -0
- /package/dist/{logging → core/src/logging}/ndjson-logger.d.ts +0 -0
- /package/dist/{reporter → core/src/reporter}/jira-comment.d.ts +0 -0
- /package/dist/{reporter → core/src/reporter}/status-writer.d.ts +0 -0
package/dist/bin/internal.js
CHANGED
|
@@ -1,504 +1,860 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
|
-
// src/bin-internal/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
// src/bin-internal/doctor.ts
|
|
5
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
6
|
+
import { join as join2 } from "path";
|
|
7
7
|
|
|
8
|
-
// src/
|
|
9
|
-
import { existsSync } from "fs";
|
|
8
|
+
// src/bin-internal/verify-prompts.ts
|
|
9
|
+
import { existsSync, readFileSync } from "fs";
|
|
10
10
|
import { join } from "path";
|
|
11
|
-
|
|
11
|
+
var IN_SCOPE_PROMPTS = [
|
|
12
|
+
"feature-from-story.md",
|
|
13
|
+
"script-from-feature.md",
|
|
14
|
+
"heal-locator.md"
|
|
15
|
+
];
|
|
16
|
+
var REQUIRED_SECTION_HEADING = "## Handling untrusted input";
|
|
17
|
+
var REQUIRED_KEYWORDS = ["UNTRUSTED", "injection-follow", "<XR_"];
|
|
18
|
+
function verifyPrompts(repoRoot) {
|
|
19
|
+
const promptsDir = join(repoRoot, "packages/prompts");
|
|
20
|
+
const results = [];
|
|
21
|
+
for (const filename of IN_SCOPE_PROMPTS) {
|
|
22
|
+
const path = join(promptsDir, filename);
|
|
23
|
+
if (!existsSync(path)) {
|
|
24
|
+
results.push({
|
|
25
|
+
ok: false,
|
|
26
|
+
message: `${filename}: file missing at packages/prompts/${filename}`
|
|
27
|
+
});
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const text = readFileSync(path, "utf8");
|
|
31
|
+
if (!text.includes(REQUIRED_SECTION_HEADING)) {
|
|
32
|
+
results.push({
|
|
33
|
+
ok: false,
|
|
34
|
+
message: `${filename}: missing required section "${REQUIRED_SECTION_HEADING}"`
|
|
35
|
+
});
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
for (const keyword of REQUIRED_KEYWORDS) {
|
|
39
|
+
if (!text.includes(keyword)) {
|
|
40
|
+
results.push({
|
|
41
|
+
ok: false,
|
|
42
|
+
message: `${filename}: missing required keyword "${keyword}" (expected in "${REQUIRED_SECTION_HEADING}" section)`
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return results;
|
|
48
|
+
}
|
|
49
|
+
async function verifyPromptsCmd(_argv) {
|
|
50
|
+
const results = verifyPrompts(process.cwd());
|
|
51
|
+
if (results.length === 0) {
|
|
52
|
+
console.log("[xera:verify-prompts] ok");
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
for (const r of results)
|
|
56
|
+
console.error(`[xera:verify-prompts] ${r.message}`);
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
12
59
|
|
|
13
|
-
// src/
|
|
60
|
+
// src/bin-internal/doctor.ts
|
|
61
|
+
var REQUIRED_FILES_PER_STAGE = {
|
|
62
|
+
"feature-from-story": ["golden/test.feature"],
|
|
63
|
+
"script-from-feature": ["golden/spec-requirements.md"],
|
|
64
|
+
"diagnose-failure": []
|
|
65
|
+
};
|
|
66
|
+
var REQUIRED_SCRIPTS = [
|
|
67
|
+
"xera:eval-prepare",
|
|
68
|
+
"xera:eval-deterministic",
|
|
69
|
+
"xera:eval-report",
|
|
70
|
+
"xera:verify-prompts",
|
|
71
|
+
"xera:doctor"
|
|
72
|
+
];
|
|
73
|
+
function frontmatterField(content, field) {
|
|
74
|
+
const m = content.match(new RegExp(`^${field}:\\s*(\\S+)\\s*$`, "m"));
|
|
75
|
+
return m?.[1] ?? null;
|
|
76
|
+
}
|
|
77
|
+
function checkGoldenEvalDir(repoRoot) {
|
|
78
|
+
const root = join2(repoRoot, "fixtures/golden-eval");
|
|
79
|
+
if (!existsSync2(root))
|
|
80
|
+
return [{ ok: false, message: "fixtures/golden-eval/ does not exist" }];
|
|
81
|
+
const dirs = readdirSync(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
82
|
+
const results = [];
|
|
83
|
+
if (dirs.length < 3) {
|
|
84
|
+
results.push({
|
|
85
|
+
ok: false,
|
|
86
|
+
message: `fixtures/golden-eval/ has ${dirs.length} ticket dir(s); need \u2265 3`
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
for (const entry of dirs) {
|
|
90
|
+
const dir = join2(root, entry.name);
|
|
91
|
+
const metaPath = join2(dir, "meta.json");
|
|
92
|
+
if (!existsSync2(metaPath)) {
|
|
93
|
+
results.push({ ok: false, message: `${entry.name}: meta.json missing` });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
let meta;
|
|
97
|
+
try {
|
|
98
|
+
meta = JSON.parse(readFileSync2(metaPath, "utf8"));
|
|
99
|
+
} catch (err) {
|
|
100
|
+
results.push({
|
|
101
|
+
ok: false,
|
|
102
|
+
message: `${entry.name}: meta.json parse error: ${err.message}`
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const stages = Array.isArray(meta.stages) ? meta.stages : [];
|
|
107
|
+
if (stages.length === 0)
|
|
108
|
+
results.push({ ok: false, message: `${entry.name}: meta.stages is empty` });
|
|
109
|
+
if (!existsSync2(join2(dir, "story.md")))
|
|
110
|
+
results.push({ ok: false, message: `${entry.name}: story.md missing` });
|
|
111
|
+
for (const stage of stages) {
|
|
112
|
+
const required = REQUIRED_FILES_PER_STAGE[stage] ?? [];
|
|
113
|
+
for (const rel of required) {
|
|
114
|
+
if (!existsSync2(join2(dir, rel))) {
|
|
115
|
+
results.push({
|
|
116
|
+
ok: false,
|
|
117
|
+
message: `${meta.id ?? entry.name}: stage "${stage}" declared but ${rel} missing`
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
function checkRubricPrompt(repoRoot) {
|
|
126
|
+
const path = join2(repoRoot, "packages/prompts/eval-rubric.md");
|
|
127
|
+
if (!existsSync2(path))
|
|
128
|
+
return [{ ok: false, message: "packages/prompts/eval-rubric.md missing" }];
|
|
129
|
+
const text = readFileSync2(path, "utf8");
|
|
130
|
+
const id = frontmatterField(text, "id");
|
|
131
|
+
const version = frontmatterField(text, "version");
|
|
132
|
+
if (id !== "eval-rubric")
|
|
133
|
+
return [{ ok: false, message: 'eval-rubric.md frontmatter "id" must be "eval-rubric"' }];
|
|
134
|
+
if (!version)
|
|
135
|
+
return [{ ok: false, message: 'eval-rubric.md frontmatter "version" missing' }];
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
function checkEvalSkill(repoRoot) {
|
|
139
|
+
const path = join2(repoRoot, "packages/skills/xera-eval.md");
|
|
140
|
+
if (!existsSync2(path))
|
|
141
|
+
return [{ ok: false, message: "packages/skills/xera-eval.md missing" }];
|
|
142
|
+
const text = readFileSync2(path, "utf8");
|
|
143
|
+
if (!frontmatterField(text, "name"))
|
|
144
|
+
return [{ ok: false, message: 'xera-eval.md frontmatter "name" missing' }];
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
function checkPromptInjectionPreamble(repoRoot) {
|
|
148
|
+
return verifyPrompts(repoRoot);
|
|
149
|
+
}
|
|
150
|
+
function checkRootScripts(repoRoot) {
|
|
151
|
+
const path = join2(repoRoot, "package.json");
|
|
152
|
+
if (!existsSync2(path))
|
|
153
|
+
return [{ ok: false, message: "root package.json missing" }];
|
|
154
|
+
const pkg = JSON.parse(readFileSync2(path, "utf8"));
|
|
155
|
+
const scripts = pkg.scripts ?? {};
|
|
156
|
+
const missing = REQUIRED_SCRIPTS.filter((s) => typeof scripts[s] !== "string");
|
|
157
|
+
return missing.map((s) => ({ ok: false, message: `root package.json missing script: ${s}` }));
|
|
158
|
+
}
|
|
159
|
+
async function doctorCmd(_argv, opts = {}) {
|
|
160
|
+
const repoRoot = opts.cwd ?? process.cwd();
|
|
161
|
+
const results = [
|
|
162
|
+
...checkGoldenEvalDir(repoRoot),
|
|
163
|
+
...checkRubricPrompt(repoRoot),
|
|
164
|
+
...checkEvalSkill(repoRoot),
|
|
165
|
+
...checkPromptInjectionPreamble(repoRoot),
|
|
166
|
+
...checkRootScripts(repoRoot)
|
|
167
|
+
];
|
|
168
|
+
if (results.length === 0) {
|
|
169
|
+
console.log("[xera:doctor] ok");
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
for (const r of results)
|
|
173
|
+
console.error(`[xera:doctor] ${r.message}`);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/bin-internal/eval-deterministic.ts
|
|
178
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
179
|
+
import { join as join4 } from "path";
|
|
180
|
+
import { validateGherkin } from "@xera-ai/web";
|
|
181
|
+
|
|
182
|
+
// src/eval/paths.ts
|
|
183
|
+
import { join as join3 } from "path";
|
|
184
|
+
function resolveEvalPaths(cwd, runId) {
|
|
185
|
+
const root = join3(cwd, ".xera", "eval", runId);
|
|
186
|
+
return {
|
|
187
|
+
root,
|
|
188
|
+
manifest: join3(root, "manifest.json"),
|
|
189
|
+
lock: join3(root, ".lock"),
|
|
190
|
+
deterministicScores: join3(root, "deterministic-scores.json"),
|
|
191
|
+
judgeScores: join3(root, "judge-scores.json"),
|
|
192
|
+
report: join3(root, "report.md"),
|
|
193
|
+
summary: join3(root, "summary.json"),
|
|
194
|
+
inputsDir: join3(root, "inputs"),
|
|
195
|
+
actualDir: join3(root, "actual"),
|
|
196
|
+
ticketInputsDir: (ticket) => join3(root, "inputs", ticket),
|
|
197
|
+
ticketActualDir: (ticket) => join3(root, "actual", ticket)
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/eval/types.ts
|
|
14
202
|
import { z } from "zod";
|
|
15
|
-
var
|
|
16
|
-
|
|
17
|
-
|
|
203
|
+
var STAGES = ["feature-from-story", "script-from-feature", "diagnose-failure"];
|
|
204
|
+
var StageSchema = z.enum(STAGES);
|
|
205
|
+
var VerdictSchema = z.enum(["PASS", "FAIL", "NA"]);
|
|
206
|
+
var PromptVersionsSchema = z.object({
|
|
207
|
+
"feature-from-story": z.string(),
|
|
208
|
+
"script-from-feature": z.string(),
|
|
209
|
+
"diagnose-failure": z.string(),
|
|
210
|
+
"eval-rubric": z.string()
|
|
18
211
|
});
|
|
19
|
-
var
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
212
|
+
var ManifestSchema = z.object({
|
|
213
|
+
run_id: z.string(),
|
|
214
|
+
started_at: z.string(),
|
|
215
|
+
git_sha: z.string(),
|
|
216
|
+
tickets: z.array(z.string()).min(1),
|
|
217
|
+
stages: z.array(StageSchema).min(1),
|
|
218
|
+
ticket_stages: z.record(z.string(), z.array(StageSchema).min(1)),
|
|
219
|
+
prompt_versions: PromptVersionsSchema,
|
|
220
|
+
flags: z.object({
|
|
221
|
+
force: z.boolean(),
|
|
222
|
+
only_prompt: StageSchema.nullable(),
|
|
223
|
+
only_ticket: z.string().nullable(),
|
|
224
|
+
judge_only: z.boolean()
|
|
225
|
+
})
|
|
25
226
|
});
|
|
26
|
-
var
|
|
27
|
-
|
|
28
|
-
|
|
227
|
+
var DimensionSchema = z.object({
|
|
228
|
+
name: z.string(),
|
|
229
|
+
verdict: VerdictSchema,
|
|
230
|
+
notes: z.string()
|
|
231
|
+
});
|
|
232
|
+
var JudgmentSchema = z.object({
|
|
233
|
+
stage: StageSchema,
|
|
234
|
+
ticket: z.string(),
|
|
235
|
+
dimensions: z.array(DimensionSchema).min(1)
|
|
236
|
+
});
|
|
237
|
+
var JudgeScoresSchema = z.object({
|
|
238
|
+
run_id: z.string(),
|
|
239
|
+
judgments: z.array(JudgmentSchema)
|
|
240
|
+
});
|
|
241
|
+
var DeterministicEntrySchema = z.object({
|
|
242
|
+
ticket: z.string(),
|
|
243
|
+
stage: StageSchema,
|
|
244
|
+
passed: z.boolean(),
|
|
245
|
+
checks: z.array(z.string()),
|
|
246
|
+
error: z.string().optional()
|
|
247
|
+
});
|
|
248
|
+
var DeterministicScoresSchema = z.object({
|
|
249
|
+
run_id: z.string(),
|
|
250
|
+
entries: z.array(DeterministicEntrySchema)
|
|
251
|
+
});
|
|
252
|
+
var ResultSchema = z.object({
|
|
253
|
+
ticket: z.string(),
|
|
254
|
+
stage: StageSchema,
|
|
255
|
+
deterministic: z.object({
|
|
256
|
+
passed: z.boolean(),
|
|
257
|
+
checks: z.array(z.string()),
|
|
258
|
+
error: z.string().optional()
|
|
29
259
|
}),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}).
|
|
35
|
-
|
|
36
|
-
message: "defaultEnv must exist in baseUrl map",
|
|
37
|
-
path: ["defaultEnv"]
|
|
260
|
+
judge: z.object({
|
|
261
|
+
passed: z.boolean(),
|
|
262
|
+
dimensions: z.array(DimensionSchema),
|
|
263
|
+
score: z.number().min(0).max(1)
|
|
264
|
+
}).nullable(),
|
|
265
|
+
skipped: z.boolean().optional()
|
|
38
266
|
});
|
|
39
|
-
var
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
267
|
+
var SummarySchema = z.object({
|
|
268
|
+
run_id: z.string(),
|
|
269
|
+
git_sha: z.string(),
|
|
270
|
+
prompt_versions: PromptVersionsSchema,
|
|
271
|
+
results: z.array(ResultSchema),
|
|
272
|
+
overall: z.object({
|
|
273
|
+
passed: z.number().int().nonnegative(),
|
|
274
|
+
failed: z.number().int().nonnegative(),
|
|
275
|
+
total: z.number().int().nonnegative(),
|
|
276
|
+
score: z.number().min(0).max(1)
|
|
46
277
|
})
|
|
47
278
|
});
|
|
48
|
-
var AISchema = z.object({
|
|
49
|
-
livePageSnapshot: z.boolean().default(true),
|
|
50
|
-
confidenceThreshold: z.enum(["low", "medium", "high"]).default("medium"),
|
|
51
|
-
maxRetries: z.object({
|
|
52
|
-
typecheck: z.number().int().min(0).max(5).default(2),
|
|
53
|
-
lint: z.number().int().min(0).max(5).default(2),
|
|
54
|
-
validateFeature: z.number().int().min(0).max(5).default(2)
|
|
55
|
-
}).default({})
|
|
56
|
-
}).default({});
|
|
57
|
-
var ReportingSchema = z.object({
|
|
58
|
-
language: z.enum(["en", "vi"]).default("en"),
|
|
59
|
-
postToJira: z.boolean().default(true),
|
|
60
|
-
transition: z.object({
|
|
61
|
-
onPass: z.string().nullable().default(null),
|
|
62
|
-
onFail: z.string().nullable().default(null)
|
|
63
|
-
}).default({}),
|
|
64
|
-
artifactLinks: z.enum(["git", "local"]).default("git")
|
|
65
|
-
}).default({});
|
|
66
|
-
var XeraConfigSchema = z.object({
|
|
67
|
-
jira: JiraSchema,
|
|
68
|
-
web: WebSchema,
|
|
69
|
-
ai: AISchema,
|
|
70
|
-
reporting: ReportingSchema,
|
|
71
|
-
adapters: z.array(z.string().min(1)).min(1).default(["web"])
|
|
72
|
-
});
|
|
73
279
|
|
|
74
|
-
// src/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
280
|
+
// src/bin-internal/eval-deterministic.ts
|
|
281
|
+
function checkFeatureFromStory(actualFeaturePath) {
|
|
282
|
+
if (!existsSync3(actualFeaturePath)) {
|
|
283
|
+
return { passed: false, checks: ["validate-feature"], error: "actual missing: test.feature" };
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const r = validateGherkin(readFileSync3(actualFeaturePath, "utf8"));
|
|
287
|
+
if (r.ok)
|
|
288
|
+
return { passed: true, checks: ["validate-feature"] };
|
|
289
|
+
return {
|
|
290
|
+
passed: false,
|
|
291
|
+
checks: ["validate-feature"],
|
|
292
|
+
error: r.errors.map((e) => `line ${e.line}: ${e.message}`).join("; ")
|
|
293
|
+
};
|
|
294
|
+
} catch (err) {
|
|
295
|
+
return { passed: false, checks: ["validate-feature"], error: err.message };
|
|
79
296
|
}
|
|
80
|
-
const mod = await import(pathToFileURL(path).href);
|
|
81
|
-
const raw = mod.default ?? mod;
|
|
82
|
-
return XeraConfigSchema.parse(raw);
|
|
83
297
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
function resolveArtifactPaths(repoRoot, ticket) {
|
|
89
|
-
if (!TICKET_RE.test(ticket)) {
|
|
90
|
-
throw new Error(`Invalid ticket key: "${ticket}" (expected e.g. JIRA-123 or SAMPLE-001)`);
|
|
298
|
+
function checkScriptFromFeature(actualTicketDir) {
|
|
299
|
+
const specPath = join4(actualTicketDir, "spec.ts");
|
|
300
|
+
if (!existsSync3(specPath)) {
|
|
301
|
+
return { passed: false, checks: ["file-presence"], error: "actual missing: spec.ts" };
|
|
91
302
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
303
|
+
return { passed: true, checks: ["file-presence"] };
|
|
304
|
+
}
|
|
305
|
+
function checkDiagnoseFailure(inputsTicketDir, actualTicketDir) {
|
|
306
|
+
const inputPath = join4(inputsTicketDir, "classifier-input.json");
|
|
307
|
+
const actualPath = join4(actualTicketDir, "classification.json");
|
|
308
|
+
if (!existsSync3(actualPath)) {
|
|
309
|
+
return {
|
|
310
|
+
passed: false,
|
|
311
|
+
checks: ["bucket-match"],
|
|
312
|
+
error: "actual missing: classification.json"
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (!existsSync3(inputPath)) {
|
|
316
|
+
return {
|
|
317
|
+
passed: false,
|
|
318
|
+
checks: ["bucket-match"],
|
|
319
|
+
error: "inputs missing: classifier-input.json"
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const golden = JSON.parse(readFileSync3(inputPath, "utf8"));
|
|
323
|
+
const actual = JSON.parse(readFileSync3(actualPath, "utf8"));
|
|
324
|
+
const goldScens = golden.scenarios ?? [];
|
|
325
|
+
const actScens = actual.scenarios ?? [];
|
|
326
|
+
const mismatches = [];
|
|
327
|
+
for (const g of goldScens) {
|
|
328
|
+
const a = actScens.find((s) => s.name === g.name);
|
|
329
|
+
if (!a) {
|
|
330
|
+
mismatches.push(`missing scenario "${g.name}"`);
|
|
331
|
+
continue;
|
|
115
332
|
}
|
|
116
|
-
|
|
333
|
+
if (a.class !== g.class)
|
|
334
|
+
mismatches.push(`scenario "${g.name}": expected class ${g.class}, got ${a.class}`);
|
|
335
|
+
}
|
|
336
|
+
if (mismatches.length > 0) {
|
|
337
|
+
return {
|
|
338
|
+
passed: false,
|
|
339
|
+
checks: ["bucket-match"],
|
|
340
|
+
error: `bucket mismatch \u2014 ${mismatches.join("; ")}`
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return { passed: true, checks: ["bucket-match"] };
|
|
117
344
|
}
|
|
118
|
-
function
|
|
119
|
-
|
|
345
|
+
async function evalDeterministicCmd(argv, opts = {}) {
|
|
346
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
347
|
+
const runId = argv[0];
|
|
348
|
+
if (!runId) {
|
|
349
|
+
console.error("[xera:eval-deterministic] usage: eval-deterministic <run-id>");
|
|
350
|
+
return 1;
|
|
351
|
+
}
|
|
352
|
+
const paths = resolveEvalPaths(cwd, runId);
|
|
353
|
+
if (!existsSync3(paths.manifest)) {
|
|
354
|
+
console.error(`[xera:eval-deterministic] missing manifest.json at ${paths.manifest}`);
|
|
355
|
+
return 1;
|
|
356
|
+
}
|
|
357
|
+
const manifest = ManifestSchema.parse(JSON.parse(readFileSync3(paths.manifest, "utf8")));
|
|
358
|
+
const entries = [];
|
|
359
|
+
for (const [ticket, ticketStages] of Object.entries(manifest.ticket_stages)) {
|
|
360
|
+
for (const stage of ticketStages) {
|
|
361
|
+
const inputsDir = paths.ticketInputsDir(ticket);
|
|
362
|
+
const actualDir = paths.ticketActualDir(ticket);
|
|
363
|
+
let result;
|
|
364
|
+
if (stage === "feature-from-story") {
|
|
365
|
+
result = checkFeatureFromStory(join4(actualDir, "test.feature"));
|
|
366
|
+
} else if (stage === "script-from-feature") {
|
|
367
|
+
result = checkScriptFromFeature(actualDir);
|
|
368
|
+
} else {
|
|
369
|
+
result = checkDiagnoseFailure(inputsDir, actualDir);
|
|
370
|
+
}
|
|
371
|
+
const entry = {
|
|
372
|
+
ticket,
|
|
373
|
+
stage,
|
|
374
|
+
passed: result.passed,
|
|
375
|
+
checks: result.checks
|
|
376
|
+
};
|
|
377
|
+
if (result.error !== undefined)
|
|
378
|
+
entry.error = result.error;
|
|
379
|
+
entries.push(entry);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const scores = { run_id: runId, entries };
|
|
383
|
+
DeterministicScoresSchema.parse(scores);
|
|
384
|
+
writeFileSync(paths.deterministicScores, JSON.stringify(scores, null, 2));
|
|
385
|
+
console.log(`[xera:eval-deterministic] wrote ${entries.length} entries`);
|
|
386
|
+
return 0;
|
|
120
387
|
}
|
|
121
388
|
|
|
122
|
-
// src/
|
|
123
|
-
import {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
389
|
+
// src/bin-internal/eval-prepare.ts
|
|
390
|
+
import {
|
|
391
|
+
copyFileSync,
|
|
392
|
+
existsSync as existsSync5,
|
|
393
|
+
mkdirSync as mkdirSync2,
|
|
394
|
+
readFileSync as readFileSync5,
|
|
395
|
+
readdirSync as readdirSync2,
|
|
396
|
+
writeFileSync as writeFileSync3
|
|
397
|
+
} from "fs";
|
|
398
|
+
import { join as join5 } from "path";
|
|
399
|
+
|
|
400
|
+
// src/eval/run-id.ts
|
|
401
|
+
import { execSync } from "child_process";
|
|
402
|
+
function defaultGetGitSha() {
|
|
403
|
+
try {
|
|
404
|
+
return execSync("git rev-parse HEAD", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
405
|
+
} catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
127
408
|
}
|
|
128
|
-
function
|
|
129
|
-
return
|
|
409
|
+
function pad(n) {
|
|
410
|
+
return n.toString().padStart(2, "0");
|
|
130
411
|
}
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
412
|
+
function generateRunId(opts = {}) {
|
|
413
|
+
const getGitSha = opts.getGitSha ?? defaultGetGitSha;
|
|
414
|
+
const now = (opts.now ?? (() => new Date))();
|
|
415
|
+
const date = `${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}`;
|
|
416
|
+
const time = `${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}`;
|
|
417
|
+
const sha = getGitSha();
|
|
418
|
+
const short = sha ? sha.slice(0, 7) : "nogit";
|
|
419
|
+
return `${date}-${time}-${short}`;
|
|
135
420
|
}
|
|
136
421
|
|
|
137
|
-
// src/
|
|
138
|
-
import { existsSync as
|
|
422
|
+
// src/lock/file-lock.ts
|
|
423
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
424
|
+
import { hostname } from "os";
|
|
139
425
|
import { dirname } from "path";
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
adapter: z2.string(),
|
|
144
|
-
xera_version: z2.string(),
|
|
145
|
-
prompts_version: z2.string(),
|
|
146
|
-
fetched_at: z2.string().optional(),
|
|
147
|
-
story_hash: z2.string().optional(),
|
|
148
|
-
feature_generated_at: z2.string().optional(),
|
|
149
|
-
feature_generated_from_story_hash: z2.string().optional(),
|
|
150
|
-
feature_hash: z2.string().optional(),
|
|
151
|
-
script_generated_at: z2.string().optional(),
|
|
152
|
-
script_generated_from_feature_hash: z2.string().optional(),
|
|
153
|
-
script_warnings: z2.array(z2.string()).optional()
|
|
154
|
-
});
|
|
155
|
-
function readMeta(path) {
|
|
156
|
-
if (!existsSync3(path))
|
|
157
|
-
return null;
|
|
158
|
-
return MetaJsonSchema.parse(JSON.parse(readFileSync2(path, "utf8")));
|
|
159
|
-
}
|
|
160
|
-
function writeMeta(path, meta) {
|
|
426
|
+
function acquireLock(path, runId) {
|
|
427
|
+
if (existsSync4(path))
|
|
428
|
+
return false;
|
|
161
429
|
mkdirSync(dirname(path), { recursive: true });
|
|
162
|
-
|
|
430
|
+
const data = {
|
|
431
|
+
pid: process.pid,
|
|
432
|
+
hostname: hostname(),
|
|
433
|
+
started_at: new Date().toISOString(),
|
|
434
|
+
run_id: runId
|
|
435
|
+
};
|
|
436
|
+
try {
|
|
437
|
+
writeFileSync2(path, JSON.stringify(data), { flag: "wx" });
|
|
438
|
+
return true;
|
|
439
|
+
} catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
163
442
|
}
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
443
|
+
function releaseLock(path) {
|
|
444
|
+
if (existsSync4(path))
|
|
445
|
+
unlinkSync(path);
|
|
446
|
+
}
|
|
447
|
+
function readLock(path) {
|
|
448
|
+
if (!existsSync4(path))
|
|
449
|
+
return null;
|
|
450
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
451
|
+
}
|
|
452
|
+
function isLockStale(path) {
|
|
453
|
+
const lock = readLock(path);
|
|
454
|
+
if (!lock)
|
|
455
|
+
return true;
|
|
456
|
+
if (lock.hostname !== hostname()) {
|
|
457
|
+
return false;
|
|
168
458
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
459
|
+
try {
|
|
460
|
+
process.kill(lock.pid, 0);
|
|
461
|
+
return false;
|
|
462
|
+
} catch {
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function forceUnlock(path) {
|
|
467
|
+
releaseLock(path);
|
|
172
468
|
}
|
|
173
469
|
|
|
174
|
-
// src/
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
backend: "mcp",
|
|
186
|
-
async fetchTicket(key, _fields) {
|
|
187
|
-
const cachePath = join3(tmpDir, `${key}.json`);
|
|
188
|
-
if (!existsSync4(cachePath)) {
|
|
189
|
-
throw new Error(`MCP-mode fetch requires the skill to first call mcp__atlassian__getJiraIssue and write ${cachePath}. ` + `If you are running this directly, unset ${MCP_ENV} to use REST.`);
|
|
470
|
+
// src/bin-internal/eval-prepare.ts
|
|
471
|
+
function parseFlags(argv) {
|
|
472
|
+
const flags = { force: false, only_prompt: null, only_ticket: null };
|
|
473
|
+
for (const arg of argv) {
|
|
474
|
+
if (arg === "--force")
|
|
475
|
+
flags.force = true;
|
|
476
|
+
else if (arg.startsWith("--prompt=")) {
|
|
477
|
+
const v = arg.slice("--prompt=".length);
|
|
478
|
+
if (!STAGES.includes(v)) {
|
|
479
|
+
return { error: `Unknown stage: ${v}. Valid: ${STAGES.join(", ")}.` };
|
|
190
480
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
481
|
+
flags.only_prompt = v;
|
|
482
|
+
} else if (arg.startsWith("--ticket=")) {
|
|
483
|
+
flags.only_ticket = arg.slice("--ticket=".length);
|
|
484
|
+
} else {
|
|
485
|
+
return { error: `Unknown argument: ${arg}` };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return flags;
|
|
489
|
+
}
|
|
490
|
+
function readPromptVersion(repoRoot, name) {
|
|
491
|
+
const path = join5(repoRoot, "packages/prompts", `${name}.md`);
|
|
492
|
+
if (!existsSync5(path))
|
|
493
|
+
return "0.0.0";
|
|
494
|
+
const text = readFileSync5(path, "utf8");
|
|
495
|
+
const m = /^version:\s*(\S+)\s*$/m.exec(text);
|
|
496
|
+
return m?.[1] ?? "0.0.0";
|
|
497
|
+
}
|
|
498
|
+
function discoverEvalTickets(repoRoot) {
|
|
499
|
+
const root = join5(repoRoot, "fixtures/golden-eval");
|
|
500
|
+
if (!existsSync5(root))
|
|
501
|
+
return [];
|
|
502
|
+
const out = [];
|
|
503
|
+
for (const entry of readdirSync2(root, { withFileTypes: true })) {
|
|
504
|
+
if (!entry.isDirectory())
|
|
505
|
+
continue;
|
|
506
|
+
if (entry.name === "README.md" || entry.name.startsWith("."))
|
|
507
|
+
continue;
|
|
508
|
+
const dir = join5(root, entry.name);
|
|
509
|
+
const metaPath = join5(dir, "meta.json");
|
|
510
|
+
if (!existsSync5(metaPath))
|
|
511
|
+
continue;
|
|
512
|
+
const meta = JSON.parse(readFileSync5(metaPath, "utf8"));
|
|
513
|
+
out.push({ id: meta.id, dir, stages: meta.stages });
|
|
514
|
+
}
|
|
515
|
+
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
516
|
+
}
|
|
517
|
+
function discoverClassifierTickets(repoRoot) {
|
|
518
|
+
const root = join5(repoRoot, "fixtures/golden-tickets");
|
|
519
|
+
if (!existsSync5(root))
|
|
520
|
+
return [];
|
|
521
|
+
const out = [];
|
|
522
|
+
for (const entry of readdirSync2(root, { withFileTypes: true })) {
|
|
523
|
+
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
|
524
|
+
continue;
|
|
525
|
+
const path = join5(root, entry.name);
|
|
526
|
+
const data = JSON.parse(readFileSync5(path, "utf8"));
|
|
527
|
+
if (typeof data.ticket === "string")
|
|
528
|
+
out.push({ id: data.ticket, path });
|
|
529
|
+
}
|
|
530
|
+
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
531
|
+
}
|
|
532
|
+
async function evalPrepareCmd(argv, opts = {}) {
|
|
533
|
+
const repoRoot = opts.cwd ?? process.cwd();
|
|
534
|
+
const flags = parseFlags(argv);
|
|
535
|
+
if ("error" in flags) {
|
|
536
|
+
console.error(`[xera:eval-prepare] ${flags.error}`);
|
|
537
|
+
return 1;
|
|
538
|
+
}
|
|
539
|
+
const evalTickets = discoverEvalTickets(repoRoot);
|
|
540
|
+
const classifierTickets = discoverClassifierTickets(repoRoot);
|
|
541
|
+
const stages = flags.only_prompt ? [flags.only_prompt] : [...STAGES];
|
|
542
|
+
const wantsEval = stages.some((s) => s !== "diagnose-failure");
|
|
543
|
+
const wantsClassifier = stages.includes("diagnose-failure");
|
|
544
|
+
let selectedTickets = [];
|
|
545
|
+
if (wantsEval)
|
|
546
|
+
selectedTickets.push(...evalTickets.map((t) => t.id));
|
|
547
|
+
if (wantsClassifier)
|
|
548
|
+
selectedTickets.push(...classifierTickets.map((t) => t.id));
|
|
549
|
+
selectedTickets = [...new Set(selectedTickets)].sort();
|
|
550
|
+
if (flags.only_ticket) {
|
|
551
|
+
if (!selectedTickets.includes(flags.only_ticket)) {
|
|
552
|
+
console.error(`[xera:eval-prepare] No golden fixture for ${flags.only_ticket}`);
|
|
553
|
+
return 1;
|
|
554
|
+
}
|
|
555
|
+
selectedTickets = [flags.only_ticket];
|
|
556
|
+
}
|
|
557
|
+
if (selectedTickets.length === 0) {
|
|
558
|
+
console.error("[xera:eval-prepare] No tickets selected (after filters).");
|
|
559
|
+
return 1;
|
|
560
|
+
}
|
|
561
|
+
const ticket_stages = {};
|
|
562
|
+
for (const ticket of selectedTickets) {
|
|
563
|
+
const evalT = evalTickets.find((t) => t.id === ticket);
|
|
564
|
+
let ticketDeclared;
|
|
565
|
+
if (evalT) {
|
|
566
|
+
ticketDeclared = evalT.stages;
|
|
567
|
+
} else {
|
|
568
|
+
ticketDeclared = ["diagnose-failure"];
|
|
569
|
+
}
|
|
570
|
+
const intersection = ticketDeclared.filter((s) => stages.includes(s));
|
|
571
|
+
if (intersection.length > 0) {
|
|
572
|
+
ticket_stages[ticket] = intersection;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
selectedTickets = selectedTickets.filter((t) => ticket_stages[t] !== undefined);
|
|
576
|
+
if (selectedTickets.length === 0) {
|
|
577
|
+
console.error("[xera:eval-prepare] No tickets applicable to requested stages.");
|
|
578
|
+
return 1;
|
|
579
|
+
}
|
|
580
|
+
const runId = generateRunId({
|
|
581
|
+
...opts.now ? { now: opts.now } : {},
|
|
582
|
+
...opts.getGitSha ? { getGitSha: opts.getGitSha } : {}
|
|
583
|
+
});
|
|
584
|
+
const paths = resolveEvalPaths(repoRoot, runId);
|
|
585
|
+
if (existsSync5(paths.root) && !flags.force) {
|
|
586
|
+
console.error(`[xera:eval-prepare] run dir already exists: ${paths.root}. Pass --force to re-run.`);
|
|
587
|
+
return 1;
|
|
588
|
+
}
|
|
589
|
+
mkdirSync2(paths.inputsDir, { recursive: true });
|
|
590
|
+
mkdirSync2(paths.actualDir, { recursive: true });
|
|
591
|
+
for (const ticket of selectedTickets) {
|
|
592
|
+
const ticketInputs = paths.ticketInputsDir(ticket);
|
|
593
|
+
mkdirSync2(ticketInputs, { recursive: true });
|
|
594
|
+
const evalT = evalTickets.find((t) => t.id === ticket);
|
|
595
|
+
const classT = classifierTickets.find((t) => t.id === ticket);
|
|
596
|
+
if (evalT) {
|
|
597
|
+
copyFileSync(join5(evalT.dir, "story.md"), join5(ticketInputs, "story.md"));
|
|
598
|
+
const featurePath = join5(evalT.dir, "golden/test.feature");
|
|
599
|
+
if (existsSync5(featurePath))
|
|
600
|
+
copyFileSync(featurePath, join5(ticketInputs, "test.feature"));
|
|
601
|
+
}
|
|
602
|
+
if (classT) {
|
|
603
|
+
copyFileSync(classT.path, join5(ticketInputs, "classifier-input.json"));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const now = (opts.now ?? (() => new Date))();
|
|
607
|
+
const manifest = {
|
|
608
|
+
run_id: runId,
|
|
609
|
+
started_at: now.toISOString(),
|
|
610
|
+
git_sha: runId.split("-")[2] ?? "nogit",
|
|
611
|
+
tickets: selectedTickets,
|
|
612
|
+
stages,
|
|
613
|
+
ticket_stages,
|
|
614
|
+
prompt_versions: {
|
|
615
|
+
"feature-from-story": readPromptVersion(repoRoot, "feature-from-story"),
|
|
616
|
+
"script-from-feature": readPromptVersion(repoRoot, "script-from-feature"),
|
|
617
|
+
"diagnose-failure": readPromptVersion(repoRoot, "diagnose-failure"),
|
|
618
|
+
"eval-rubric": readPromptVersion(repoRoot, "eval-rubric")
|
|
202
619
|
},
|
|
203
|
-
|
|
204
|
-
|
|
620
|
+
flags: {
|
|
621
|
+
force: flags.force,
|
|
622
|
+
only_prompt: flags.only_prompt,
|
|
623
|
+
only_ticket: flags.only_ticket,
|
|
624
|
+
judge_only: false
|
|
205
625
|
}
|
|
206
626
|
};
|
|
627
|
+
ManifestSchema.parse(manifest);
|
|
628
|
+
writeFileSync3(paths.manifest, JSON.stringify(manifest, null, 2));
|
|
629
|
+
if (!acquireLock(paths.lock, runId)) {
|
|
630
|
+
console.error(`[xera:eval-prepare] failed to acquire lock at ${paths.lock}`);
|
|
631
|
+
return 4;
|
|
632
|
+
}
|
|
633
|
+
console.log(`[xera:eval-prepare] prepared ${selectedTickets.length} ticket(s) for stages: ${stages.join(", ")}`);
|
|
634
|
+
console.log(`RUN_ID=${runId}`);
|
|
635
|
+
return 0;
|
|
207
636
|
}
|
|
208
637
|
|
|
209
|
-
// src/
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
638
|
+
// src/bin-internal/eval-report.ts
|
|
639
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
640
|
+
function scoreJudgment(j) {
|
|
641
|
+
const nonNa = j.dimensions.filter((d) => d.verdict !== "NA");
|
|
642
|
+
if (nonNa.length === 0)
|
|
643
|
+
return { passed: true, score: 1 };
|
|
644
|
+
const passes = nonNa.filter((d) => d.verdict === "PASS").length;
|
|
645
|
+
const score = passes / nonNa.length;
|
|
646
|
+
const passed = nonNa.every((d) => d.verdict === "PASS");
|
|
647
|
+
return { passed, score };
|
|
648
|
+
}
|
|
649
|
+
function renderReport(summary) {
|
|
650
|
+
const lines = [];
|
|
651
|
+
lines.push(`# xera eval report ${summary.run_id}`);
|
|
652
|
+
lines.push("");
|
|
653
|
+
lines.push(`**Git SHA:** \`${summary.git_sha}\``);
|
|
654
|
+
lines.push("");
|
|
655
|
+
lines.push("**Prompt versions:**");
|
|
656
|
+
for (const [k, v] of Object.entries(summary.prompt_versions))
|
|
657
|
+
lines.push(`- \`${k}\`: ${v}`);
|
|
658
|
+
lines.push("");
|
|
659
|
+
lines.push(`**Overall:** ${summary.overall.passed}/${summary.overall.total} PASS (score ${(summary.overall.score * 100).toFixed(0)}%)`);
|
|
660
|
+
lines.push("");
|
|
661
|
+
lines.push("## Results");
|
|
662
|
+
lines.push("");
|
|
663
|
+
lines.push("| Ticket | Stage | Deterministic | Judge | Score |");
|
|
664
|
+
lines.push("|---|---|---|---|---|");
|
|
665
|
+
for (const r of summary.results) {
|
|
666
|
+
const det = r.deterministic.passed ? "PASS" : `FAIL (${r.deterministic.error ?? ""})`;
|
|
667
|
+
const judge = r.skipped ? "SKIPPED" : r.judge ? r.judge.passed ? "PASS" : "FAIL" : "SKIPPED";
|
|
668
|
+
const score = r.judge ? `${(r.judge.score * 100).toFixed(0)}%` : "\u2014";
|
|
669
|
+
lines.push(`| ${r.ticket} | ${r.stage} | ${det} | ${judge} | ${score} |`);
|
|
227
670
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const ticket = {
|
|
240
|
-
key: json.key,
|
|
241
|
-
summary: String(f.summary ?? ""),
|
|
242
|
-
story: String(f[fields.story] ?? ""),
|
|
243
|
-
attachments,
|
|
244
|
-
raw: f
|
|
245
|
-
};
|
|
246
|
-
if (fields.acceptanceCriteria) {
|
|
247
|
-
ticket.acceptanceCriteria = String(f[fields.acceptanceCriteria] ?? "");
|
|
248
|
-
}
|
|
249
|
-
return ticket;
|
|
250
|
-
},
|
|
251
|
-
async postComment(key, body) {
|
|
252
|
-
const r = await req(`/rest/api/3/issue/${encodeURIComponent(key)}/comment`, {
|
|
253
|
-
method: "POST",
|
|
254
|
-
body: JSON.stringify({
|
|
255
|
-
body: { type: "doc", version: 1, content: [{ type: "paragraph", content: [{ type: "text", text: body }] }] }
|
|
256
|
-
})
|
|
257
|
-
});
|
|
258
|
-
const json = await r.json();
|
|
259
|
-
return { id: json.id };
|
|
260
|
-
},
|
|
261
|
-
async transitionStatus(key, statusName) {
|
|
262
|
-
const tr = await req(`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`);
|
|
263
|
-
const json = await tr.json();
|
|
264
|
-
const t = json.transitions.find((x) => x.name === statusName);
|
|
265
|
-
if (!t)
|
|
266
|
-
throw new Error(`No transition named "${statusName}" available for ${key}`);
|
|
267
|
-
await req(`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`, {
|
|
268
|
-
method: "POST",
|
|
269
|
-
body: JSON.stringify({ transition: { id: t.id } })
|
|
270
|
-
});
|
|
271
|
-
},
|
|
272
|
-
async listFields(sampleKey) {
|
|
273
|
-
const r = await req(`/rest/api/3/issue/${encodeURIComponent(sampleKey)}?fields=*all`);
|
|
274
|
-
const json = await r.json();
|
|
275
|
-
return Object.entries(json.fields).map(([id, value]) => ({
|
|
276
|
-
id,
|
|
277
|
-
name: id,
|
|
278
|
-
hasContent: value !== null && value !== undefined && value !== ""
|
|
279
|
-
}));
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// src/jira/client.ts
|
|
285
|
-
async function createJiraClient(opts) {
|
|
286
|
-
if (opts.preferMcp !== false) {
|
|
287
|
-
const mcp = await createMcpBackend(opts.baseUrl);
|
|
288
|
-
if (mcp)
|
|
289
|
-
return mcp;
|
|
290
|
-
}
|
|
291
|
-
if (!opts.rest) {
|
|
292
|
-
throw new Error("Atlassian MCP not connected and no REST credentials provided (JIRA_EMAIL + JIRA_API_TOKEN).");
|
|
293
|
-
}
|
|
294
|
-
return createRestBackend(opts.baseUrl, opts.rest);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// src/bin-internal/fetch.ts
|
|
298
|
-
async function fetchCmd(argv, opts = {}) {
|
|
299
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
300
|
-
const ticket = argv[0];
|
|
301
|
-
if (!ticket) {
|
|
302
|
-
console.error("[xera:fetch] usage: xera-internal fetch <TICKET>");
|
|
303
|
-
return 1;
|
|
304
|
-
}
|
|
305
|
-
const config = await loadConfig(cwd);
|
|
306
|
-
const paths = resolveArtifactPaths(cwd, ticket);
|
|
307
|
-
let t;
|
|
308
|
-
if (process.env.XERA_TEST_JIRA) {
|
|
309
|
-
t = JSON.parse(process.env.XERA_TEST_JIRA);
|
|
310
|
-
} else {
|
|
311
|
-
const client = await createJiraClient({
|
|
312
|
-
baseUrl: config.jira.baseUrl,
|
|
313
|
-
preferMcp: true,
|
|
314
|
-
...process.env.JIRA_EMAIL && process.env.JIRA_API_TOKEN ? { rest: { email: process.env.JIRA_EMAIL, apiToken: process.env.JIRA_API_TOKEN } } : {}
|
|
315
|
-
});
|
|
316
|
-
const fieldMap = config.jira.fields.acceptanceCriteria !== undefined ? { story: config.jira.fields.story, acceptanceCriteria: config.jira.fields.acceptanceCriteria } : { story: config.jira.fields.story };
|
|
317
|
-
t = await client.fetchTicket(ticket, fieldMap);
|
|
318
|
-
}
|
|
319
|
-
const story = renderStory(t);
|
|
320
|
-
mkdirSync3(dirname2(paths.storyPath), { recursive: true });
|
|
321
|
-
writeFileSync3(paths.storyPath, story);
|
|
322
|
-
const existing = readMeta(paths.metaPath);
|
|
323
|
-
writeMeta(paths.metaPath, {
|
|
324
|
-
ticket,
|
|
325
|
-
adapter: "web",
|
|
326
|
-
xera_version: "0.1.0",
|
|
327
|
-
prompts_version: "1.0.0",
|
|
328
|
-
...existing ?? {},
|
|
329
|
-
story_hash: hashString(story),
|
|
330
|
-
fetched_at: new Date().toISOString()
|
|
331
|
-
});
|
|
332
|
-
console.log(`[xera:fetch] wrote ${paths.storyPath}`);
|
|
333
|
-
return 0;
|
|
334
|
-
}
|
|
335
|
-
function renderStory(t) {
|
|
336
|
-
const lines = [];
|
|
337
|
-
lines.push(`# ${t.key}: ${t.summary}`, "");
|
|
338
|
-
const story = t.story.trim();
|
|
339
|
-
if (/^##\s+story\b/i.test(story)) {
|
|
340
|
-
lines.push(story, "");
|
|
341
|
-
} else {
|
|
342
|
-
lines.push("## Story", "", story, "");
|
|
343
|
-
}
|
|
344
|
-
if (t.acceptanceCriteria && t.acceptanceCriteria.trim()) {
|
|
345
|
-
const ac = t.acceptanceCriteria.trim();
|
|
346
|
-
if (/^##\s+acceptance\s+criteria\b/i.test(ac)) {
|
|
347
|
-
lines.push(ac, "");
|
|
348
|
-
} else {
|
|
349
|
-
lines.push("## Acceptance Criteria", "", ac, "");
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
if (t.attachments.length > 0) {
|
|
353
|
-
lines.push("## Attachments", "", ...t.attachments.map((a) => `- [${a.filename}](${a.url})`), "");
|
|
671
|
+
lines.push("");
|
|
672
|
+
lines.push("## Dimension breakdown");
|
|
673
|
+
lines.push("");
|
|
674
|
+
for (const r of summary.results) {
|
|
675
|
+
if (!r.judge || r.judge.dimensions.length === 0)
|
|
676
|
+
continue;
|
|
677
|
+
lines.push(`### ${r.ticket} \u2014 ${r.stage}`);
|
|
678
|
+
lines.push("");
|
|
679
|
+
for (const d of r.judge.dimensions)
|
|
680
|
+
lines.push(`- **${d.name}** \u2014 ${d.verdict}: ${d.notes}`);
|
|
681
|
+
lines.push("");
|
|
354
682
|
}
|
|
355
683
|
return lines.join(`
|
|
356
684
|
`);
|
|
357
685
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const ticket = argv[0];
|
|
364
|
-
if (!ticket) {
|
|
365
|
-
console.error("[xera:validate-feature] usage: validate-feature <TICKET>");
|
|
686
|
+
async function evalReportCmd(argv, opts = {}) {
|
|
687
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
688
|
+
const runId = argv[0];
|
|
689
|
+
if (!runId) {
|
|
690
|
+
console.error("[xera:eval-report] usage: eval-report <run-id>");
|
|
366
691
|
return 1;
|
|
367
692
|
}
|
|
368
|
-
const paths =
|
|
369
|
-
if (!
|
|
370
|
-
console.error(`[xera:
|
|
693
|
+
const paths = resolveEvalPaths(cwd, runId);
|
|
694
|
+
if (!existsSync6(paths.manifest)) {
|
|
695
|
+
console.error(`[xera:eval-report] missing manifest.json at ${paths.manifest}`);
|
|
371
696
|
return 1;
|
|
372
697
|
}
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
698
|
+
const manifest = ManifestSchema.parse(JSON.parse(readFileSync6(paths.manifest, "utf8")));
|
|
699
|
+
try {
|
|
700
|
+
let det;
|
|
701
|
+
let judge;
|
|
702
|
+
try {
|
|
703
|
+
det = DeterministicScoresSchema.parse(JSON.parse(readFileSync6(paths.deterministicScores, "utf8")));
|
|
704
|
+
} catch (err) {
|
|
705
|
+
console.error(`[xera:eval-report] invalid deterministic-scores.json: ${err.message}`);
|
|
706
|
+
return 2;
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
judge = JudgeScoresSchema.parse(JSON.parse(readFileSync6(paths.judgeScores, "utf8")));
|
|
710
|
+
} catch (err) {
|
|
711
|
+
console.error(`[xera:eval-report] invalid judge-scores.json: ${err.message}`);
|
|
712
|
+
return 2;
|
|
713
|
+
}
|
|
714
|
+
const results = [];
|
|
715
|
+
for (const detEntry of det.entries) {
|
|
716
|
+
const judgment = judge.judgments.find((j) => j.ticket === detEntry.ticket && j.stage === detEntry.stage);
|
|
717
|
+
if (!judgment && detEntry.error?.startsWith("actual missing")) {
|
|
718
|
+
const r2 = {
|
|
719
|
+
ticket: detEntry.ticket,
|
|
720
|
+
stage: detEntry.stage,
|
|
721
|
+
deterministic: {
|
|
722
|
+
passed: detEntry.passed,
|
|
723
|
+
checks: detEntry.checks,
|
|
724
|
+
...detEntry.error !== undefined ? { error: detEntry.error } : {}
|
|
725
|
+
},
|
|
726
|
+
judge: null,
|
|
727
|
+
skipped: true
|
|
728
|
+
};
|
|
729
|
+
results.push(r2);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (!judgment) {
|
|
733
|
+
const r2 = {
|
|
734
|
+
ticket: detEntry.ticket,
|
|
735
|
+
stage: detEntry.stage,
|
|
736
|
+
deterministic: {
|
|
737
|
+
passed: detEntry.passed,
|
|
738
|
+
checks: detEntry.checks,
|
|
739
|
+
...detEntry.error !== undefined ? { error: detEntry.error } : {}
|
|
740
|
+
},
|
|
741
|
+
judge: { passed: false, dimensions: [], score: 0 }
|
|
742
|
+
};
|
|
743
|
+
results.push(r2);
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
const { passed, score } = scoreJudgment(judgment);
|
|
747
|
+
const r = {
|
|
748
|
+
ticket: detEntry.ticket,
|
|
749
|
+
stage: detEntry.stage,
|
|
750
|
+
deterministic: {
|
|
751
|
+
passed: detEntry.passed,
|
|
752
|
+
checks: detEntry.checks,
|
|
753
|
+
...detEntry.error !== undefined ? { error: detEntry.error } : {}
|
|
754
|
+
},
|
|
755
|
+
judge: { passed, dimensions: judgment.dimensions, score }
|
|
756
|
+
};
|
|
757
|
+
results.push(r);
|
|
758
|
+
}
|
|
759
|
+
const counted = results.filter((r) => !r.skipped);
|
|
760
|
+
const passedCount = counted.filter((r) => r.deterministic.passed && r.judge?.passed).length;
|
|
761
|
+
const failedCount = counted.length - passedCount;
|
|
762
|
+
const avgScore = counted.length === 0 ? 0 : counted.reduce((acc, r) => acc + (r.deterministic.passed && r.judge ? r.judge.score : 0), 0) / counted.length;
|
|
763
|
+
const summary = {
|
|
764
|
+
run_id: runId,
|
|
765
|
+
git_sha: manifest.git_sha,
|
|
766
|
+
prompt_versions: manifest.prompt_versions,
|
|
767
|
+
results,
|
|
768
|
+
overall: { passed: passedCount, failed: failedCount, total: counted.length, score: avgScore }
|
|
769
|
+
};
|
|
770
|
+
SummarySchema.parse(summary);
|
|
771
|
+
writeFileSync4(paths.summary, JSON.stringify(summary, null, 2));
|
|
772
|
+
writeFileSync4(paths.report, renderReport(summary));
|
|
773
|
+
console.log(`[xera:eval-report] ${passedCount}/${counted.length} PASS (avg ${(avgScore * 100).toFixed(0)}%)`);
|
|
376
774
|
return 0;
|
|
775
|
+
} finally {
|
|
776
|
+
releaseLock(paths.lock);
|
|
377
777
|
}
|
|
378
|
-
for (const e of r.errors)
|
|
379
|
-
console.error(`[xera:validate-feature] line ${e.line}: ${e.message}`);
|
|
380
|
-
return 2;
|
|
381
778
|
}
|
|
382
779
|
|
|
383
|
-
// src/bin-internal/
|
|
384
|
-
import {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
console.error("[xera:typecheck] usage: typecheck <TICKET>");
|
|
389
|
-
return 1;
|
|
390
|
-
}
|
|
391
|
-
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
392
|
-
const r = await typecheckTicket(paths.ticketDir);
|
|
393
|
-
if (r.ok) {
|
|
394
|
-
console.log("[xera:typecheck] ok");
|
|
395
|
-
return 0;
|
|
396
|
-
}
|
|
397
|
-
for (const e of r.errors)
|
|
398
|
-
console.error(`[xera:typecheck] ${e}`);
|
|
399
|
-
return 2;
|
|
400
|
-
}
|
|
780
|
+
// src/bin-internal/exec.ts
|
|
781
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5 } from "fs";
|
|
782
|
+
import { join as join9 } from "path";
|
|
783
|
+
import { chromium } from "@playwright/test";
|
|
784
|
+
import { runAuthSetup, runPlaywright, stagePlaywrightState } from "@xera-ai/web";
|
|
401
785
|
|
|
402
|
-
// src/
|
|
403
|
-
import {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (!ticket) {
|
|
407
|
-
|
|
408
|
-
return 1;
|
|
409
|
-
}
|
|
410
|
-
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
411
|
-
const r = await lintTicket(paths.ticketDir);
|
|
412
|
-
if (r.ok) {
|
|
413
|
-
console.log("[xera:lint] ok");
|
|
414
|
-
return 0;
|
|
786
|
+
// src/artifact/paths.ts
|
|
787
|
+
import { join as join6 } from "path";
|
|
788
|
+
var TICKET_RE = /^[A-Z][A-Z0-9_]*-\d+$|^SAMPLE-\d+$/;
|
|
789
|
+
function resolveArtifactPaths(repoRoot, ticket) {
|
|
790
|
+
if (!TICKET_RE.test(ticket)) {
|
|
791
|
+
throw new Error(`Invalid ticket key: "${ticket}" (expected e.g. JIRA-123 or SAMPLE-001)`);
|
|
415
792
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
793
|
+
const ticketDir = join6(repoRoot, ".xera", ticket);
|
|
794
|
+
return {
|
|
795
|
+
ticketDir,
|
|
796
|
+
storyPath: join6(ticketDir, "story.md"),
|
|
797
|
+
featurePath: join6(ticketDir, "test.feature"),
|
|
798
|
+
specPath: join6(ticketDir, "spec.ts"),
|
|
799
|
+
pageObjectsDir: join6(ticketDir, "page-objects"),
|
|
800
|
+
runsDir: join6(ticketDir, "runs"),
|
|
801
|
+
metaPath: join6(ticketDir, "meta.json"),
|
|
802
|
+
statusPath: join6(ticketDir, "status.json"),
|
|
803
|
+
logPath: join6(ticketDir, "xera.log"),
|
|
804
|
+
lockPath: join6(ticketDir, ".lock"),
|
|
805
|
+
authDir: join6(repoRoot, ".xera", ".auth"),
|
|
806
|
+
runPath: (runId) => {
|
|
807
|
+
const runDir = join6(ticketDir, "runs", runId);
|
|
808
|
+
return {
|
|
809
|
+
runDir,
|
|
810
|
+
reportJsonPath: join6(runDir, "report.json"),
|
|
811
|
+
tracePath: join6(runDir, "trace.zip"),
|
|
812
|
+
normalizedPath: join6(runDir, "normalized.json"),
|
|
813
|
+
screenshotsDir: join6(runDir, "screenshots"),
|
|
814
|
+
videoDir: join6(runDir, "videos")
|
|
815
|
+
};
|
|
816
|
+
}
|
|
434
817
|
};
|
|
435
|
-
try {
|
|
436
|
-
writeFileSync4(path, JSON.stringify(data), { flag: "wx" });
|
|
437
|
-
return true;
|
|
438
|
-
} catch {
|
|
439
|
-
return false;
|
|
440
|
-
}
|
|
441
818
|
}
|
|
442
|
-
function
|
|
443
|
-
|
|
444
|
-
unlinkSync(path);
|
|
819
|
+
function generateRunId2(now = new Date) {
|
|
820
|
+
return now.toISOString().replace(/[:.]/g, "-").replace("Z", "");
|
|
445
821
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
822
|
+
|
|
823
|
+
// src/auth/refresh.ts
|
|
824
|
+
var RE = /^(\d+)([hms])$/;
|
|
825
|
+
function parseDuration(d) {
|
|
826
|
+
const m = RE.exec(d);
|
|
827
|
+
if (!m)
|
|
828
|
+
throw new Error(`Bad duration "${d}" \u2014 expected e.g. "8h", "30m", "45s"`);
|
|
829
|
+
const n = Number(m[1]);
|
|
830
|
+
const unit = m[2];
|
|
831
|
+
if (unit === "h")
|
|
832
|
+
return n * 3600 * 1000;
|
|
833
|
+
if (unit === "m")
|
|
834
|
+
return n * 60 * 1000;
|
|
835
|
+
return n * 1000;
|
|
450
836
|
}
|
|
451
|
-
function
|
|
452
|
-
|
|
453
|
-
if (!lock)
|
|
837
|
+
function needsRefresh(entry, policy, now = new Date) {
|
|
838
|
+
if (!entry)
|
|
454
839
|
return true;
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
process.kill(lock.pid, 0);
|
|
460
|
-
return false;
|
|
461
|
-
} catch {
|
|
840
|
+
const ttlMs = parseDuration(policy.ttl);
|
|
841
|
+
const bufMs = parseDuration(policy.refreshBuffer);
|
|
842
|
+
const createdAt = new Date(entry.created_at).getTime();
|
|
843
|
+
if (now.getTime() - createdAt > ttlMs)
|
|
462
844
|
return true;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
845
|
+
const expiresAt = new Date(entry.expires_at).getTime();
|
|
846
|
+
if (expiresAt - now.getTime() < bufMs)
|
|
847
|
+
return true;
|
|
848
|
+
return false;
|
|
467
849
|
}
|
|
468
850
|
|
|
469
|
-
// src/
|
|
470
|
-
import {
|
|
471
|
-
import {
|
|
472
|
-
|
|
473
|
-
class NdjsonLogger {
|
|
474
|
-
path;
|
|
475
|
-
constructor(path) {
|
|
476
|
-
this.path = path;
|
|
477
|
-
mkdirSync5(dirname4(path), { recursive: true });
|
|
478
|
-
}
|
|
479
|
-
log(payload) {
|
|
480
|
-
const entry = { ts: new Date().toISOString(), ...payload };
|
|
481
|
-
appendFileSync(this.path, `${JSON.stringify(entry)}
|
|
482
|
-
`);
|
|
483
|
-
}
|
|
484
|
-
static readAll(path) {
|
|
485
|
-
if (!existsSync7(path))
|
|
486
|
-
return [];
|
|
487
|
-
const txt = readFileSync6(path, "utf8").trim();
|
|
488
|
-
if (!txt)
|
|
489
|
-
return [];
|
|
490
|
-
return txt.split(`
|
|
491
|
-
`).map((line) => JSON.parse(line));
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// src/auth/state.ts
|
|
496
|
-
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
497
|
-
import { join as join4 } from "path";
|
|
498
|
-
import { z as z3 } from "zod";
|
|
851
|
+
// src/auth/state.ts
|
|
852
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
853
|
+
import { join as join7 } from "path";
|
|
854
|
+
import { z as z2 } from "zod";
|
|
499
855
|
|
|
500
856
|
// src/auth/encrypt.ts
|
|
501
|
-
import {
|
|
857
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
502
858
|
var ALGO = "aes-256-gcm";
|
|
503
859
|
var KEY_LEN = 32;
|
|
504
860
|
var IV_LEN = 12;
|
|
@@ -543,7 +899,7 @@ var AUTH_KEY_ENV = "XERA_AUTH_KEY";
|
|
|
543
899
|
function resolveAuthKey() {
|
|
544
900
|
const key = process.env[AUTH_KEY_ENV];
|
|
545
901
|
if (!key) {
|
|
546
|
-
throw new Error(`${AUTH_KEY_ENV} not set. It is auto-generated by \`xera init\` and saved to .env.
|
|
902
|
+
throw new Error(`${AUTH_KEY_ENV} not set. It is auto-generated by \`xera init\` and saved to .env. If you deleted .env, regenerate it by running \`xera init --update\` \u2014 note that any cached auth state will be invalidated.`);
|
|
547
903
|
}
|
|
548
904
|
if (!/^[0-9a-f]{64}$/i.test(key)) {
|
|
549
905
|
throw new Error(`${AUTH_KEY_ENV} must be a 64-character hex string (32 bytes).`);
|
|
@@ -552,63 +908,134 @@ function resolveAuthKey() {
|
|
|
552
908
|
}
|
|
553
909
|
|
|
554
910
|
// src/auth/state.ts
|
|
555
|
-
var AuthStateEntrySchema =
|
|
556
|
-
role:
|
|
557
|
-
strategy:
|
|
558
|
-
created_at:
|
|
559
|
-
expires_at:
|
|
560
|
-
payload:
|
|
911
|
+
var AuthStateEntrySchema = z2.object({
|
|
912
|
+
role: z2.string(),
|
|
913
|
+
strategy: z2.enum(["storageState", "apiToken"]),
|
|
914
|
+
created_at: z2.string(),
|
|
915
|
+
expires_at: z2.string(),
|
|
916
|
+
payload: z2.record(z2.string(), z2.unknown())
|
|
561
917
|
});
|
|
562
918
|
function pathFor(authDir, role) {
|
|
563
|
-
return
|
|
919
|
+
return join7(authDir, `${role}.json`);
|
|
564
920
|
}
|
|
565
921
|
function writeAuthState(authDir, entry) {
|
|
566
|
-
|
|
922
|
+
mkdirSync3(authDir, { recursive: true });
|
|
567
923
|
const ct = encrypt(JSON.stringify(entry), resolveAuthKey());
|
|
568
924
|
writeFileSync5(pathFor(authDir, entry.role), ct);
|
|
569
925
|
}
|
|
570
926
|
function readAuthState(authDir, role) {
|
|
571
927
|
const p = pathFor(authDir, role);
|
|
572
|
-
if (!
|
|
928
|
+
if (!existsSync7(p))
|
|
573
929
|
return null;
|
|
574
930
|
const txt = readFileSync7(p, "utf8");
|
|
575
931
|
const plain = decrypt(txt, resolveAuthKey());
|
|
576
932
|
return AuthStateEntrySchema.parse(JSON.parse(plain));
|
|
577
933
|
}
|
|
578
934
|
|
|
579
|
-
// src/
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
935
|
+
// src/config/load.ts
|
|
936
|
+
import { existsSync as existsSync8 } from "fs";
|
|
937
|
+
import { join as join8 } from "path";
|
|
938
|
+
import { pathToFileURL } from "url";
|
|
939
|
+
|
|
940
|
+
// src/config/schema.ts
|
|
941
|
+
import { z as z3 } from "zod";
|
|
942
|
+
var AuthRoleSchema = z3.object({
|
|
943
|
+
envEmail: z3.string().min(1),
|
|
944
|
+
envPassword: z3.string().min(1)
|
|
945
|
+
});
|
|
946
|
+
var AuthSchema = z3.object({
|
|
947
|
+
strategy: z3.enum(["storageState", "apiToken", "none"]).default("none"),
|
|
948
|
+
ttl: z3.string().default("8h"),
|
|
949
|
+
refreshBuffer: z3.string().default("30m"),
|
|
950
|
+
setupScript: z3.string().optional(),
|
|
951
|
+
roles: z3.record(z3.string(), AuthRoleSchema).default({})
|
|
952
|
+
});
|
|
953
|
+
var WebSchema = z3.object({
|
|
954
|
+
baseUrl: z3.record(z3.string(), z3.string().url()).refine((m) => Object.keys(m).length > 0, {
|
|
955
|
+
message: "baseUrl must have at least one environment"
|
|
956
|
+
}),
|
|
957
|
+
defaultEnv: z3.string(),
|
|
958
|
+
auth: AuthSchema.default({}),
|
|
959
|
+
testData: z3.object({
|
|
960
|
+
users: z3.record(z3.string(), z3.object({ fromAuth: z3.string() })).default({})
|
|
961
|
+
}).default({ users: {} })
|
|
962
|
+
}).refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
|
|
963
|
+
message: "defaultEnv must exist in baseUrl map",
|
|
964
|
+
path: ["defaultEnv"]
|
|
965
|
+
});
|
|
966
|
+
var JiraSchema = z3.object({
|
|
967
|
+
baseUrl: z3.string().url(),
|
|
968
|
+
projectKeys: z3.array(z3.string().min(1)).min(1),
|
|
969
|
+
fields: z3.object({
|
|
970
|
+
story: z3.string().min(1),
|
|
971
|
+
acceptanceCriteria: z3.string().optional(),
|
|
972
|
+
attachments: z3.string().default("attachment")
|
|
973
|
+
})
|
|
974
|
+
});
|
|
975
|
+
var AISchema = z3.object({
|
|
976
|
+
livePageSnapshot: z3.boolean().default(true),
|
|
977
|
+
confidenceThreshold: z3.enum(["low", "medium", "high"]).default("medium"),
|
|
978
|
+
maxRetries: z3.object({
|
|
979
|
+
typecheck: z3.number().int().min(0).max(5).default(2),
|
|
980
|
+
lint: z3.number().int().min(0).max(5).default(2),
|
|
981
|
+
validateFeature: z3.number().int().min(0).max(5).default(2)
|
|
982
|
+
}).default({})
|
|
983
|
+
}).default({});
|
|
984
|
+
var ReportingSchema = z3.object({
|
|
985
|
+
language: z3.enum(["en", "vi"]).default("en"),
|
|
986
|
+
postToJira: z3.boolean().default(true),
|
|
987
|
+
transition: z3.object({
|
|
988
|
+
onPass: z3.string().nullable().default(null),
|
|
989
|
+
onFail: z3.string().nullable().default(null)
|
|
990
|
+
}).default({}),
|
|
991
|
+
artifactLinks: z3.enum(["git", "local"]).default("git")
|
|
992
|
+
}).default({});
|
|
993
|
+
var XeraConfigSchema = z3.object({
|
|
994
|
+
jira: JiraSchema,
|
|
995
|
+
web: WebSchema,
|
|
996
|
+
ai: AISchema,
|
|
997
|
+
reporting: ReportingSchema,
|
|
998
|
+
adapters: z3.array(z3.string().min(1)).min(1).default(["web"])
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// src/config/load.ts
|
|
1002
|
+
async function loadConfig(cwd) {
|
|
1003
|
+
const path = join8(cwd, "xera.config.ts");
|
|
1004
|
+
if (!existsSync8(path)) {
|
|
1005
|
+
throw new Error(`xera.config.ts not found in ${cwd}`);
|
|
1006
|
+
}
|
|
1007
|
+
const mod = await import(pathToFileURL(path).href);
|
|
1008
|
+
const raw = mod.default ?? mod;
|
|
1009
|
+
return XeraConfigSchema.parse(raw);
|
|
592
1010
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1011
|
+
|
|
1012
|
+
// src/logging/ndjson-logger.ts
|
|
1013
|
+
import { appendFileSync, existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync8 } from "fs";
|
|
1014
|
+
import { dirname as dirname2 } from "path";
|
|
1015
|
+
|
|
1016
|
+
class NdjsonLogger {
|
|
1017
|
+
path;
|
|
1018
|
+
constructor(path) {
|
|
1019
|
+
this.path = path;
|
|
1020
|
+
mkdirSync4(dirname2(path), { recursive: true });
|
|
1021
|
+
}
|
|
1022
|
+
log(payload) {
|
|
1023
|
+
const entry = { ts: new Date().toISOString(), ...payload };
|
|
1024
|
+
appendFileSync(this.path, `${JSON.stringify(entry)}
|
|
1025
|
+
`);
|
|
1026
|
+
}
|
|
1027
|
+
static readAll(path) {
|
|
1028
|
+
if (!existsSync9(path))
|
|
1029
|
+
return [];
|
|
1030
|
+
const txt = readFileSync8(path, "utf8").trim();
|
|
1031
|
+
if (!txt)
|
|
1032
|
+
return [];
|
|
1033
|
+
return txt.split(`
|
|
1034
|
+
`).map((line) => JSON.parse(line));
|
|
1035
|
+
}
|
|
605
1036
|
}
|
|
606
1037
|
|
|
607
1038
|
// src/bin-internal/exec.ts
|
|
608
|
-
import { stagePlaywrightState, runAuthSetup, runPlaywright } from "@xera-ai/web";
|
|
609
|
-
import { chromium } from "@playwright/test";
|
|
610
|
-
import { mkdirSync as mkdirSync7, existsSync as existsSync9 } from "fs";
|
|
611
|
-
import { join as join5 } from "path";
|
|
612
1039
|
async function execCmd(argv) {
|
|
613
1040
|
const ticket = argv[0];
|
|
614
1041
|
if (!ticket) {
|
|
@@ -618,7 +1045,7 @@ async function execCmd(argv) {
|
|
|
618
1045
|
const cwd = process.cwd();
|
|
619
1046
|
const config = await loadConfig(cwd);
|
|
620
1047
|
const paths = resolveArtifactPaths(cwd, ticket);
|
|
621
|
-
const runId =
|
|
1048
|
+
const runId = generateRunId2();
|
|
622
1049
|
const log = new NdjsonLogger(paths.logPath);
|
|
623
1050
|
if (!acquireLock(paths.lockPath, runId)) {
|
|
624
1051
|
if (isLockStale(paths.lockPath)) {
|
|
@@ -638,7 +1065,10 @@ async function execCmd(argv) {
|
|
|
638
1065
|
try {
|
|
639
1066
|
for (const [roleName, roleCreds] of Object.entries(config.web.auth.roles)) {
|
|
640
1067
|
const entry = readAuthState(paths.authDir, roleName);
|
|
641
|
-
if (needsRefresh(entry, {
|
|
1068
|
+
if (needsRefresh(entry, {
|
|
1069
|
+
ttl: config.web.auth.ttl,
|
|
1070
|
+
refreshBuffer: config.web.auth.refreshBuffer
|
|
1071
|
+
})) {
|
|
642
1072
|
const email = process.env[roleCreds.envEmail];
|
|
643
1073
|
const password = process.env[roleCreds.envPassword];
|
|
644
1074
|
if (!email || !password) {
|
|
@@ -648,7 +1078,7 @@ async function execCmd(argv) {
|
|
|
648
1078
|
await runAuthSetup({
|
|
649
1079
|
role: roleName,
|
|
650
1080
|
creds: { email, password },
|
|
651
|
-
setupScriptPath:
|
|
1081
|
+
setupScriptPath: join9(cwd, config.web.auth.setupScript),
|
|
652
1082
|
authDir: paths.authDir,
|
|
653
1083
|
browser
|
|
654
1084
|
});
|
|
@@ -666,16 +1096,16 @@ async function execCmd(argv) {
|
|
|
666
1096
|
}
|
|
667
1097
|
}
|
|
668
1098
|
}
|
|
669
|
-
const cfgPath =
|
|
670
|
-
if (!
|
|
1099
|
+
const cfgPath = join9(cwd, "playwright.config.ts");
|
|
1100
|
+
if (!existsSync10(cfgPath)) {
|
|
671
1101
|
console.error(`[xera:exec] missing ${cfgPath}. Run \`xera init\` to scaffold it, then re-run.`);
|
|
672
1102
|
return 1;
|
|
673
1103
|
}
|
|
674
1104
|
const runDir = paths.runPath(runId).runDir;
|
|
675
|
-
|
|
1105
|
+
mkdirSync5(runDir, { recursive: true });
|
|
676
1106
|
const envName = process.env.XERA_ENV ?? config.web.defaultEnv;
|
|
677
1107
|
const baseURL = config.web.baseUrl[envName] ?? config.web.baseUrl[config.web.defaultEnv];
|
|
678
|
-
const reportJsonPath =
|
|
1108
|
+
const reportJsonPath = join9(runDir, "report.json");
|
|
679
1109
|
log.log({ step: "exec.start", runId, env: envName, baseURL });
|
|
680
1110
|
const r = await runPlaywright({
|
|
681
1111
|
specPath: paths.specPath,
|
|
@@ -695,10 +1125,840 @@ async function execCmd(argv) {
|
|
|
695
1125
|
}
|
|
696
1126
|
}
|
|
697
1127
|
|
|
1128
|
+
// src/bin-internal/fetch.ts
|
|
1129
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
1130
|
+
import { dirname as dirname4 } from "path";
|
|
1131
|
+
|
|
1132
|
+
// src/artifact/hash.ts
|
|
1133
|
+
import { createHash } from "crypto";
|
|
1134
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
1135
|
+
function hashString(s) {
|
|
1136
|
+
return `sha256:${createHash("sha256").update(s).digest("hex")}`;
|
|
1137
|
+
}
|
|
1138
|
+
function hashFile(path) {
|
|
1139
|
+
return hashString(readFileSync9(path, "utf8"));
|
|
1140
|
+
}
|
|
1141
|
+
function hashFileIfExists(path) {
|
|
1142
|
+
if (!existsSync11(path))
|
|
1143
|
+
return null;
|
|
1144
|
+
return hashFile(path);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// src/artifact/meta.ts
|
|
1148
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
|
|
1149
|
+
import { dirname as dirname3 } from "path";
|
|
1150
|
+
import { z as z4 } from "zod";
|
|
1151
|
+
var MetaJsonSchema = z4.object({
|
|
1152
|
+
ticket: z4.string(),
|
|
1153
|
+
adapter: z4.string(),
|
|
1154
|
+
xera_version: z4.string(),
|
|
1155
|
+
prompts_version: z4.string(),
|
|
1156
|
+
fetched_at: z4.string().optional(),
|
|
1157
|
+
story_hash: z4.string().optional(),
|
|
1158
|
+
feature_generated_at: z4.string().optional(),
|
|
1159
|
+
feature_generated_from_story_hash: z4.string().optional(),
|
|
1160
|
+
feature_hash: z4.string().optional(),
|
|
1161
|
+
script_generated_at: z4.string().optional(),
|
|
1162
|
+
script_generated_from_feature_hash: z4.string().optional(),
|
|
1163
|
+
script_warnings: z4.array(z4.string()).optional()
|
|
1164
|
+
});
|
|
1165
|
+
function readMeta(path) {
|
|
1166
|
+
if (!existsSync12(path))
|
|
1167
|
+
return null;
|
|
1168
|
+
return MetaJsonSchema.parse(JSON.parse(readFileSync10(path, "utf8")));
|
|
1169
|
+
}
|
|
1170
|
+
function writeMeta(path, meta) {
|
|
1171
|
+
mkdirSync6(dirname3(path), { recursive: true });
|
|
1172
|
+
writeFileSync6(path, JSON.stringify(meta, null, 2));
|
|
1173
|
+
}
|
|
1174
|
+
function updateMeta(path, patch) {
|
|
1175
|
+
const existing = readMeta(path);
|
|
1176
|
+
if (!existing) {
|
|
1177
|
+
throw new Error(`meta.json not found at ${path}; cannot update`);
|
|
1178
|
+
}
|
|
1179
|
+
const next = { ...existing, ...patch };
|
|
1180
|
+
writeMeta(path, next);
|
|
1181
|
+
return next;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// src/jira/mcp-backend.ts
|
|
1185
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
|
|
1186
|
+
import { tmpdir } from "os";
|
|
1187
|
+
import { join as join10 } from "path";
|
|
1188
|
+
var MCP_ENV = "XERA_MCP_JIRA";
|
|
1189
|
+
async function createMcpBackend(_baseUrl) {
|
|
1190
|
+
if (process.env[MCP_ENV] !== "1")
|
|
1191
|
+
return null;
|
|
1192
|
+
const tmpDir = join10(tmpdir(), "xera-mcp");
|
|
1193
|
+
mkdirSync7(tmpDir, { recursive: true });
|
|
1194
|
+
return {
|
|
1195
|
+
backend: "mcp",
|
|
1196
|
+
async fetchTicket(key, _fields) {
|
|
1197
|
+
const cachePath = join10(tmpDir, `${key}.json`);
|
|
1198
|
+
if (!existsSync13(cachePath)) {
|
|
1199
|
+
throw new Error(`MCP-mode fetch requires the skill to first call mcp__atlassian__getJiraIssue and write ${cachePath}. ` + `If you are running this directly, unset ${MCP_ENV} to use REST.`);
|
|
1200
|
+
}
|
|
1201
|
+
const parsed = JSON.parse(readFileSync11(cachePath, "utf8"));
|
|
1202
|
+
return parsed;
|
|
1203
|
+
},
|
|
1204
|
+
async postComment(key, body) {
|
|
1205
|
+
const outPath = join10(tmpDir, `${key}.comment.json`);
|
|
1206
|
+
writeFileSync7(outPath, JSON.stringify({ key, body }));
|
|
1207
|
+
return { id: "mcp-pending" };
|
|
1208
|
+
},
|
|
1209
|
+
async transitionStatus(key, statusName) {
|
|
1210
|
+
const outPath = join10(tmpDir, `${key}.transition.json`);
|
|
1211
|
+
writeFileSync7(outPath, JSON.stringify({ key, statusName }));
|
|
1212
|
+
},
|
|
1213
|
+
async listFields(_sampleKey) {
|
|
1214
|
+
throw new Error("listFields is REST-only; init flow uses REST for field discovery.");
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/jira/rest-backend.ts
|
|
1220
|
+
function createRestBackend(baseUrl, creds) {
|
|
1221
|
+
const authHeader = `Basic ${Buffer.from(`${creds.email}:${creds.apiToken}`).toString("base64")}`;
|
|
1222
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
1223
|
+
async function req(path, init) {
|
|
1224
|
+
const r = await fetch(`${base}${path}`, {
|
|
1225
|
+
...init,
|
|
1226
|
+
headers: {
|
|
1227
|
+
Authorization: authHeader,
|
|
1228
|
+
Accept: "application/json",
|
|
1229
|
+
"Content-Type": "application/json",
|
|
1230
|
+
...init?.headers ?? {}
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
if (!r.ok && r.status !== 201) {
|
|
1234
|
+
throw new Error(`Jira REST ${init?.method ?? "GET"} ${path} failed: ${r.status} ${await r.text()}`);
|
|
1235
|
+
}
|
|
1236
|
+
return r;
|
|
1237
|
+
}
|
|
1238
|
+
return {
|
|
1239
|
+
backend: "rest",
|
|
1240
|
+
async fetchTicket(key, fields) {
|
|
1241
|
+
const want = ["summary", fields.story];
|
|
1242
|
+
if (fields.acceptanceCriteria)
|
|
1243
|
+
want.push(fields.acceptanceCriteria);
|
|
1244
|
+
want.push("attachment");
|
|
1245
|
+
const r = await req(`/rest/api/3/issue/${encodeURIComponent(key)}?fields=${want.join(",")}`);
|
|
1246
|
+
const json = await r.json();
|
|
1247
|
+
const f = json.fields;
|
|
1248
|
+
const attachments = Array.isArray(f.attachment) ? f.attachment.map((a) => ({
|
|
1249
|
+
filename: a.filename,
|
|
1250
|
+
url: a.content
|
|
1251
|
+
})) : [];
|
|
1252
|
+
const ticket = {
|
|
1253
|
+
key: json.key,
|
|
1254
|
+
summary: String(f.summary ?? ""),
|
|
1255
|
+
story: String(f[fields.story] ?? ""),
|
|
1256
|
+
attachments,
|
|
1257
|
+
raw: f
|
|
1258
|
+
};
|
|
1259
|
+
if (fields.acceptanceCriteria) {
|
|
1260
|
+
ticket.acceptanceCriteria = String(f[fields.acceptanceCriteria] ?? "");
|
|
1261
|
+
}
|
|
1262
|
+
return ticket;
|
|
1263
|
+
},
|
|
1264
|
+
async postComment(key, body) {
|
|
1265
|
+
const r = await req(`/rest/api/3/issue/${encodeURIComponent(key)}/comment`, {
|
|
1266
|
+
method: "POST",
|
|
1267
|
+
body: JSON.stringify({
|
|
1268
|
+
body: {
|
|
1269
|
+
type: "doc",
|
|
1270
|
+
version: 1,
|
|
1271
|
+
content: [{ type: "paragraph", content: [{ type: "text", text: body }] }]
|
|
1272
|
+
}
|
|
1273
|
+
})
|
|
1274
|
+
});
|
|
1275
|
+
const json = await r.json();
|
|
1276
|
+
return { id: json.id };
|
|
1277
|
+
},
|
|
1278
|
+
async transitionStatus(key, statusName) {
|
|
1279
|
+
const tr = await req(`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`);
|
|
1280
|
+
const json = await tr.json();
|
|
1281
|
+
const t = json.transitions.find((x) => x.name === statusName);
|
|
1282
|
+
if (!t)
|
|
1283
|
+
throw new Error(`No transition named "${statusName}" available for ${key}`);
|
|
1284
|
+
await req(`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`, {
|
|
1285
|
+
method: "POST",
|
|
1286
|
+
body: JSON.stringify({ transition: { id: t.id } })
|
|
1287
|
+
});
|
|
1288
|
+
},
|
|
1289
|
+
async listFields(sampleKey) {
|
|
1290
|
+
const r = await req(`/rest/api/3/issue/${encodeURIComponent(sampleKey)}?fields=*all`);
|
|
1291
|
+
const json = await r.json();
|
|
1292
|
+
return Object.entries(json.fields).map(([id, value]) => ({
|
|
1293
|
+
id,
|
|
1294
|
+
name: id,
|
|
1295
|
+
hasContent: value !== null && value !== undefined && value !== ""
|
|
1296
|
+
}));
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/jira/client.ts
|
|
1302
|
+
async function createJiraClient(opts) {
|
|
1303
|
+
if (opts.preferMcp !== false) {
|
|
1304
|
+
const mcp = await createMcpBackend(opts.baseUrl);
|
|
1305
|
+
if (mcp)
|
|
1306
|
+
return mcp;
|
|
1307
|
+
}
|
|
1308
|
+
if (!opts.rest) {
|
|
1309
|
+
throw new Error("Atlassian MCP not connected and no REST credentials provided (JIRA_EMAIL + JIRA_API_TOKEN).");
|
|
1310
|
+
}
|
|
1311
|
+
return createRestBackend(opts.baseUrl, opts.rest);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/bin-internal/fetch.ts
|
|
1315
|
+
async function fetchCmd(argv, opts = {}) {
|
|
1316
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1317
|
+
const ticket = argv[0];
|
|
1318
|
+
if (!ticket) {
|
|
1319
|
+
console.error("[xera:fetch] usage: xera-internal fetch <TICKET>");
|
|
1320
|
+
return 1;
|
|
1321
|
+
}
|
|
1322
|
+
const config = await loadConfig(cwd);
|
|
1323
|
+
const paths = resolveArtifactPaths(cwd, ticket);
|
|
1324
|
+
let t;
|
|
1325
|
+
if (process.env.XERA_TEST_JIRA) {
|
|
1326
|
+
t = JSON.parse(process.env.XERA_TEST_JIRA);
|
|
1327
|
+
} else {
|
|
1328
|
+
const client = await createJiraClient({
|
|
1329
|
+
baseUrl: config.jira.baseUrl,
|
|
1330
|
+
preferMcp: true,
|
|
1331
|
+
...process.env.JIRA_EMAIL && process.env.JIRA_API_TOKEN ? { rest: { email: process.env.JIRA_EMAIL, apiToken: process.env.JIRA_API_TOKEN } } : {}
|
|
1332
|
+
});
|
|
1333
|
+
const fieldMap = config.jira.fields.acceptanceCriteria !== undefined ? {
|
|
1334
|
+
story: config.jira.fields.story,
|
|
1335
|
+
acceptanceCriteria: config.jira.fields.acceptanceCriteria
|
|
1336
|
+
} : { story: config.jira.fields.story };
|
|
1337
|
+
t = await client.fetchTicket(ticket, fieldMap);
|
|
1338
|
+
}
|
|
1339
|
+
const story = renderStory(t);
|
|
1340
|
+
mkdirSync8(dirname4(paths.storyPath), { recursive: true });
|
|
1341
|
+
writeFileSync8(paths.storyPath, story);
|
|
1342
|
+
const existing = readMeta(paths.metaPath);
|
|
1343
|
+
writeMeta(paths.metaPath, {
|
|
1344
|
+
ticket,
|
|
1345
|
+
adapter: "web",
|
|
1346
|
+
xera_version: "0.1.0",
|
|
1347
|
+
prompts_version: "1.0.0",
|
|
1348
|
+
...existing ?? {},
|
|
1349
|
+
story_hash: hashString(story),
|
|
1350
|
+
fetched_at: new Date().toISOString()
|
|
1351
|
+
});
|
|
1352
|
+
console.log(`[xera:fetch] wrote ${paths.storyPath}`);
|
|
1353
|
+
return 0;
|
|
1354
|
+
}
|
|
1355
|
+
function renderStory(t) {
|
|
1356
|
+
const lines = [];
|
|
1357
|
+
lines.push(`# ${t.key}: ${t.summary}`, "");
|
|
1358
|
+
const story = t.story.trim();
|
|
1359
|
+
if (/^##\s+story\b/i.test(story)) {
|
|
1360
|
+
lines.push(story, "");
|
|
1361
|
+
} else {
|
|
1362
|
+
lines.push("## Story", "", story, "");
|
|
1363
|
+
}
|
|
1364
|
+
if (t.acceptanceCriteria?.trim()) {
|
|
1365
|
+
const ac = t.acceptanceCriteria.trim();
|
|
1366
|
+
if (/^##\s+acceptance\s+criteria\b/i.test(ac)) {
|
|
1367
|
+
lines.push(ac, "");
|
|
1368
|
+
} else {
|
|
1369
|
+
lines.push("## Acceptance Criteria", "", ac, "");
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (t.attachments.length > 0) {
|
|
1373
|
+
lines.push("## Attachments", "", ...t.attachments.map((a) => `- [${a.filename}](${a.url})`), "");
|
|
1374
|
+
}
|
|
1375
|
+
return lines.join(`
|
|
1376
|
+
`);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// src/bin-internal/heal-prepare.ts
|
|
1380
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12, readdirSync as readdirSync3, writeFileSync as writeFileSync9 } from "fs";
|
|
1381
|
+
import { join as join11 } from "path";
|
|
1382
|
+
import { scrubFreeText } from "@xera-ai/web";
|
|
1383
|
+
|
|
1384
|
+
// ../../node_modules/.bun/fflate@0.8.2/node_modules/fflate/esm/index.mjs
|
|
1385
|
+
import { createRequire } from "module";
|
|
1386
|
+
var require2 = createRequire("/");
|
|
1387
|
+
var Worker;
|
|
1388
|
+
try {
|
|
1389
|
+
Worker = require2("worker_threads").Worker;
|
|
1390
|
+
} catch (e) {}
|
|
1391
|
+
var u8 = Uint8Array;
|
|
1392
|
+
var u16 = Uint16Array;
|
|
1393
|
+
var i32 = Int32Array;
|
|
1394
|
+
var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0]);
|
|
1395
|
+
var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 0, 0]);
|
|
1396
|
+
var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
|
|
1397
|
+
var freb = function(eb, start) {
|
|
1398
|
+
var b = new u16(31);
|
|
1399
|
+
for (var i = 0;i < 31; ++i) {
|
|
1400
|
+
b[i] = start += 1 << eb[i - 1];
|
|
1401
|
+
}
|
|
1402
|
+
var r = new i32(b[30]);
|
|
1403
|
+
for (var i = 1;i < 30; ++i) {
|
|
1404
|
+
for (var j = b[i];j < b[i + 1]; ++j) {
|
|
1405
|
+
r[j] = j - b[i] << 5 | i;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return { b, r };
|
|
1409
|
+
};
|
|
1410
|
+
var _a = freb(fleb, 2);
|
|
1411
|
+
var fl = _a.b;
|
|
1412
|
+
var revfl = _a.r;
|
|
1413
|
+
fl[28] = 258, revfl[258] = 28;
|
|
1414
|
+
var _b = freb(fdeb, 0);
|
|
1415
|
+
var fd = _b.b;
|
|
1416
|
+
var revfd = _b.r;
|
|
1417
|
+
var rev = new u16(32768);
|
|
1418
|
+
for (i = 0;i < 32768; ++i) {
|
|
1419
|
+
x = (i & 43690) >> 1 | (i & 21845) << 1;
|
|
1420
|
+
x = (x & 52428) >> 2 | (x & 13107) << 2;
|
|
1421
|
+
x = (x & 61680) >> 4 | (x & 3855) << 4;
|
|
1422
|
+
rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
|
|
1423
|
+
}
|
|
1424
|
+
var x;
|
|
1425
|
+
var i;
|
|
1426
|
+
var hMap = function(cd, mb, r) {
|
|
1427
|
+
var s = cd.length;
|
|
1428
|
+
var i2 = 0;
|
|
1429
|
+
var l = new u16(mb);
|
|
1430
|
+
for (;i2 < s; ++i2) {
|
|
1431
|
+
if (cd[i2])
|
|
1432
|
+
++l[cd[i2] - 1];
|
|
1433
|
+
}
|
|
1434
|
+
var le = new u16(mb);
|
|
1435
|
+
for (i2 = 1;i2 < mb; ++i2) {
|
|
1436
|
+
le[i2] = le[i2 - 1] + l[i2 - 1] << 1;
|
|
1437
|
+
}
|
|
1438
|
+
var co;
|
|
1439
|
+
if (r) {
|
|
1440
|
+
co = new u16(1 << mb);
|
|
1441
|
+
var rvb = 15 - mb;
|
|
1442
|
+
for (i2 = 0;i2 < s; ++i2) {
|
|
1443
|
+
if (cd[i2]) {
|
|
1444
|
+
var sv = i2 << 4 | cd[i2];
|
|
1445
|
+
var r_1 = mb - cd[i2];
|
|
1446
|
+
var v = le[cd[i2] - 1]++ << r_1;
|
|
1447
|
+
for (var m = v | (1 << r_1) - 1;v <= m; ++v) {
|
|
1448
|
+
co[rev[v] >> rvb] = sv;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
} else {
|
|
1453
|
+
co = new u16(s);
|
|
1454
|
+
for (i2 = 0;i2 < s; ++i2) {
|
|
1455
|
+
if (cd[i2]) {
|
|
1456
|
+
co[i2] = rev[le[cd[i2] - 1]++] >> 15 - cd[i2];
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return co;
|
|
1461
|
+
};
|
|
1462
|
+
var flt = new u8(288);
|
|
1463
|
+
for (i = 0;i < 144; ++i)
|
|
1464
|
+
flt[i] = 8;
|
|
1465
|
+
var i;
|
|
1466
|
+
for (i = 144;i < 256; ++i)
|
|
1467
|
+
flt[i] = 9;
|
|
1468
|
+
var i;
|
|
1469
|
+
for (i = 256;i < 280; ++i)
|
|
1470
|
+
flt[i] = 7;
|
|
1471
|
+
var i;
|
|
1472
|
+
for (i = 280;i < 288; ++i)
|
|
1473
|
+
flt[i] = 8;
|
|
1474
|
+
var i;
|
|
1475
|
+
var fdt = new u8(32);
|
|
1476
|
+
for (i = 0;i < 32; ++i)
|
|
1477
|
+
fdt[i] = 5;
|
|
1478
|
+
var i;
|
|
1479
|
+
var flrm = /* @__PURE__ */ hMap(flt, 9, 1);
|
|
1480
|
+
var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1);
|
|
1481
|
+
var max = function(a) {
|
|
1482
|
+
var m = a[0];
|
|
1483
|
+
for (var i2 = 1;i2 < a.length; ++i2) {
|
|
1484
|
+
if (a[i2] > m)
|
|
1485
|
+
m = a[i2];
|
|
1486
|
+
}
|
|
1487
|
+
return m;
|
|
1488
|
+
};
|
|
1489
|
+
var bits = function(d, p, m) {
|
|
1490
|
+
var o = p / 8 | 0;
|
|
1491
|
+
return (d[o] | d[o + 1] << 8) >> (p & 7) & m;
|
|
1492
|
+
};
|
|
1493
|
+
var bits16 = function(d, p) {
|
|
1494
|
+
var o = p / 8 | 0;
|
|
1495
|
+
return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7);
|
|
1496
|
+
};
|
|
1497
|
+
var shft = function(p) {
|
|
1498
|
+
return (p + 7) / 8 | 0;
|
|
1499
|
+
};
|
|
1500
|
+
var slc = function(v, s, e) {
|
|
1501
|
+
if (s == null || s < 0)
|
|
1502
|
+
s = 0;
|
|
1503
|
+
if (e == null || e > v.length)
|
|
1504
|
+
e = v.length;
|
|
1505
|
+
return new u8(v.subarray(s, e));
|
|
1506
|
+
};
|
|
1507
|
+
var ec = [
|
|
1508
|
+
"unexpected EOF",
|
|
1509
|
+
"invalid block type",
|
|
1510
|
+
"invalid length/literal",
|
|
1511
|
+
"invalid distance",
|
|
1512
|
+
"stream finished",
|
|
1513
|
+
"no stream handler",
|
|
1514
|
+
,
|
|
1515
|
+
"no callback",
|
|
1516
|
+
"invalid UTF-8 data",
|
|
1517
|
+
"extra field too long",
|
|
1518
|
+
"date not in range 1980-2099",
|
|
1519
|
+
"filename too long",
|
|
1520
|
+
"stream finishing",
|
|
1521
|
+
"invalid zip data"
|
|
1522
|
+
];
|
|
1523
|
+
var err = function(ind, msg, nt) {
|
|
1524
|
+
var e = new Error(msg || ec[ind]);
|
|
1525
|
+
e.code = ind;
|
|
1526
|
+
if (Error.captureStackTrace)
|
|
1527
|
+
Error.captureStackTrace(e, err);
|
|
1528
|
+
if (!nt)
|
|
1529
|
+
throw e;
|
|
1530
|
+
return e;
|
|
1531
|
+
};
|
|
1532
|
+
var inflt = function(dat, st, buf, dict) {
|
|
1533
|
+
var sl = dat.length, dl = dict ? dict.length : 0;
|
|
1534
|
+
if (!sl || st.f && !st.l)
|
|
1535
|
+
return buf || new u8(0);
|
|
1536
|
+
var noBuf = !buf;
|
|
1537
|
+
var resize = noBuf || st.i != 2;
|
|
1538
|
+
var noSt = st.i;
|
|
1539
|
+
if (noBuf)
|
|
1540
|
+
buf = new u8(sl * 3);
|
|
1541
|
+
var cbuf = function(l2) {
|
|
1542
|
+
var bl = buf.length;
|
|
1543
|
+
if (l2 > bl) {
|
|
1544
|
+
var nbuf = new u8(Math.max(bl * 2, l2));
|
|
1545
|
+
nbuf.set(buf);
|
|
1546
|
+
buf = nbuf;
|
|
1547
|
+
}
|
|
1548
|
+
};
|
|
1549
|
+
var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
|
|
1550
|
+
var tbts = sl * 8;
|
|
1551
|
+
do {
|
|
1552
|
+
if (!lm) {
|
|
1553
|
+
final = bits(dat, pos, 1);
|
|
1554
|
+
var type = bits(dat, pos + 1, 3);
|
|
1555
|
+
pos += 3;
|
|
1556
|
+
if (!type) {
|
|
1557
|
+
var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
|
|
1558
|
+
if (t > sl) {
|
|
1559
|
+
if (noSt)
|
|
1560
|
+
err(0);
|
|
1561
|
+
break;
|
|
1562
|
+
}
|
|
1563
|
+
if (resize)
|
|
1564
|
+
cbuf(bt + l);
|
|
1565
|
+
buf.set(dat.subarray(s, t), bt);
|
|
1566
|
+
st.b = bt += l, st.p = pos = t * 8, st.f = final;
|
|
1567
|
+
continue;
|
|
1568
|
+
} else if (type == 1)
|
|
1569
|
+
lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
|
|
1570
|
+
else if (type == 2) {
|
|
1571
|
+
var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
|
|
1572
|
+
var tl = hLit + bits(dat, pos + 5, 31) + 1;
|
|
1573
|
+
pos += 14;
|
|
1574
|
+
var ldt = new u8(tl);
|
|
1575
|
+
var clt = new u8(19);
|
|
1576
|
+
for (var i2 = 0;i2 < hcLen; ++i2) {
|
|
1577
|
+
clt[clim[i2]] = bits(dat, pos + i2 * 3, 7);
|
|
1578
|
+
}
|
|
1579
|
+
pos += hcLen * 3;
|
|
1580
|
+
var clb = max(clt), clbmsk = (1 << clb) - 1;
|
|
1581
|
+
var clm = hMap(clt, clb, 1);
|
|
1582
|
+
for (var i2 = 0;i2 < tl; ) {
|
|
1583
|
+
var r = clm[bits(dat, pos, clbmsk)];
|
|
1584
|
+
pos += r & 15;
|
|
1585
|
+
var s = r >> 4;
|
|
1586
|
+
if (s < 16) {
|
|
1587
|
+
ldt[i2++] = s;
|
|
1588
|
+
} else {
|
|
1589
|
+
var c = 0, n = 0;
|
|
1590
|
+
if (s == 16)
|
|
1591
|
+
n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i2 - 1];
|
|
1592
|
+
else if (s == 17)
|
|
1593
|
+
n = 3 + bits(dat, pos, 7), pos += 3;
|
|
1594
|
+
else if (s == 18)
|
|
1595
|
+
n = 11 + bits(dat, pos, 127), pos += 7;
|
|
1596
|
+
while (n--)
|
|
1597
|
+
ldt[i2++] = c;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
|
|
1601
|
+
lbt = max(lt);
|
|
1602
|
+
dbt = max(dt);
|
|
1603
|
+
lm = hMap(lt, lbt, 1);
|
|
1604
|
+
dm = hMap(dt, dbt, 1);
|
|
1605
|
+
} else
|
|
1606
|
+
err(1);
|
|
1607
|
+
if (pos > tbts) {
|
|
1608
|
+
if (noSt)
|
|
1609
|
+
err(0);
|
|
1610
|
+
break;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (resize)
|
|
1614
|
+
cbuf(bt + 131072);
|
|
1615
|
+
var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
|
|
1616
|
+
var lpos = pos;
|
|
1617
|
+
for (;; lpos = pos) {
|
|
1618
|
+
var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
|
|
1619
|
+
pos += c & 15;
|
|
1620
|
+
if (pos > tbts) {
|
|
1621
|
+
if (noSt)
|
|
1622
|
+
err(0);
|
|
1623
|
+
break;
|
|
1624
|
+
}
|
|
1625
|
+
if (!c)
|
|
1626
|
+
err(2);
|
|
1627
|
+
if (sym < 256)
|
|
1628
|
+
buf[bt++] = sym;
|
|
1629
|
+
else if (sym == 256) {
|
|
1630
|
+
lpos = pos, lm = null;
|
|
1631
|
+
break;
|
|
1632
|
+
} else {
|
|
1633
|
+
var add = sym - 254;
|
|
1634
|
+
if (sym > 264) {
|
|
1635
|
+
var i2 = sym - 257, b = fleb[i2];
|
|
1636
|
+
add = bits(dat, pos, (1 << b) - 1) + fl[i2];
|
|
1637
|
+
pos += b;
|
|
1638
|
+
}
|
|
1639
|
+
var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
|
|
1640
|
+
if (!d)
|
|
1641
|
+
err(3);
|
|
1642
|
+
pos += d & 15;
|
|
1643
|
+
var dt = fd[dsym];
|
|
1644
|
+
if (dsym > 3) {
|
|
1645
|
+
var b = fdeb[dsym];
|
|
1646
|
+
dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
|
|
1647
|
+
}
|
|
1648
|
+
if (pos > tbts) {
|
|
1649
|
+
if (noSt)
|
|
1650
|
+
err(0);
|
|
1651
|
+
break;
|
|
1652
|
+
}
|
|
1653
|
+
if (resize)
|
|
1654
|
+
cbuf(bt + 131072);
|
|
1655
|
+
var end = bt + add;
|
|
1656
|
+
if (bt < dt) {
|
|
1657
|
+
var shift = dl - dt, dend = Math.min(dt, end);
|
|
1658
|
+
if (shift + bt < 0)
|
|
1659
|
+
err(3);
|
|
1660
|
+
for (;bt < dend; ++bt)
|
|
1661
|
+
buf[bt] = dict[shift + bt];
|
|
1662
|
+
}
|
|
1663
|
+
for (;bt < end; ++bt)
|
|
1664
|
+
buf[bt] = buf[bt - dt];
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
st.l = lm, st.p = lpos, st.b = bt, st.f = final;
|
|
1668
|
+
if (lm)
|
|
1669
|
+
final = 1, st.m = lbt, st.d = dm, st.n = dbt;
|
|
1670
|
+
} while (!final);
|
|
1671
|
+
return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
|
|
1672
|
+
};
|
|
1673
|
+
var et = /* @__PURE__ */ new u8(0);
|
|
1674
|
+
var b2 = function(d, b) {
|
|
1675
|
+
return d[b] | d[b + 1] << 8;
|
|
1676
|
+
};
|
|
1677
|
+
var b4 = function(d, b) {
|
|
1678
|
+
return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0;
|
|
1679
|
+
};
|
|
1680
|
+
var b8 = function(d, b) {
|
|
1681
|
+
return b4(d, b) + b4(d, b + 4) * 4294967296;
|
|
1682
|
+
};
|
|
1683
|
+
function inflateSync(data, opts) {
|
|
1684
|
+
return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary);
|
|
1685
|
+
}
|
|
1686
|
+
var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder;
|
|
1687
|
+
var tds = 0;
|
|
1688
|
+
try {
|
|
1689
|
+
td.decode(et, { stream: true });
|
|
1690
|
+
tds = 1;
|
|
1691
|
+
} catch (e) {}
|
|
1692
|
+
var dutf8 = function(d) {
|
|
1693
|
+
for (var r = "", i2 = 0;; ) {
|
|
1694
|
+
var c = d[i2++];
|
|
1695
|
+
var eb = (c > 127) + (c > 223) + (c > 239);
|
|
1696
|
+
if (i2 + eb > d.length)
|
|
1697
|
+
return { s: r, r: slc(d, i2 - 1) };
|
|
1698
|
+
if (!eb)
|
|
1699
|
+
r += String.fromCharCode(c);
|
|
1700
|
+
else if (eb == 3) {
|
|
1701
|
+
c = ((c & 15) << 18 | (d[i2++] & 63) << 12 | (d[i2++] & 63) << 6 | d[i2++] & 63) - 65536, r += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023);
|
|
1702
|
+
} else if (eb & 1)
|
|
1703
|
+
r += String.fromCharCode((c & 31) << 6 | d[i2++] & 63);
|
|
1704
|
+
else
|
|
1705
|
+
r += String.fromCharCode((c & 15) << 12 | (d[i2++] & 63) << 6 | d[i2++] & 63);
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
function strFromU8(dat, latin1) {
|
|
1709
|
+
if (latin1) {
|
|
1710
|
+
var r = "";
|
|
1711
|
+
for (var i2 = 0;i2 < dat.length; i2 += 16384)
|
|
1712
|
+
r += String.fromCharCode.apply(null, dat.subarray(i2, i2 + 16384));
|
|
1713
|
+
return r;
|
|
1714
|
+
} else if (td) {
|
|
1715
|
+
return td.decode(dat);
|
|
1716
|
+
} else {
|
|
1717
|
+
var _a2 = dutf8(dat), s = _a2.s, r = _a2.r;
|
|
1718
|
+
if (r.length)
|
|
1719
|
+
err(8);
|
|
1720
|
+
return s;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
var slzh = function(d, b) {
|
|
1724
|
+
return b + 30 + b2(d, b + 26) + b2(d, b + 28);
|
|
1725
|
+
};
|
|
1726
|
+
var zh = function(d, b, z5) {
|
|
1727
|
+
var fnl = b2(d, b + 28), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl, bs = b4(d, b + 20);
|
|
1728
|
+
var _a2 = z5 && bs == 4294967295 ? z64e(d, es) : [bs, b4(d, b + 24), b4(d, b + 42)], sc = _a2[0], su = _a2[1], off = _a2[2];
|
|
1729
|
+
return [b2(d, b + 10), sc, su, fn, es + b2(d, b + 30) + b2(d, b + 32), off];
|
|
1730
|
+
};
|
|
1731
|
+
var z64e = function(d, b) {
|
|
1732
|
+
for (;b2(d, b) != 1; b += 4 + b2(d, b + 2))
|
|
1733
|
+
;
|
|
1734
|
+
return [b8(d, b + 12), b8(d, b + 4), b8(d, b + 20)];
|
|
1735
|
+
};
|
|
1736
|
+
function unzipSync(data, opts) {
|
|
1737
|
+
var files = {};
|
|
1738
|
+
var e = data.length - 22;
|
|
1739
|
+
for (;b4(data, e) != 101010256; --e) {
|
|
1740
|
+
if (!e || data.length - e > 65558)
|
|
1741
|
+
err(13);
|
|
1742
|
+
}
|
|
1743
|
+
var c = b2(data, e + 8);
|
|
1744
|
+
if (!c)
|
|
1745
|
+
return {};
|
|
1746
|
+
var o = b4(data, e + 16);
|
|
1747
|
+
var z5 = o == 4294967295 || c == 65535;
|
|
1748
|
+
if (z5) {
|
|
1749
|
+
var ze = b4(data, e - 12);
|
|
1750
|
+
z5 = b4(data, ze) == 101075792;
|
|
1751
|
+
if (z5) {
|
|
1752
|
+
c = b4(data, ze + 32);
|
|
1753
|
+
o = b4(data, ze + 48);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
var fltr = opts && opts.filter;
|
|
1757
|
+
for (var i2 = 0;i2 < c; ++i2) {
|
|
1758
|
+
var _a2 = zh(data, o, z5), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
|
|
1759
|
+
o = no;
|
|
1760
|
+
if (!fltr || fltr({
|
|
1761
|
+
name: fn,
|
|
1762
|
+
size: sc,
|
|
1763
|
+
originalSize: su,
|
|
1764
|
+
compression: c_2
|
|
1765
|
+
})) {
|
|
1766
|
+
if (!c_2)
|
|
1767
|
+
files[fn] = slc(data, b, b + sc);
|
|
1768
|
+
else if (c_2 == 8)
|
|
1769
|
+
files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) });
|
|
1770
|
+
else
|
|
1771
|
+
err(14, "unknown compression type " + c_2);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
return files;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/bin-internal/heal-prepare.ts
|
|
1778
|
+
var LOCATOR_LINE_RE = /^Locator:\s*(.+)$/m;
|
|
1779
|
+
function classifyKind(raw) {
|
|
1780
|
+
if (/^getByRole\b/.test(raw))
|
|
1781
|
+
return "role";
|
|
1782
|
+
if (/^getByTestId\b/.test(raw))
|
|
1783
|
+
return "test-id";
|
|
1784
|
+
if (/^getByLabel\b/.test(raw))
|
|
1785
|
+
return "label";
|
|
1786
|
+
if (/^getByText\b/.test(raw))
|
|
1787
|
+
return "text";
|
|
1788
|
+
if (/^locator\(\s*['"`]\s*\.[A-Za-z_-]/.test(raw))
|
|
1789
|
+
return "css-class";
|
|
1790
|
+
return "other";
|
|
1791
|
+
}
|
|
1792
|
+
function extractDomSnapshot(tracePath) {
|
|
1793
|
+
if (!existsSync14(tracePath))
|
|
1794
|
+
return "";
|
|
1795
|
+
const buf = readFileSync12(tracePath);
|
|
1796
|
+
const entries = unzipSync(buf);
|
|
1797
|
+
const traceKey = Object.keys(entries).find((name) => name.endsWith(".trace"));
|
|
1798
|
+
let chosenKey = null;
|
|
1799
|
+
if (traceKey) {
|
|
1800
|
+
const traceText = new TextDecoder().decode(entries[traceKey]);
|
|
1801
|
+
const lines = traceText.split(`
|
|
1802
|
+
`).filter(Boolean);
|
|
1803
|
+
for (let i2 = lines.length - 1;i2 >= 0; i2--) {
|
|
1804
|
+
try {
|
|
1805
|
+
const evt = JSON.parse(lines[i2]);
|
|
1806
|
+
const isSnapshot = evt.type === "frame-snapshot" || evt.type === "snapshot";
|
|
1807
|
+
if (!isSnapshot)
|
|
1808
|
+
continue;
|
|
1809
|
+
const snap = evt.snapshot ?? {};
|
|
1810
|
+
const resourceName = snap.resourceName;
|
|
1811
|
+
if (typeof resourceName === "string") {
|
|
1812
|
+
if (entries[resourceName]) {
|
|
1813
|
+
chosenKey = resourceName;
|
|
1814
|
+
break;
|
|
1815
|
+
}
|
|
1816
|
+
const guessed = `resources/${resourceName.replace(/^resources\//, "")}`;
|
|
1817
|
+
if (entries[guessed]) {
|
|
1818
|
+
chosenKey = guessed;
|
|
1819
|
+
break;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
const snapshotName = snap.snapshotName;
|
|
1823
|
+
if (typeof snapshotName === "string") {
|
|
1824
|
+
const directGuess = Object.keys(entries).find((k) => k.endsWith(".html") && k.includes(snapshotName));
|
|
1825
|
+
if (directGuess) {
|
|
1826
|
+
chosenKey = directGuess;
|
|
1827
|
+
break;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
} catch {}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
if (!chosenKey) {
|
|
1834
|
+
const htmlKeys = Object.keys(entries).filter((name) => name.endsWith(".html")).sort();
|
|
1835
|
+
chosenKey = htmlKeys[htmlKeys.length - 1] ?? null;
|
|
1836
|
+
}
|
|
1837
|
+
if (!chosenKey)
|
|
1838
|
+
return "";
|
|
1839
|
+
const html = new TextDecoder().decode(entries[chosenKey]);
|
|
1840
|
+
return scrubFreeText(html);
|
|
1841
|
+
}
|
|
1842
|
+
function findPomLine(ticketDir, rawLocator) {
|
|
1843
|
+
const pomDir = join11(ticketDir, "page-objects");
|
|
1844
|
+
const candidates = [];
|
|
1845
|
+
if (existsSync14(pomDir)) {
|
|
1846
|
+
for (const name of readdirSync3(pomDir)) {
|
|
1847
|
+
if (name.endsWith(".ts"))
|
|
1848
|
+
candidates.push(join11(pomDir, name));
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
for (const file of candidates) {
|
|
1852
|
+
const text = readFileSync12(file, "utf8");
|
|
1853
|
+
const lines = text.split(`
|
|
1854
|
+
`);
|
|
1855
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
1856
|
+
const line = lines[i2];
|
|
1857
|
+
if (line.includes(rawLocator)) {
|
|
1858
|
+
const methodMatch = /^\s*(\w+)\s*=/.exec(line);
|
|
1859
|
+
return {
|
|
1860
|
+
pomFile: file,
|
|
1861
|
+
pomLine: i2 + 1,
|
|
1862
|
+
pomLineContent: line,
|
|
1863
|
+
pomMethodName: methodMatch?.[1] ?? "<anonymous>"
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
throw new Error(`POM line not found for locator: ${rawLocator}`);
|
|
1869
|
+
}
|
|
1870
|
+
function findGherkinStep(featureText, rawLocator) {
|
|
1871
|
+
const quoteMatch = /['"`]([^'"`]{2,})['"`]/.exec(rawLocator);
|
|
1872
|
+
if (quoteMatch) {
|
|
1873
|
+
const needle = quoteMatch[1];
|
|
1874
|
+
for (const line of featureText.split(`
|
|
1875
|
+
`)) {
|
|
1876
|
+
if (line.includes(needle) && /^\s*(When|Then|And|Given)\b/.test(line)) {
|
|
1877
|
+
return line.trim();
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
for (const line of featureText.split(`
|
|
1882
|
+
`)) {
|
|
1883
|
+
if (/^\s*(When|Then)\b/.test(line))
|
|
1884
|
+
return line.trim();
|
|
1885
|
+
}
|
|
1886
|
+
return "";
|
|
1887
|
+
}
|
|
1888
|
+
function healPrepare(repoRoot, ticket, runId, scenarioName) {
|
|
1889
|
+
const paths = resolveArtifactPaths(repoRoot, ticket);
|
|
1890
|
+
const classifierPath = join11(paths.ticketDir, "classifier-input.json");
|
|
1891
|
+
const classifier = JSON.parse(readFileSync12(classifierPath, "utf8"));
|
|
1892
|
+
const cls = classifier.scenarios.find((s) => s.name === scenarioName);
|
|
1893
|
+
if (!cls)
|
|
1894
|
+
throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
|
|
1895
|
+
const runDir = join11(paths.runsDir, runId);
|
|
1896
|
+
const normalized = JSON.parse(readFileSync12(join11(runDir, "normalized.json"), "utf8"));
|
|
1897
|
+
const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
|
|
1898
|
+
if (!normSc?.failure)
|
|
1899
|
+
throw new Error(`no failure recorded for scenario "${scenarioName}"`);
|
|
1900
|
+
const errorMessage = normSc.failure.errorMessage ?? "";
|
|
1901
|
+
const m = LOCATOR_LINE_RE.exec(errorMessage);
|
|
1902
|
+
if (!m)
|
|
1903
|
+
throw new Error(`cannot extract locator from errorMessage: ${errorMessage.slice(0, 80)}`);
|
|
1904
|
+
const raw = m[1].trim();
|
|
1905
|
+
const kind = classifyKind(raw);
|
|
1906
|
+
const pomLoc = findPomLine(paths.ticketDir, raw);
|
|
1907
|
+
const featureText = readFileSync12(paths.featurePath, "utf8");
|
|
1908
|
+
const gherkinStep = findGherkinStep(featureText, raw);
|
|
1909
|
+
const domSnapshotAtFailure = extractDomSnapshot(join11(runDir, "trace.zip"));
|
|
1910
|
+
return {
|
|
1911
|
+
ticket,
|
|
1912
|
+
runId,
|
|
1913
|
+
scenarioName,
|
|
1914
|
+
failedLocator: { raw, kind, ...pomLoc },
|
|
1915
|
+
gherkinStep,
|
|
1916
|
+
domSnapshotAtFailure
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
async function healPrepareCmd(argv) {
|
|
1920
|
+
const [ticket, runId, ...scenarioParts] = argv;
|
|
1921
|
+
if (!ticket || !runId || scenarioParts.length === 0) {
|
|
1922
|
+
console.error("[xera:heal-prepare] usage: heal-prepare <TICKET> <RUN_ID> <SCENARIO_NAME>");
|
|
1923
|
+
return 1;
|
|
1924
|
+
}
|
|
1925
|
+
const scenarioName = scenarioParts.join(" ");
|
|
1926
|
+
try {
|
|
1927
|
+
const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
|
|
1928
|
+
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
1929
|
+
const outPath = join11(paths.runsDir, runId, "heal-input.json");
|
|
1930
|
+
writeFileSync9(outPath, JSON.stringify(result, null, 2));
|
|
1931
|
+
console.log(`[xera:heal-prepare] wrote ${outPath}`);
|
|
1932
|
+
return 0;
|
|
1933
|
+
} catch (err2) {
|
|
1934
|
+
console.error(`[xera:heal-prepare] ${err2.message}`);
|
|
1935
|
+
return 1;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// src/bin-internal/lint.ts
|
|
1940
|
+
import { lintTicket } from "@xera-ai/web";
|
|
1941
|
+
async function lintCmd(argv) {
|
|
1942
|
+
const ticket = argv[0];
|
|
1943
|
+
if (!ticket) {
|
|
1944
|
+
console.error("[xera:lint] usage: lint <TICKET>");
|
|
1945
|
+
return 1;
|
|
1946
|
+
}
|
|
1947
|
+
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
1948
|
+
const r = await lintTicket(paths.ticketDir);
|
|
1949
|
+
if (r.ok) {
|
|
1950
|
+
console.log("[xera:lint] ok");
|
|
1951
|
+
return 0;
|
|
1952
|
+
}
|
|
1953
|
+
for (const w of r.warnings)
|
|
1954
|
+
console.error(`[xera:lint] ${w.file}:${w.line} [${w.rule}] ${w.message}`);
|
|
1955
|
+
return 2;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
698
1958
|
// src/bin-internal/normalize.ts
|
|
1959
|
+
import { existsSync as existsSync15, readdirSync as readdirSync4 } from "fs";
|
|
1960
|
+
import { join as join12 } from "path";
|
|
699
1961
|
import { normalizeRun } from "@xera-ai/web";
|
|
700
|
-
import { readdirSync, existsSync as existsSync10 } from "fs";
|
|
701
|
-
import { join as join6 } from "path";
|
|
702
1962
|
async function normalizeCmd(argv) {
|
|
703
1963
|
const ticket = argv[0];
|
|
704
1964
|
if (!ticket) {
|
|
@@ -707,13 +1967,13 @@ async function normalizeCmd(argv) {
|
|
|
707
1967
|
}
|
|
708
1968
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
709
1969
|
const runArg = argv.find((a) => a.startsWith("--run="));
|
|
710
|
-
const runId = runArg ? runArg.split("=")[1] :
|
|
1970
|
+
const runId = runArg ? runArg.split("=")[1] : readdirSync4(paths.runsDir).filter((n) => !n.startsWith(".")).sort().pop();
|
|
711
1971
|
if (!runId) {
|
|
712
1972
|
console.error("[xera:normalize] no run found");
|
|
713
1973
|
return 1;
|
|
714
1974
|
}
|
|
715
|
-
const runDir =
|
|
716
|
-
if (!
|
|
1975
|
+
const runDir = join12(paths.runsDir, runId);
|
|
1976
|
+
if (!existsSync15(runDir)) {
|
|
717
1977
|
console.error(`[xera:normalize] runs/${runId} missing`);
|
|
718
1978
|
return 1;
|
|
719
1979
|
}
|
|
@@ -722,77 +1982,46 @@ async function normalizeCmd(argv) {
|
|
|
722
1982
|
return 0;
|
|
723
1983
|
}
|
|
724
1984
|
|
|
725
|
-
// src/bin-internal/
|
|
726
|
-
import {
|
|
727
|
-
import { join as
|
|
728
|
-
|
|
729
|
-
// src/classifier/aggregate.ts
|
|
730
|
-
var CLASS_PRIORITY = [
|
|
731
|
-
"REAL_BUG",
|
|
732
|
-
"TEST_BUG",
|
|
733
|
-
"SELECTOR_DRIFT",
|
|
734
|
-
"FLAKY",
|
|
735
|
-
"PASS"
|
|
736
|
-
];
|
|
737
|
-
var CONF_RANK = { low: 1, medium: 2, high: 3 };
|
|
738
|
-
function aggregateScenarios(scenarios) {
|
|
739
|
-
if (scenarios.length === 0) {
|
|
740
|
-
return { overall: "PASS", overallConfidence: "low", scenarios: [] };
|
|
741
|
-
}
|
|
742
|
-
if (scenarios.every((s) => s.outcome === "PASS")) {
|
|
743
|
-
return { overall: "PASS", overallConfidence: "high", scenarios };
|
|
744
|
-
}
|
|
745
|
-
let chosen = "PASS";
|
|
746
|
-
for (const cls of CLASS_PRIORITY) {
|
|
747
|
-
if (scenarios.some((s) => s.class === cls)) {
|
|
748
|
-
chosen = cls;
|
|
749
|
-
break;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
const matching = scenarios.filter((s) => s.class === chosen);
|
|
753
|
-
const minConf = matching.reduce((acc, s) => CONF_RANK[s.confidence] < CONF_RANK[acc] ? s.confidence : acc, "high");
|
|
754
|
-
return { overall: chosen, overallConfidence: minConf, scenarios };
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// src/reporter/status-writer.ts
|
|
758
|
-
import { existsSync as existsSync12 } from "fs";
|
|
1985
|
+
// src/bin-internal/post.ts
|
|
1986
|
+
import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
|
|
1987
|
+
import { join as join13 } from "path";
|
|
759
1988
|
|
|
760
1989
|
// src/artifact/status.ts
|
|
761
|
-
import { existsSync as
|
|
1990
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
|
|
762
1991
|
import { dirname as dirname5 } from "path";
|
|
763
|
-
import { z as
|
|
764
|
-
var ClassificationEnum =
|
|
765
|
-
var ResultEnum =
|
|
766
|
-
var ConfidenceEnum =
|
|
767
|
-
var HistoryEntrySchema =
|
|
768
|
-
ts:
|
|
1992
|
+
import { z as z5 } from "zod";
|
|
1993
|
+
var ClassificationEnum = z5.enum(["PASS", "REAL_BUG", "SELECTOR_DRIFT", "FLAKY", "TEST_BUG"]);
|
|
1994
|
+
var ResultEnum = z5.enum(["PASS", "FAIL"]);
|
|
1995
|
+
var ConfidenceEnum = z5.enum(["low", "medium", "high"]);
|
|
1996
|
+
var HistoryEntrySchema = z5.object({
|
|
1997
|
+
ts: z5.string(),
|
|
769
1998
|
result: ResultEnum,
|
|
770
1999
|
class: ClassificationEnum
|
|
771
2000
|
});
|
|
772
|
-
var StatusJsonSchema =
|
|
773
|
-
ticket:
|
|
774
|
-
lastRun:
|
|
2001
|
+
var StatusJsonSchema = z5.object({
|
|
2002
|
+
ticket: z5.string(),
|
|
2003
|
+
lastRun: z5.string(),
|
|
775
2004
|
result: ResultEnum,
|
|
776
2005
|
classification: ClassificationEnum,
|
|
777
2006
|
confidence: ConfidenceEnum,
|
|
778
|
-
scenarios:
|
|
779
|
-
total:
|
|
780
|
-
passed:
|
|
781
|
-
failed:
|
|
782
|
-
skipped:
|
|
2007
|
+
scenarios: z5.object({
|
|
2008
|
+
total: z5.number().int().nonnegative(),
|
|
2009
|
+
passed: z5.number().int().nonnegative(),
|
|
2010
|
+
failed: z5.number().int().nonnegative(),
|
|
2011
|
+
skipped: z5.number().int().nonnegative()
|
|
783
2012
|
}),
|
|
784
|
-
history:
|
|
785
|
-
last_jira_comment_id:
|
|
2013
|
+
history: z5.array(HistoryEntrySchema).default([]),
|
|
2014
|
+
last_jira_comment_id: z5.string().optional()
|
|
786
2015
|
});
|
|
787
2016
|
var HISTORY_CAP = 20;
|
|
788
2017
|
function readStatus(path) {
|
|
789
|
-
if (!
|
|
2018
|
+
if (!existsSync16(path))
|
|
790
2019
|
return null;
|
|
791
|
-
return StatusJsonSchema.parse(JSON.parse(
|
|
2020
|
+
return StatusJsonSchema.parse(JSON.parse(readFileSync13(path, "utf8")));
|
|
792
2021
|
}
|
|
793
2022
|
function writeStatus(path, status) {
|
|
794
|
-
|
|
795
|
-
|
|
2023
|
+
mkdirSync9(dirname5(path), { recursive: true });
|
|
2024
|
+
writeFileSync10(path, JSON.stringify(status, null, 2));
|
|
796
2025
|
}
|
|
797
2026
|
function appendHistory(path, entry) {
|
|
798
2027
|
const s = readStatus(path);
|
|
@@ -804,32 +2033,82 @@ function appendHistory(path, entry) {
|
|
|
804
2033
|
return s;
|
|
805
2034
|
}
|
|
806
2035
|
|
|
807
|
-
// src/
|
|
808
|
-
function
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
ticket: input.ticket,
|
|
814
|
-
lastRun: input.runTs,
|
|
815
|
-
result,
|
|
816
|
-
classification: input.classification.overall,
|
|
817
|
-
confidence: input.classification.overallConfidence,
|
|
818
|
-
scenarios: input.scenarioCounts,
|
|
819
|
-
history: [entry]
|
|
820
|
-
});
|
|
821
|
-
return;
|
|
2036
|
+
// src/bin-internal/post.ts
|
|
2037
|
+
async function postCmd(argv) {
|
|
2038
|
+
const ticket = argv[0];
|
|
2039
|
+
if (!ticket) {
|
|
2040
|
+
console.error("[xera:post] usage: post <TICKET>");
|
|
2041
|
+
return 1;
|
|
822
2042
|
}
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
2043
|
+
const cwd = process.cwd();
|
|
2044
|
+
const config = await loadConfig(cwd);
|
|
2045
|
+
if (!config.reporting.postToJira) {
|
|
2046
|
+
console.log("[xera:post] postToJira disabled in config; skipping");
|
|
2047
|
+
return 0;
|
|
2048
|
+
}
|
|
2049
|
+
const paths = resolveArtifactPaths(cwd, ticket);
|
|
2050
|
+
const draftPath = join13(paths.ticketDir, "jira-comment.draft.md");
|
|
2051
|
+
if (!existsSync17(draftPath)) {
|
|
2052
|
+
console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
|
|
2053
|
+
return 1;
|
|
2054
|
+
}
|
|
2055
|
+
const body = readFileSync14(draftPath, "utf8");
|
|
2056
|
+
const client = await createJiraClient({
|
|
2057
|
+
baseUrl: config.jira.baseUrl,
|
|
2058
|
+
preferMcp: true,
|
|
2059
|
+
...process.env.JIRA_EMAIL && process.env.JIRA_API_TOKEN ? { rest: { email: process.env.JIRA_EMAIL, apiToken: process.env.JIRA_API_TOKEN } } : {}
|
|
831
2060
|
});
|
|
832
|
-
|
|
2061
|
+
const r = await client.postComment(ticket, body);
|
|
2062
|
+
console.log(`[xera:post] posted comment id=${r.id}`);
|
|
2063
|
+
const s = readStatus(paths.statusPath);
|
|
2064
|
+
if (s)
|
|
2065
|
+
writeStatus(paths.statusPath, { ...s, last_jira_comment_id: r.id });
|
|
2066
|
+
return 0;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// src/bin-internal/promote.ts
|
|
2070
|
+
import { promotePom } from "@xera-ai/web";
|
|
2071
|
+
async function promoteCmd(argv) {
|
|
2072
|
+
const [ticket, className] = argv;
|
|
2073
|
+
if (!ticket || !className) {
|
|
2074
|
+
console.error("[xera:promote] usage: promote <TICKET> <PomClassName>");
|
|
2075
|
+
return 1;
|
|
2076
|
+
}
|
|
2077
|
+
await promotePom({ repoRoot: process.cwd(), ticket, className });
|
|
2078
|
+
console.log(`[xera:promote] moved ${className} \u2192 shared/page-objects/`);
|
|
2079
|
+
return 0;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// src/bin-internal/report.ts
|
|
2083
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "fs";
|
|
2084
|
+
import { join as join14 } from "path";
|
|
2085
|
+
|
|
2086
|
+
// src/classifier/aggregate.ts
|
|
2087
|
+
var CLASS_PRIORITY = [
|
|
2088
|
+
"REAL_BUG",
|
|
2089
|
+
"TEST_BUG",
|
|
2090
|
+
"SELECTOR_DRIFT",
|
|
2091
|
+
"FLAKY",
|
|
2092
|
+
"PASS"
|
|
2093
|
+
];
|
|
2094
|
+
var CONF_RANK = { low: 1, medium: 2, high: 3 };
|
|
2095
|
+
function aggregateScenarios(scenarios) {
|
|
2096
|
+
if (scenarios.length === 0) {
|
|
2097
|
+
return { overall: "PASS", overallConfidence: "low", scenarios: [] };
|
|
2098
|
+
}
|
|
2099
|
+
if (scenarios.every((s) => s.outcome === "PASS")) {
|
|
2100
|
+
return { overall: "PASS", overallConfidence: "high", scenarios };
|
|
2101
|
+
}
|
|
2102
|
+
let chosen = "PASS";
|
|
2103
|
+
for (const cls of CLASS_PRIORITY) {
|
|
2104
|
+
if (scenarios.some((s) => s.class === cls)) {
|
|
2105
|
+
chosen = cls;
|
|
2106
|
+
break;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
const matching = scenarios.filter((s) => s.class === chosen);
|
|
2110
|
+
const minConf = matching.reduce((acc, s) => CONF_RANK[s.confidence] < CONF_RANK[acc] ? s.confidence : acc, "high");
|
|
2111
|
+
return { overall: chosen, overallConfidence: minConf, scenarios };
|
|
833
2112
|
}
|
|
834
2113
|
|
|
835
2114
|
// src/reporter/jira-comment.ts
|
|
@@ -861,6 +2140,35 @@ xera v${input.xeraVersion} \u2022 prompts v${input.promptsVersion}`;
|
|
|
861
2140
|
`);
|
|
862
2141
|
}
|
|
863
2142
|
|
|
2143
|
+
// src/reporter/status-writer.ts
|
|
2144
|
+
import { existsSync as existsSync18 } from "fs";
|
|
2145
|
+
function writeStatusFromClassification(path, input) {
|
|
2146
|
+
const result = input.classification.overall === "PASS" ? "PASS" : "FAIL";
|
|
2147
|
+
const entry = { ts: input.runTs, result, class: input.classification.overall };
|
|
2148
|
+
if (!existsSync18(path)) {
|
|
2149
|
+
writeStatus(path, {
|
|
2150
|
+
ticket: input.ticket,
|
|
2151
|
+
lastRun: input.runTs,
|
|
2152
|
+
result,
|
|
2153
|
+
classification: input.classification.overall,
|
|
2154
|
+
confidence: input.classification.overallConfidence,
|
|
2155
|
+
scenarios: input.scenarioCounts,
|
|
2156
|
+
history: [entry]
|
|
2157
|
+
});
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
const cur = readStatus(path);
|
|
2161
|
+
writeStatus(path, {
|
|
2162
|
+
...cur,
|
|
2163
|
+
lastRun: input.runTs,
|
|
2164
|
+
result,
|
|
2165
|
+
classification: input.classification.overall,
|
|
2166
|
+
confidence: input.classification.overallConfidence,
|
|
2167
|
+
scenarios: input.scenarioCounts
|
|
2168
|
+
});
|
|
2169
|
+
appendHistory(path, entry);
|
|
2170
|
+
}
|
|
2171
|
+
|
|
864
2172
|
// src/bin-internal/report.ts
|
|
865
2173
|
async function reportCmd(argv) {
|
|
866
2174
|
const ticket = argv[0];
|
|
@@ -870,7 +2178,7 @@ async function reportCmd(argv) {
|
|
|
870
2178
|
return 1;
|
|
871
2179
|
}
|
|
872
2180
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
873
|
-
const input = JSON.parse(
|
|
2181
|
+
const input = JSON.parse(readFileSync15(inputArg.slice("--input=".length), "utf8"));
|
|
874
2182
|
const aggregated = aggregateScenarios(input.scenarios);
|
|
875
2183
|
const ts = new Date().toISOString();
|
|
876
2184
|
writeStatusFromClassification(paths.statusPath, {
|
|
@@ -888,47 +2196,12 @@ async function reportCmd(argv) {
|
|
|
888
2196
|
xeraVersion: "0.1.0",
|
|
889
2197
|
promptsVersion: "1.0.0"
|
|
890
2198
|
});
|
|
891
|
-
const draftPath =
|
|
892
|
-
|
|
2199
|
+
const draftPath = join14(paths.ticketDir, "jira-comment.draft.md");
|
|
2200
|
+
writeFileSync11(draftPath, md);
|
|
893
2201
|
console.log(`[xera:report] wrote status.json and ${draftPath}`);
|
|
894
2202
|
return 0;
|
|
895
2203
|
}
|
|
896
2204
|
|
|
897
|
-
// src/bin-internal/post.ts
|
|
898
|
-
import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
|
|
899
|
-
import { join as join8 } from "path";
|
|
900
|
-
async function postCmd(argv) {
|
|
901
|
-
const ticket = argv[0];
|
|
902
|
-
if (!ticket) {
|
|
903
|
-
console.error("[xera:post] usage: post <TICKET>");
|
|
904
|
-
return 1;
|
|
905
|
-
}
|
|
906
|
-
const cwd = process.cwd();
|
|
907
|
-
const config = await loadConfig(cwd);
|
|
908
|
-
if (!config.reporting.postToJira) {
|
|
909
|
-
console.log("[xera:post] postToJira disabled in config; skipping");
|
|
910
|
-
return 0;
|
|
911
|
-
}
|
|
912
|
-
const paths = resolveArtifactPaths(cwd, ticket);
|
|
913
|
-
const draftPath = join8(paths.ticketDir, "jira-comment.draft.md");
|
|
914
|
-
if (!existsSync13(draftPath)) {
|
|
915
|
-
console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
|
|
916
|
-
return 1;
|
|
917
|
-
}
|
|
918
|
-
const body = readFileSync10(draftPath, "utf8");
|
|
919
|
-
const client = await createJiraClient({
|
|
920
|
-
baseUrl: config.jira.baseUrl,
|
|
921
|
-
preferMcp: true,
|
|
922
|
-
...process.env.JIRA_EMAIL && process.env.JIRA_API_TOKEN ? { rest: { email: process.env.JIRA_EMAIL, apiToken: process.env.JIRA_API_TOKEN } } : {}
|
|
923
|
-
});
|
|
924
|
-
const r = await client.postComment(ticket, body);
|
|
925
|
-
console.log(`[xera:post] posted comment id=${r.id}`);
|
|
926
|
-
const s = readStatus(paths.statusPath);
|
|
927
|
-
if (s)
|
|
928
|
-
writeStatus(paths.statusPath, { ...s, last_jira_comment_id: r.id });
|
|
929
|
-
return 0;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
2205
|
// src/bin-internal/status-cmd.ts
|
|
933
2206
|
async function statusCmd(argv) {
|
|
934
2207
|
const ticket = argv[0];
|
|
@@ -948,6 +2221,25 @@ async function statusCmd(argv) {
|
|
|
948
2221
|
return 0;
|
|
949
2222
|
}
|
|
950
2223
|
|
|
2224
|
+
// src/bin-internal/typecheck.ts
|
|
2225
|
+
import { typecheckTicket } from "@xera-ai/web";
|
|
2226
|
+
async function typecheckCmd(argv) {
|
|
2227
|
+
const ticket = argv[0];
|
|
2228
|
+
if (!ticket) {
|
|
2229
|
+
console.error("[xera:typecheck] usage: typecheck <TICKET>");
|
|
2230
|
+
return 1;
|
|
2231
|
+
}
|
|
2232
|
+
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
2233
|
+
const r = await typecheckTicket(paths.ticketDir);
|
|
2234
|
+
if (r.ok) {
|
|
2235
|
+
console.log("[xera:typecheck] ok");
|
|
2236
|
+
return 0;
|
|
2237
|
+
}
|
|
2238
|
+
for (const e of r.errors)
|
|
2239
|
+
console.error(`[xera:typecheck] ${e}`);
|
|
2240
|
+
return 2;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
951
2243
|
// src/bin-internal/unlock.ts
|
|
952
2244
|
async function unlockCmd(argv) {
|
|
953
2245
|
const ticket = argv[0];
|
|
@@ -971,32 +2263,49 @@ async function unlockCmd(argv) {
|
|
|
971
2263
|
return 0;
|
|
972
2264
|
}
|
|
973
2265
|
|
|
974
|
-
// src/bin-internal/
|
|
975
|
-
import {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
2266
|
+
// src/bin-internal/validate-feature.ts
|
|
2267
|
+
import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
|
|
2268
|
+
import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
|
|
2269
|
+
async function validateFeatureCmd(argv) {
|
|
2270
|
+
const ticket = argv[0];
|
|
2271
|
+
if (!ticket) {
|
|
2272
|
+
console.error("[xera:validate-feature] usage: validate-feature <TICKET>");
|
|
980
2273
|
return 1;
|
|
981
2274
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
2275
|
+
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
2276
|
+
if (!existsSync19(paths.featurePath)) {
|
|
2277
|
+
console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
|
|
2278
|
+
return 1;
|
|
2279
|
+
}
|
|
2280
|
+
const r = validateGherkin2(readFileSync16(paths.featurePath, "utf8"));
|
|
2281
|
+
if (r.ok) {
|
|
2282
|
+
console.log("[xera:validate-feature] ok");
|
|
2283
|
+
return 0;
|
|
2284
|
+
}
|
|
2285
|
+
for (const e of r.errors)
|
|
2286
|
+
console.error(`[xera:validate-feature] line ${e.line}: ${e.message}`);
|
|
2287
|
+
return 2;
|
|
985
2288
|
}
|
|
986
2289
|
|
|
987
2290
|
// src/bin-internal/index.ts
|
|
988
2291
|
var COMMANDS = {
|
|
2292
|
+
doctor: doctorCmd,
|
|
2293
|
+
"eval-deterministic": evalDeterministicCmd,
|
|
2294
|
+
"eval-prepare": evalPrepareCmd,
|
|
2295
|
+
"eval-report": evalReportCmd,
|
|
2296
|
+
exec: execCmd,
|
|
989
2297
|
fetch: fetchCmd,
|
|
990
|
-
"
|
|
991
|
-
typecheck: typecheckCmd,
|
|
2298
|
+
"heal-prepare": healPrepareCmd,
|
|
992
2299
|
lint: lintCmd,
|
|
993
|
-
exec: execCmd,
|
|
994
2300
|
normalize: normalizeCmd,
|
|
995
|
-
report: reportCmd,
|
|
996
2301
|
post: postCmd,
|
|
2302
|
+
promote: promoteCmd,
|
|
2303
|
+
report: reportCmd,
|
|
997
2304
|
status: statusCmd,
|
|
2305
|
+
typecheck: typecheckCmd,
|
|
998
2306
|
unlock: unlockCmd,
|
|
999
|
-
|
|
2307
|
+
"validate-feature": validateFeatureCmd,
|
|
2308
|
+
"verify-prompts": verifyPromptsCmd
|
|
1000
2309
|
};
|
|
1001
2310
|
async function run(argv) {
|
|
1002
2311
|
const [cmd, ...rest] = argv;
|
|
@@ -1007,8 +2316,8 @@ Commands: ${Object.keys(COMMANDS).join(", ")}`);
|
|
|
1007
2316
|
}
|
|
1008
2317
|
try {
|
|
1009
2318
|
return await COMMANDS[cmd](rest);
|
|
1010
|
-
} catch (
|
|
1011
|
-
console.error(`[xera:${cmd}] failed: ${
|
|
2319
|
+
} catch (err2) {
|
|
2320
|
+
console.error(`[xera:${cmd}] failed: ${err2.message}`);
|
|
1012
2321
|
return 4;
|
|
1013
2322
|
}
|
|
1014
2323
|
}
|