@ats-cx/cx-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +23 -0
  2. package/dist/apm/client.d.ts +59 -0
  3. package/dist/apm/client.js +247 -0
  4. package/dist/apm/config-state.d.ts +19 -0
  5. package/dist/apm/config-state.js +18 -0
  6. package/dist/apm/config.d.ts +93 -0
  7. package/dist/apm/config.js +72 -0
  8. package/dist/apm/errors.d.ts +39 -0
  9. package/dist/apm/errors.js +39 -0
  10. package/dist/apm/portal.d.ts +20 -0
  11. package/dist/apm/portal.js +89 -0
  12. package/dist/apm/values.d.ts +13 -0
  13. package/dist/apm/values.js +58 -0
  14. package/dist/config.d.ts +22 -0
  15. package/dist/config.js +168 -0
  16. package/dist/db/client.d.ts +23 -0
  17. package/dist/db/client.js +21 -0
  18. package/dist/diff/project-diff.d.ts +125 -0
  19. package/dist/diff/project-diff.js +531 -0
  20. package/dist/index.d.ts +38 -0
  21. package/dist/index.js +23 -0
  22. package/dist/normalize/log.d.ts +12 -0
  23. package/dist/normalize/log.js +143 -0
  24. package/dist/normalize/project.d.ts +18 -0
  25. package/dist/normalize/project.js +254 -0
  26. package/dist/providers/log-provider.d.ts +27 -0
  27. package/dist/providers/log-provider.js +63 -0
  28. package/dist/providers/project-provider.d.ts +10 -0
  29. package/dist/providers/project-provider.js +22 -0
  30. package/dist/providers/remote-project-provider.d.ts +28 -0
  31. package/dist/providers/remote-project-provider.js +167 -0
  32. package/dist/providers/source-provider.d.ts +11 -0
  33. package/dist/providers/source-provider.js +33 -0
  34. package/dist/rules/engine.d.ts +13 -0
  35. package/dist/rules/engine.js +213 -0
  36. package/dist/run/result-schema.d.ts +98 -0
  37. package/dist/run/result-schema.js +50 -0
  38. package/dist/run/store.d.ts +23 -0
  39. package/dist/run/store.js +49 -0
  40. package/dist/semantics/resolver.d.ts +37 -0
  41. package/dist/semantics/resolver.js +220 -0
  42. package/dist/semantics/table-loader.d.ts +11 -0
  43. package/dist/semantics/table-loader.js +44 -0
  44. package/dist/semantics/table-schema.d.ts +30 -0
  45. package/dist/semantics/table-schema.js +186 -0
  46. package/dist/types.d.ts +164 -0
  47. package/dist/types.js +2 -0
  48. package/package.json +33 -0
@@ -0,0 +1,167 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ /**
4
+ * 快照表:列 UIDPK(bigint 主键)/ PROJECT_UID / PROJECT_XML_DATA / CREATE_TIME。
5
+ *
6
+ * 两条 SQL 都按 UIDPK 升序取整组,不用 `LIMIT 1`:CREATE_TIME 无小数秒,文件名的序号后缀由记录在
7
+ * 同秒组内的位次决定,只取一行就无从得知位次——会把组内末条写成无后缀基名,与 `--history` 写的
8
+ * 组内首条撞名互相覆盖。最新一条改为「查出最新一秒的整组,落其中 UIDPK 最大的那条」,
9
+ * 两种模式因此共用同一套命名,同一条记录在哪种模式下都得到同一个文件名。
10
+ */
11
+ const LATEST_SECOND_SQL = "SELECT UIDPK, PROJECT_UID, PROJECT_XML_DATA, CREATE_TIME FROM aprod_failed_project_info WHERE PROJECT_UID = ? AND CREATE_TIME = (SELECT MAX(CREATE_TIME) FROM aprod_failed_project_info WHERE PROJECT_UID = ?) ORDER BY UIDPK ASC";
12
+ const HISTORY_SQL = "SELECT UIDPK, PROJECT_UID, PROJECT_XML_DATA, CREATE_TIME FROM aprod_failed_project_info WHERE PROJECT_UID = ? ORDER BY CREATE_TIME ASC, UIDPK ASC";
13
+ /**
14
+ * 远端快照拉取:查 DB → 转 JSON → 落盘 `<projectHistoryDir>/<projectId>/<projectId>_<时间>.json`
15
+ * (同秒的第 2 条起带 `_02` 序号后缀,见 planSnapshots)。
16
+ *
17
+ * 逐 projectId 循环执行单项目 SQL(不用 CTE + ROW_NUMBER):不依赖 MySQL 8,错误定位到单 id;
18
+ * pull 的 id 数量极小,N 次往返可忽略。
19
+ * 连接 / SQL 错误一律向上抛(由调用方转 exit 1),只有「查到 0 条」与「单条记录坏数据」降级为警告 / 失败记录。
20
+ */
21
+ export function createRemoteProjectProvider(client, projectHistoryDir) {
22
+ async function pull(mode, projectIds) {
23
+ const projects = [];
24
+ const warnings = [];
25
+ for (const projectId of projectIds) {
26
+ const rows = mode === "latest"
27
+ ? await client.query(LATEST_SECOND_SQL, [projectId, projectId])
28
+ : await client.query(HISTORY_SQL, [projectId]);
29
+ const entry = { projectId, snapshotCount: 0, files: [], failures: [] };
30
+ projects.push(entry);
31
+ if (rows.length === 0) {
32
+ warnings.push(`项目 ${projectId} 未查到任何记录`);
33
+ continue;
34
+ }
35
+ // latest 查的是最新一秒的整组,只落其中最后写入的一条——但沿用它在组内的文件名。
36
+ const plans = planSnapshots(projectId, rows);
37
+ const selected = mode === "latest" ? [...plans].sort(byUidpk).slice(-1) : plans;
38
+ const written = new Set();
39
+ for (const plan of selected) {
40
+ if (plan.error !== "") {
41
+ entry.failures.push(plan.error);
42
+ continue;
43
+ }
44
+ const filePath = join(projectHistoryDir, projectId, plan.fileName);
45
+ // 防御性兜底:planSnapshots 保证同批文件名唯一,真撞上说明有 bug,
46
+ // 宁可少写一条并显式报错,也不静默覆盖让 snapshotCount 虚高。
47
+ if (written.has(filePath)) {
48
+ entry.failures.push(`快照文件名重复,跳过写入避免覆盖: ${filePath}`);
49
+ continue;
50
+ }
51
+ try {
52
+ await writeSnapshot(filePath, plan.row);
53
+ }
54
+ catch (error) {
55
+ entry.failures.push(messageOf(error));
56
+ continue;
57
+ }
58
+ written.add(filePath);
59
+ entry.files.push(filePath);
60
+ entry.snapshotCount += 1;
61
+ }
62
+ }
63
+ return { projects, warnings };
64
+ }
65
+ return {
66
+ pullLatest: projectIds => pull("latest", projectIds),
67
+ pullHistory: projectIds => pull("history", projectIds),
68
+ };
69
+ }
70
+ /**
71
+ * 行 → 文件名。`CREATE_TIME` 是 timestamp(无小数秒),同秒多条记录很常见且内容各不相同,
72
+ * 只按时间命名会让后写的静默覆盖前一条,历史版本真丢。
73
+ *
74
+ * 同秒组内按 `UIDPK`(自增主键)升序定位次:第 1 条沿用无后缀命名,其后追加 `_02`、`_03`……
75
+ * - 位次只由 UIDPK 决定,不受 SQL 返回顺序与其他行是否落盘失败影响,同一批数据两次 pull 得到同样的文件名(幂等)。
76
+ * 代价是组内首条解析失败时目录里只有 `_02` 而无基名文件——宁可留空位,也不让其余行的文件名跟着漂移。
77
+ * 幂等依赖历史 SQL 既 SELECT 了 UIDPK 又按它排序:真缺 UIDPK 时 byUidpk 退化成保留返回顺序。
78
+ * - 字典序仍等于版本时间序(`.` 0x2E < `_` 0x5F,无后缀者排最前),本地快照 provider 靠 `readdir().sort()`
79
+ * 定版本序,存量文件与 `brief` / `diff-project` 的版本名消费方式都不受影响。
80
+ * 同秒超过 99 条时序号自然变 3 位,`_100` 会排到 `_11` 前面——仅该秒内排序降级,不丢数据(实测单秒最多 36 条)。
81
+ */
82
+ function planSnapshots(projectId, rows) {
83
+ const plans = [];
84
+ const groups = new Map();
85
+ for (const row of rows) {
86
+ let timeKey;
87
+ try {
88
+ timeKey = formatCreateTime(row.CREATE_TIME);
89
+ }
90
+ catch (error) {
91
+ plans.push({ row, fileName: "", error: messageOf(error) });
92
+ continue;
93
+ }
94
+ const plan = { row, fileName: "", error: "" };
95
+ plans.push(plan);
96
+ const group = groups.get(timeKey);
97
+ if (group) {
98
+ group.push(plan);
99
+ }
100
+ else {
101
+ groups.set(timeKey, [plan]);
102
+ }
103
+ }
104
+ for (const [timeKey, group] of groups) {
105
+ [...group].sort(byUidpk).forEach((plan, index) => {
106
+ const suffix = index === 0 ? "" : `_${String(index + 1).padStart(2, "0")}`;
107
+ plan.fileName = `${projectId}_${timeKey}${suffix}.json`;
108
+ });
109
+ }
110
+ return plans;
111
+ }
112
+ /** UIDPK 缺失或非数值时视为相等,交给 sort 的稳定性保留原返回顺序。 */
113
+ function byUidpk(left, right) {
114
+ const a = uidpkOf(left.row);
115
+ const b = uidpkOf(right.row);
116
+ if (a === undefined || b === undefined || a === b) {
117
+ return 0;
118
+ }
119
+ return a < b ? -1 : 1;
120
+ }
121
+ function messageOf(error) {
122
+ return error instanceof Error ? error.message : String(error);
123
+ }
124
+ function uidpkOf(row) {
125
+ if (row.UIDPK === undefined || row.UIDPK === null) {
126
+ return undefined;
127
+ }
128
+ const value = Number(row.UIDPK);
129
+ return Number.isFinite(value) ? value : undefined;
130
+ }
131
+ async function writeSnapshot(filePath, row) {
132
+ const data = await parseSnapshotData(String(row.PROJECT_XML_DATA ?? ""));
133
+ mkdirSync(dirname(filePath), { recursive: true });
134
+ writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
135
+ }
136
+ /**
137
+ * 按「trim 后以 `<` 开头且以 `>` 结尾」判定 XML,否则按 JSON 解析;选中的分支失败即报错。
138
+ * 不做两个分支互相回退:形状判定互斥(以 `<` 开头结尾的串不可能是合法 JSON,反之亦然),
139
+ * 回退只会掩盖真实的解析错误。线上数据目前全是 JSON 字符串,XML 分支为防御性保留。
140
+ */
141
+ async function parseSnapshotData(raw) {
142
+ const trimmed = raw.trim();
143
+ const looksXml = trimmed.startsWith("<") && trimmed.endsWith(">");
144
+ try {
145
+ return looksXml ? await parseXml(trimmed) : JSON.parse(trimmed);
146
+ }
147
+ catch (error) {
148
+ const format = looksXml ? "XML" : "JSON";
149
+ throw new Error(`PROJECT_XML_DATA 不是有效 ${format}: ${messageOf(error)}(前 80 字符: ${trimmed.slice(0, 80)})`);
150
+ }
151
+ }
152
+ /** xml2js 动态 import:XML 是防御性分支,正常数据不该为它付加载成本。 */
153
+ async function parseXml(raw) {
154
+ const { parseStringPromise } = await import("xml2js");
155
+ return (await parseStringPromise(raw, { explicitArray: false, ignoreAttrs: true, trim: true }));
156
+ }
157
+ /** `YYYY-MM-DD_HH-MM-SS`,本地时区;与存量快照文件名格式一致。 */
158
+ function formatCreateTime(value) {
159
+ const date = value instanceof Date ? value : new Date(String(value ?? ""));
160
+ if (Number.isNaN(date.getTime())) {
161
+ throw new Error(`CREATE_TIME 无效,无法生成快照文件名(收到: ${String(value)})`);
162
+ }
163
+ const pad = (part) => String(part).padStart(2, "0");
164
+ const day = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
165
+ return `${day}_${pad(date.getHours())}-${pad(date.getMinutes())}-${pad(date.getSeconds())}`;
166
+ }
167
+ //# sourceMappingURL=remote-project-provider.js.map
@@ -0,0 +1,11 @@
1
+ import type { SourceRepoBinding, SourceVersion } from "../types.js";
2
+ export interface SourceProvider {
3
+ snapshotVersions(): SourceVersion[];
4
+ }
5
+ /**
6
+ * 源码仓库的 git 版本探针。
7
+ *
8
+ * 只负责在 `run new` 时抓每个仓库的 commit/branch/dirty 写进 `source-versions.json`,
9
+ * 供 `report.md` 头部统一声明版本漂移。源码检索本身交还宿主 Agent(ADR 0002)。
10
+ */
11
+ export declare function createSourceProvider(repos: SourceRepoBinding[]): SourceProvider;
@@ -0,0 +1,33 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ /**
4
+ * 源码仓库的 git 版本探针。
5
+ *
6
+ * 只负责在 `run new` 时抓每个仓库的 commit/branch/dirty 写进 `source-versions.json`,
7
+ * 供 `report.md` 头部统一声明版本漂移。源码检索本身交还宿主 Agent(ADR 0002)。
8
+ */
9
+ export function createSourceProvider(repos) {
10
+ return {
11
+ snapshotVersions() {
12
+ return repos.map(repo => {
13
+ if (!repo.path || !existsSync(repo.path)) {
14
+ return { repoId: repo.id, commit: null, branch: null, dirty: null, reachable: false };
15
+ }
16
+ try {
17
+ const git = (...args) => execFileSync("git", ["-C", repo.path, ...args], { encoding: "utf8" }).trim();
18
+ return {
19
+ repoId: repo.id,
20
+ commit: git("rev-parse", "HEAD"),
21
+ branch: git("rev-parse", "--abbrev-ref", "HEAD"),
22
+ dirty: git("status", "--porcelain").length > 0,
23
+ reachable: true,
24
+ };
25
+ }
26
+ catch {
27
+ return { repoId: repo.id, commit: null, branch: null, dirty: null, reachable: true };
28
+ }
29
+ });
30
+ },
31
+ };
32
+ }
33
+ //# sourceMappingURL=source-provider.js.map
@@ -0,0 +1,13 @@
1
+ import type { AnnotatedEvent, DiagnosticRule, SuspiciousSignal } from "../types.js";
2
+ /**
3
+ * 通用规则引擎。
4
+ *
5
+ * 注意这里的规则不直接判断某个 eventName,而是判断:
6
+ * - 是否是显式错误语义
7
+ * - 是否是状态变更语义
8
+ * - 是否是保存语义
9
+ * - 是否是进入/离开语义
10
+ *
11
+ * 这样规则可以跨事件复用。
12
+ */
13
+ export declare function runDiagnosticRules(timeline: AnnotatedEvent[], rules: DiagnosticRule[]): SuspiciousSignal[];
@@ -0,0 +1,213 @@
1
+ import { describeStateChange, hasStateChange, isEnterEvent, isErrorEvent, isLeaveEvent, isSaveEvent, } from "../semantics/resolver.js";
2
+ /**
3
+ * 通用规则引擎。
4
+ *
5
+ * 注意这里的规则不直接判断某个 eventName,而是判断:
6
+ * - 是否是显式错误语义
7
+ * - 是否是状态变更语义
8
+ * - 是否是保存语义
9
+ * - 是否是进入/离开语义
10
+ *
11
+ * 这样规则可以跨事件复用。
12
+ */
13
+ export function runDiagnosticRules(timeline, rules) {
14
+ const signals = [];
15
+ for (const rule of rules) {
16
+ if (!rule.enabled) {
17
+ continue;
18
+ }
19
+ signals.push(...executeRule(rule, timeline));
20
+ }
21
+ return uniqBy(signals, signal => `${signal.ruleId}:${signal.type}:${signal.logTime}:${signal.eventName}`);
22
+ }
23
+ /**
24
+ * 按 rule.kind 分发到具体规则实现。
25
+ */
26
+ function executeRule(rule, timeline) {
27
+ switch (rule.kind) {
28
+ case "explicit_error":
29
+ return detectExplicitErrorSignals(timeline, rule);
30
+ case "state_change_without_save":
31
+ return detectUnsavedStateChangeSignals(timeline, rule);
32
+ case "reentry_after_leave":
33
+ return detectReentrySignals(timeline, rule);
34
+ case "state_change_save_reentry":
35
+ return detectPersistedStateChangeSignals(timeline, rule);
36
+ default:
37
+ return [];
38
+ }
39
+ }
40
+ /**
41
+ * 规则一:显式报错类问题。
42
+ *
43
+ * 只要事件语义属于 error,或者 payload 明确带 message / stack,
44
+ * 就可以判定为高置信度失败线索。
45
+ */
46
+ function detectExplicitErrorSignals(timeline, rule) {
47
+ const signals = [];
48
+ for (const item of timeline) {
49
+ const message = item.payload.message;
50
+ const stack = item.payload.stack;
51
+ if (!isErrorEvent(item.semantics) && !message && !stack) {
52
+ continue;
53
+ }
54
+ signals.push({
55
+ severity: resolveSeverity(rule, "high"),
56
+ ruleId: rule.id,
57
+ type: "explicit_error_signal",
58
+ eventName: item.eventName,
59
+ logTime: item.logTime,
60
+ reason: (typeof message === "string" ? message : null) ||
61
+ renderReason(rule, {
62
+ eventName: item.eventName,
63
+ }, `事件 ${item.eventName} 具有显式失败语义,建议直接查看错误信息、调用栈和相关接口响应。`),
64
+ });
65
+ }
66
+ return signals;
67
+ }
68
+ /**
69
+ * 规则二:可能修改了状态,但离开前没有看到保存。
70
+ */
71
+ function detectUnsavedStateChangeSignals(timeline, rule) {
72
+ const signals = [];
73
+ let lastStatefulEvent = null;
74
+ for (const item of timeline) {
75
+ if (hasStateChange(item.semantics, { includePossible: rule.includePossibleStateChange, includeCommit: false })) {
76
+ lastStatefulEvent = item;
77
+ continue;
78
+ }
79
+ if (isSaveEvent(item.semantics)) {
80
+ lastStatefulEvent = null;
81
+ continue;
82
+ }
83
+ if (isLeaveEvent(item.semantics) && lastStatefulEvent) {
84
+ signals.push({
85
+ severity: resolveStateChangeSeverity(rule, lastStatefulEvent.semantics.stateChange, "medium"),
86
+ ruleId: rule.id,
87
+ type: "state_change_without_save",
88
+ eventName: item.eventName,
89
+ logTime: item.logTime,
90
+ reason: renderReason(rule, {
91
+ stateChangeLabel: describeStateChange(lastStatefulEvent.semantics),
92
+ lastEventTime: lastStatefulEvent.logTime,
93
+ lastEventName: lastStatefulEvent.eventName,
94
+ }, `最后一个 ${describeStateChange(lastStatefulEvent.semantics)} 是 ${lastStatefulEvent.logTime} 的 ${lastStatefulEvent.eventName},但在离开页面前没有看到保存事件。`),
95
+ });
96
+ lastStatefulEvent = null;
97
+ }
98
+ }
99
+ return signals;
100
+ }
101
+ /**
102
+ * 规则三:离开后快速重进。
103
+ */
104
+ function detectReentrySignals(timeline, rule) {
105
+ const signals = [];
106
+ for (let index = 0; index < timeline.length - 1; index += 1) {
107
+ const current = timeline[index];
108
+ const next = timeline[index + 1];
109
+ if (!isLeaveEvent(current.semantics) || !isEnterEvent(next.semantics)) {
110
+ continue;
111
+ }
112
+ const seconds = Math.max(0, Math.round((next.logTimestamp - current.logTimestamp) / 1000));
113
+ if (rule.maxGapSeconds !== null && seconds > rule.maxGapSeconds) {
114
+ continue;
115
+ }
116
+ signals.push({
117
+ severity: resolveSeverity(rule, "low"),
118
+ ruleId: rule.id,
119
+ type: "reentry_after_leave",
120
+ eventName: `${current.eventName} -> ${next.eventName}`,
121
+ logTime: current.logTime,
122
+ reason: renderReason(rule, {
123
+ gapSeconds: seconds,
124
+ leaveEventName: current.eventName,
125
+ enterEventName: next.eventName,
126
+ }, `用户在离开编辑器 ${seconds} 秒后再次进入,这通常意味着刷新、异常恢复或手动重开。`),
127
+ });
128
+ }
129
+ return signals;
130
+ }
131
+ /**
132
+ * 规则四:状态变更 -> 保存 -> 重进。
133
+ */
134
+ function detectPersistedStateChangeSignals(timeline, rule) {
135
+ const signals = [];
136
+ for (let index = 0; index < timeline.length; index += 1) {
137
+ const current = timeline[index];
138
+ if (!hasStateChange(current.semantics, { includePossible: rule.includePossibleStateChange, includeCommit: false })) {
139
+ continue;
140
+ }
141
+ const laterEvents = timeline.slice(index + 1, index + 1 + rule.lookahead);
142
+ const saveIndex = laterEvents.findIndex(item => isSaveEvent(item.semantics));
143
+ if (saveIndex === -1) {
144
+ continue;
145
+ }
146
+ const reopenExists = laterEvents.slice(saveIndex + 1).some(item => isEnterEvent(item.semantics));
147
+ if (!reopenExists) {
148
+ continue;
149
+ }
150
+ signals.push({
151
+ severity: resolveStateChangeSeverity(rule, current.semantics.stateChange, "medium"),
152
+ ruleId: rule.id,
153
+ type: "state_change_save_reentry",
154
+ eventName: current.eventName,
155
+ logTime: current.logTime,
156
+ reason: renderReason(rule, {
157
+ stateChangeLabel: describeStateChange(current.semantics),
158
+ eventName: current.eventName,
159
+ }, `${describeStateChange(current.semantics)} ${current.eventName} 之后出现了保存,随后又重新进入编辑器。建议对比这个时间点前后的 project 版本,确认状态是否真正落盘。`),
160
+ });
161
+ }
162
+ return signals;
163
+ }
164
+ /**
165
+ * 解析规则严重级别,优先使用规则配置,缺失时回退到调用方默认值。
166
+ */
167
+ function resolveSeverity(rule, fallbackSeverity) {
168
+ return rule.severity || fallbackSeverity;
169
+ }
170
+ /**
171
+ * 根据 stateChange 粒度覆盖严重级别。
172
+ */
173
+ function resolveStateChangeSeverity(rule, stateChange, fallbackSeverity) {
174
+ return rule.severityByStateChange[stateChange] || rule.severity || fallbackSeverity;
175
+ }
176
+ /**
177
+ * 用规则模板渲染原因文案;没有模板时回退到代码内置文案。
178
+ */
179
+ function renderReason(rule, variables, fallbackReason) {
180
+ const template = rule.reasonTemplates.default;
181
+ if (!template) {
182
+ return fallbackReason;
183
+ }
184
+ return formatTemplate(template, variables);
185
+ }
186
+ /**
187
+ * 执行简单的 `{key}` 模板替换。
188
+ */
189
+ function formatTemplate(template, variables) {
190
+ return template.replace(/\{([A-Za-z0-9_]+)\}/g, (_match, key) => {
191
+ if (variables[key] === undefined || variables[key] === null) {
192
+ return "";
193
+ }
194
+ return String(variables[key]);
195
+ });
196
+ }
197
+ /**
198
+ * 按指定 key 去重,并保留第一次出现的项。
199
+ */
200
+ function uniqBy(items, iteratee) {
201
+ const seen = new Set();
202
+ const result = [];
203
+ for (const item of items) {
204
+ const key = iteratee(item);
205
+ if (seen.has(key)) {
206
+ continue;
207
+ }
208
+ seen.add(key);
209
+ result.push(item);
210
+ }
211
+ return result;
212
+ }
213
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+ export declare const diagnosisResultSchema: z.ZodObject<{
3
+ projectId: z.ZodString;
4
+ runId: z.ZodString;
5
+ status: z.ZodEnum<["completed", "failed"]>;
6
+ diagnosisStatus: z.ZodEnum<["confirmed", "likely", "inconclusive", "not_a_bug"]>;
7
+ rootCauses: z.ZodArray<z.ZodObject<{
8
+ description: z.ZodString;
9
+ confidence: z.ZodEnum<["confirmed", "likely", "inconclusive"]>;
10
+ evidenceRefs: z.ZodArray<z.ZodString, "many">;
11
+ }, "strip", z.ZodTypeAny, {
12
+ description: string;
13
+ confidence: "confirmed" | "likely" | "inconclusive";
14
+ evidenceRefs: string[];
15
+ }, {
16
+ description: string;
17
+ confidence: "confirmed" | "likely" | "inconclusive";
18
+ evidenceRefs: string[];
19
+ }>, "many">;
20
+ confirmedFacts: z.ZodArray<z.ZodObject<{
21
+ description: z.ZodString;
22
+ evidenceRefs: z.ZodArray<z.ZodString, "many">;
23
+ }, "strip", z.ZodTypeAny, {
24
+ description: string;
25
+ evidenceRefs: string[];
26
+ }, {
27
+ description: string;
28
+ evidenceRefs: string[];
29
+ }>, "many">;
30
+ hypothesesRejected: z.ZodArray<z.ZodObject<{
31
+ description: z.ZodString;
32
+ reason: z.ZodString;
33
+ }, "strip", z.ZodTypeAny, {
34
+ description: string;
35
+ reason: string;
36
+ }, {
37
+ description: string;
38
+ reason: string;
39
+ }>, "many">;
40
+ missingEvidence: z.ZodArray<z.ZodString, "many">;
41
+ recommendedNextActions: z.ZodArray<z.ZodString, "many">;
42
+ evidenceRefs: z.ZodArray<z.ZodString, "many">;
43
+ }, "strip", z.ZodTypeAny, {
44
+ status: "completed" | "failed";
45
+ projectId: string;
46
+ evidenceRefs: string[];
47
+ runId: string;
48
+ diagnosisStatus: "confirmed" | "likely" | "inconclusive" | "not_a_bug";
49
+ rootCauses: {
50
+ description: string;
51
+ confidence: "confirmed" | "likely" | "inconclusive";
52
+ evidenceRefs: string[];
53
+ }[];
54
+ confirmedFacts: {
55
+ description: string;
56
+ evidenceRefs: string[];
57
+ }[];
58
+ hypothesesRejected: {
59
+ description: string;
60
+ reason: string;
61
+ }[];
62
+ missingEvidence: string[];
63
+ recommendedNextActions: string[];
64
+ }, {
65
+ status: "completed" | "failed";
66
+ projectId: string;
67
+ evidenceRefs: string[];
68
+ runId: string;
69
+ diagnosisStatus: "confirmed" | "likely" | "inconclusive" | "not_a_bug";
70
+ rootCauses: {
71
+ description: string;
72
+ confidence: "confirmed" | "likely" | "inconclusive";
73
+ evidenceRefs: string[];
74
+ }[];
75
+ confirmedFacts: {
76
+ description: string;
77
+ evidenceRefs: string[];
78
+ }[];
79
+ hypothesesRejected: {
80
+ description: string;
81
+ reason: string;
82
+ }[];
83
+ missingEvidence: string[];
84
+ recommendedNextActions: string[];
85
+ }>;
86
+ export type DiagnosisResult = z.infer<typeof diagnosisResultSchema>;
87
+ /**
88
+ * 校验诊断结论。除 schema 外还执行证据纪律:
89
+ * diagnosisStatus 为 confirmed 时,必须至少有一个根因,且 confirmed 级根因与
90
+ * 每条 confirmedFacts 都必须带证据引用。
91
+ *
92
+ * 注意:只校验证据引用非空,不校验证据 ID 的格式与来源真实性(见 ADR 0002)。
93
+ */
94
+ export declare function validateDiagnosisResult(raw: unknown): {
95
+ ok: boolean;
96
+ result?: DiagnosisResult;
97
+ errors?: string[];
98
+ };
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ const confidenceEnum = z.enum(["confirmed", "likely", "inconclusive"]);
3
+ const rootCauseSchema = z.object({
4
+ description: z.string().min(1),
5
+ confidence: confidenceEnum,
6
+ evidenceRefs: z.array(z.string()),
7
+ });
8
+ export const diagnosisResultSchema = z.object({
9
+ projectId: z.string().min(1),
10
+ runId: z.string().min(1),
11
+ status: z.enum(["completed", "failed"]),
12
+ diagnosisStatus: z.enum(["confirmed", "likely", "inconclusive", "not_a_bug"]),
13
+ rootCauses: z.array(rootCauseSchema),
14
+ confirmedFacts: z.array(z.object({ description: z.string().min(1), evidenceRefs: z.array(z.string()) })),
15
+ hypothesesRejected: z.array(z.object({ description: z.string().min(1), reason: z.string() })),
16
+ missingEvidence: z.array(z.string()),
17
+ recommendedNextActions: z.array(z.string()),
18
+ evidenceRefs: z.array(z.string()),
19
+ });
20
+ /**
21
+ * 校验诊断结论。除 schema 外还执行证据纪律:
22
+ * diagnosisStatus 为 confirmed 时,必须至少有一个根因,且 confirmed 级根因与
23
+ * 每条 confirmedFacts 都必须带证据引用。
24
+ *
25
+ * 注意:只校验证据引用非空,不校验证据 ID 的格式与来源真实性(见 ADR 0002)。
26
+ */
27
+ export function validateDiagnosisResult(raw) {
28
+ const parsed = diagnosisResultSchema.safeParse(raw);
29
+ if (!parsed.success) {
30
+ return { ok: false, errors: parsed.error.issues.map(issue => `${issue.path.join(".")}: ${issue.message}`) };
31
+ }
32
+ const result = parsed.data;
33
+ if (result.diagnosisStatus === "confirmed") {
34
+ const errors = [];
35
+ if (result.rootCauses.length === 0) {
36
+ errors.push("confirmed 结论必须至少给出一个根因");
37
+ }
38
+ if (result.rootCauses.some(cause => cause.confidence === "confirmed" && cause.evidenceRefs.length === 0)) {
39
+ errors.push("confirmed 级根因必须提供证据引用(evidenceRefs 不能为空)");
40
+ }
41
+ if (result.confirmedFacts.some(fact => fact.evidenceRefs.length === 0)) {
42
+ errors.push("confirmed 结论的 confirmedFacts 必须逐条提供证据引用");
43
+ }
44
+ if (errors.length > 0) {
45
+ return { ok: false, errors };
46
+ }
47
+ }
48
+ return { ok: true, result };
49
+ }
50
+ //# sourceMappingURL=result-schema.js.map
@@ -0,0 +1,23 @@
1
+ export interface RunInput {
2
+ projectId: string;
3
+ complaint: string;
4
+ }
5
+ export interface RunHandle {
6
+ runId: string;
7
+ runDir: string;
8
+ input: RunInput;
9
+ }
10
+ export declare function createRunDir(runsDir: string, input: RunInput): RunHandle;
11
+ export declare function loadRun(runsDir: string, runId: string): RunHandle;
12
+ export interface TraceEntry {
13
+ actor: "cli" | "agent";
14
+ command: string;
15
+ /** 已脱敏的参数:调用方保证不含 cookie/token/敏感 header。 */
16
+ args: Record<string, unknown>;
17
+ outputSummary: string;
18
+ }
19
+ export declare function appendTrace(runDir: string, entry: TraceEntry): void;
20
+ export declare function writeArtifact(runDir: string, name: string, data: unknown): string;
21
+ export declare function readArtifact<T>(runDir: string, name: string): T;
22
+ export type StageName = "init_run" | "collect_context" | "precompute_evidence" | "investigate" | "finalize";
23
+ export declare function writeStageStatus(runDir: string, stage: StageName, status: "completed" | "failed", detail?: string): void;
@@ -0,0 +1,49 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export function createRunDir(runsDir, input) {
4
+ const now = new Date();
5
+ const stamp = [
6
+ now.getFullYear(),
7
+ String(now.getMonth() + 1).padStart(2, "0"),
8
+ String(now.getDate()).padStart(2, "0"),
9
+ ].join("") + "-" + [
10
+ String(now.getHours()).padStart(2, "0"),
11
+ String(now.getMinutes()).padStart(2, "0"),
12
+ String(now.getSeconds()).padStart(2, "0"),
13
+ ].join("");
14
+ const suffix = Math.random().toString(36).slice(2, 6);
15
+ const runId = `r-${stamp}-${suffix}`;
16
+ const runDir = join(runsDir, runId);
17
+ mkdirSync(join(runDir, "artifacts"), { recursive: true });
18
+ writeFileSync(join(runDir, "input.json"), JSON.stringify({ ...input, createdAt: now.toISOString() }, null, 2));
19
+ return { runId, runDir, input };
20
+ }
21
+ export function loadRun(runsDir, runId) {
22
+ const runDir = join(runsDir, runId);
23
+ if (!existsSync(join(runDir, "input.json"))) {
24
+ throw new Error(`run ${runId} 不存在于 ${runsDir}`);
25
+ }
26
+ const input = JSON.parse(readFileSync(join(runDir, "input.json"), "utf8"));
27
+ return { runId, runDir, input };
28
+ }
29
+ export function appendTrace(runDir, entry) {
30
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...entry });
31
+ appendFileSync(join(runDir, "trace.jsonl"), line + "\n");
32
+ }
33
+ export function writeArtifact(runDir, name, data) {
34
+ const filePath = join(runDir, "artifacts", name);
35
+ writeFileSync(filePath, JSON.stringify(data, null, 2));
36
+ return filePath;
37
+ }
38
+ export function readArtifact(runDir, name) {
39
+ return JSON.parse(readFileSync(join(runDir, "artifacts", name), "utf8"));
40
+ }
41
+ export function writeStageStatus(runDir, stage, status, detail = "") {
42
+ const filePath = join(runDir, "stage-status.json");
43
+ const current = existsSync(filePath)
44
+ ? JSON.parse(readFileSync(filePath, "utf8"))
45
+ : {};
46
+ current[stage] = { status, detail, at: new Date().toISOString() };
47
+ writeFileSync(filePath, JSON.stringify(current, null, 2));
48
+ }
49
+ //# sourceMappingURL=store.js.map