@kunlv/atdc 1.0.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 (222) hide show
  1. package/AGENTS.md +58 -0
  2. package/GO.md +245 -0
  3. package/bin/add-command.py +144 -0
  4. package/bin/atdc.js +460 -0
  5. package/bin/cls.js +98 -0
  6. package/central/link.sh +60 -0
  7. package/central/ticket_center_server/.handoffs/TASK-238389_QYinVipLimit.md +128 -0
  8. package/central/ticket_center_server/.test_plans/TASK-227254-SupportMembership-self-test-plan.md +566 -0
  9. package/central/ticket_center_server/.test_plans/task-238389-qyin-vip-limit-self-test-plan.md +210 -0
  10. package/commands/claude/atd.md +3 -0
  11. package/commands/claude/atdc/1-spec.md +5 -0
  12. package/commands/claude/atdc/1-update.md +5 -0
  13. package/commands/claude/atdc/2-qa.md +3 -0
  14. package/commands/claude/atdc/3-impl-skip-spec.md +5 -0
  15. package/commands/claude/atdc/3-impl.md +3 -0
  16. package/commands/claude/atdc/4-verify.md +3 -0
  17. package/commands/claude/atdc/5-done.md +3 -0
  18. package/commands/claude/atdc/add-command.md +4 -0
  19. package/commands/claude/atdc/add-to-hc.md +3 -0
  20. package/commands/claude/atdc/analysis-cls-log.md +4 -0
  21. package/commands/claude/atdc/article.md +6 -0
  22. package/commands/claude/atdc/bugfix.md +4 -0
  23. package/commands/claude/atdc/code-simplifier.md +8 -0
  24. package/commands/claude/atdc/commit.md +4 -0
  25. package/commands/claude/atdc/compound.md +5 -0
  26. package/commands/claude/atdc/context.md +3 -0
  27. package/commands/claude/atdc/diagnose.md +8 -0
  28. package/commands/claude/atdc/git-guardrails.md +18 -0
  29. package/commands/claude/atdc/grill-with-docs.md +8 -0
  30. package/commands/claude/atdc/handoff.md +8 -0
  31. package/commands/claude/atdc/health.md +3 -0
  32. package/commands/claude/atdc/learn.md +4 -0
  33. package/commands/claude/atdc/review.md +10 -0
  34. package/commands/claude/atdc/search.md +4 -0
  35. package/commands/claude/atdc/strategy.md +3 -0
  36. package/commands/claude/atdc/table2xlsx.md +4 -0
  37. package/commands/claude/atdc/test-plan.md +5 -0
  38. package/commands/claude/atdc/travel-planner.md +4 -0
  39. package/commands/claude/atdc/xlsx2sql-insert.md +4 -0
  40. package/commands/claude/atdc/xlsx2sql-update.md +4 -0
  41. package/commands/claude/atdc/zoom-out.md +8 -0
  42. package/commands/codex/atdc/SKILL.md +8 -0
  43. package/commands/codex/atdc-1-spec/SKILL.md +8 -0
  44. package/commands/codex/atdc-1-update/SKILL.md +8 -0
  45. package/commands/codex/atdc-2-qa/SKILL.md +8 -0
  46. package/commands/codex/atdc-3-impl/SKILL.md +8 -0
  47. package/commands/codex/atdc-3-impl-skip-spec/SKILL.md +8 -0
  48. package/commands/codex/atdc-4-verify/SKILL.md +8 -0
  49. package/commands/codex/atdc-5-done/SKILL.md +8 -0
  50. package/commands/codex/atdc-add-command/SKILL.md +7 -0
  51. package/commands/codex/atdc-add-to-hc/SKILL.md +8 -0
  52. package/commands/codex/atdc-analysis-cls-log/SKILL.md +5 -0
  53. package/commands/codex/atdc-article/SKILL.md +8 -0
  54. package/commands/codex/atdc-bugfix/SKILL.md +5 -0
  55. package/commands/codex/atdc-code-simplifier/SKILL.md +7 -0
  56. package/commands/codex/atdc-commit/SKILL.md +7 -0
  57. package/commands/codex/atdc-compound/SKILL.md +8 -0
  58. package/commands/codex/atdc-context/SKILL.md +8 -0
  59. package/commands/codex/atdc-diagnose/SKILL.md +9 -0
  60. package/commands/codex/atdc-git-guardrails/SKILL.md +19 -0
  61. package/commands/codex/atdc-grill-with-docs/SKILL.md +9 -0
  62. package/commands/codex/atdc-handoff/SKILL.md +9 -0
  63. package/commands/codex/atdc-health/SKILL.md +8 -0
  64. package/commands/codex/atdc-learn/SKILL.md +7 -0
  65. package/commands/codex/atdc-review/SKILL.md +49 -0
  66. package/commands/codex/atdc-search/SKILL.md +5 -0
  67. package/commands/codex/atdc-strategy/SKILL.md +8 -0
  68. package/commands/codex/atdc-table2xlsx/SKILL.md +8 -0
  69. package/commands/codex/atdc-test-plan/SKILL.md +13 -0
  70. package/commands/codex/atdc-travel-planner/SKILL.md +7 -0
  71. package/commands/codex/atdc-xlsx2sql-insert/SKILL.md +8 -0
  72. package/commands/codex/atdc-xlsx2sql-update/SKILL.md +8 -0
  73. package/commands/codex/atdc-zoom-out/SKILL.md +9 -0
  74. package/commands/cursor/atdc/SKILL.md +8 -0
  75. package/commands/cursor/atdc-1-spec/SKILL.md +8 -0
  76. package/commands/cursor/atdc-1-update/SKILL.md +8 -0
  77. package/commands/cursor/atdc-2-qa/SKILL.md +8 -0
  78. package/commands/cursor/atdc-3-impl/SKILL.md +8 -0
  79. package/commands/cursor/atdc-3-impl-skip-spec/SKILL.md +8 -0
  80. package/commands/cursor/atdc-4-verify/SKILL.md +8 -0
  81. package/commands/cursor/atdc-5-done/SKILL.md +8 -0
  82. package/commands/cursor/atdc-add-command/SKILL.md +7 -0
  83. package/commands/cursor/atdc-add-to-hc/SKILL.md +8 -0
  84. package/commands/cursor/atdc-analysis-cls-log/SKILL.md +5 -0
  85. package/commands/cursor/atdc-article/SKILL.md +8 -0
  86. package/commands/cursor/atdc-bugfix/SKILL.md +5 -0
  87. package/commands/cursor/atdc-code-simplifier/SKILL.md +7 -0
  88. package/commands/cursor/atdc-commit/SKILL.md +7 -0
  89. package/commands/cursor/atdc-compound/SKILL.md +8 -0
  90. package/commands/cursor/atdc-context/SKILL.md +8 -0
  91. package/commands/cursor/atdc-diagnose/SKILL.md +8 -0
  92. package/commands/cursor/atdc-git-guardrails/SKILL.md +18 -0
  93. package/commands/cursor/atdc-grill-with-docs/SKILL.md +8 -0
  94. package/commands/cursor/atdc-handoff/SKILL.md +8 -0
  95. package/commands/cursor/atdc-health/SKILL.md +8 -0
  96. package/commands/cursor/atdc-learn/SKILL.md +7 -0
  97. package/commands/cursor/atdc-review/SKILL.md +49 -0
  98. package/commands/cursor/atdc-search/SKILL.md +5 -0
  99. package/commands/cursor/atdc-strategy/SKILL.md +8 -0
  100. package/commands/cursor/atdc-table2xlsx/SKILL.md +8 -0
  101. package/commands/cursor/atdc-test-plan/SKILL.md +7 -0
  102. package/commands/cursor/atdc-travel-planner/SKILL.md +7 -0
  103. package/commands/cursor/atdc-xlsx2sql-insert/SKILL.md +8 -0
  104. package/commands/cursor/atdc-xlsx2sql-update/SKILL.md +8 -0
  105. package/commands/cursor/atdc-zoom-out/SKILL.md +8 -0
  106. package/commands/opencode/atd.md +6 -0
  107. package/commands/opencode/atdc/1-spec.md +8 -0
  108. package/commands/opencode/atdc/1-update.md +8 -0
  109. package/commands/opencode/atdc/2-qa.md +6 -0
  110. package/commands/opencode/atdc/3-impl-skip-spec.md +8 -0
  111. package/commands/opencode/atdc/3-impl.md +6 -0
  112. package/commands/opencode/atdc/4-verify.md +6 -0
  113. package/commands/opencode/atdc/5-done.md +6 -0
  114. package/commands/opencode/atdc/add-command.md +6 -0
  115. package/commands/opencode/atdc/add-to-hc.md +6 -0
  116. package/commands/opencode/atdc/analysis-cls-log.md +6 -0
  117. package/commands/opencode/atdc/article.md +6 -0
  118. package/commands/opencode/atdc/bugfix.md +6 -0
  119. package/commands/opencode/atdc/code-simplifier.md +8 -0
  120. package/commands/opencode/atdc/commit.md +6 -0
  121. package/commands/opencode/atdc/compound.md +8 -0
  122. package/commands/opencode/atdc/context.md +6 -0
  123. package/commands/opencode/atdc/diagnose.md +8 -0
  124. package/commands/opencode/atdc/git-guardrails.md +18 -0
  125. package/commands/opencode/atdc/grill-with-docs.md +8 -0
  126. package/commands/opencode/atdc/handoff.md +8 -0
  127. package/commands/opencode/atdc/health.md +6 -0
  128. package/commands/opencode/atdc/learn.md +6 -0
  129. package/commands/opencode/atdc/review.md +7 -0
  130. package/commands/opencode/atdc/search.md +6 -0
  131. package/commands/opencode/atdc/strategy.md +6 -0
  132. package/commands/opencode/atdc/table2xlsx.md +6 -0
  133. package/commands/opencode/atdc/test-plan.md +6 -0
  134. package/commands/opencode/atdc/travel-planner.md +6 -0
  135. package/commands/opencode/atdc/xlsx2sql-insert.md +6 -0
  136. package/commands/opencode/atdc/xlsx2sql-update.md +6 -0
  137. package/commands/opencode/atdc/zoom-out.md +8 -0
  138. package/config/cls-defaults.json +19 -0
  139. package/hooks/atd-archive.sh +86 -0
  140. package/hooks/atd-state.sh +165 -0
  141. package/hooks/evidence-hook.sh +88 -0
  142. package/hooks/evidence-log.sh +54 -0
  143. package/hooks/guardrails.sh +65 -0
  144. package/hooks/handoff-compress.sh +104 -0
  145. package/hooks/phase-guard.sh +134 -0
  146. package/hooks/phase-write-guard.sh +96 -0
  147. package/hooks/ponytail-fix.sh +179 -0
  148. package/hooks/ponytail-guard.sh +66 -0
  149. package/hooks/ponytail-scan.sh +62 -0
  150. package/install.sh +276 -0
  151. package/package.json +20 -0
  152. package/packages/cls/bin/cls.js +146 -0
  153. package/packages/cls/config/defaults.json +17 -0
  154. package/packages/cls/mcp/server.js +142 -0
  155. package/packages/cls/package.json +29 -0
  156. package/packages/cls/scripts/postinstall.js +19 -0
  157. package/packages/cls/src/client.js +262 -0
  158. package/prompts/1-spec.md +271 -0
  159. package/prompts/1-update.md +198 -0
  160. package/prompts/2-qa.md +164 -0
  161. package/prompts/3-impl-bugfix.md +271 -0
  162. package/prompts/3-impl.md +419 -0
  163. package/prompts/4-verify.md +160 -0
  164. package/prompts/5-done.md +227 -0
  165. package/prompts/6-article.md +571 -0
  166. package/prompts/X-add-command.md +60 -0
  167. package/prompts/X-add-to-hc.md +178 -0
  168. package/prompts/X-analysis-cls-log.md +68 -0
  169. package/prompts/X-bugfix.md +10 -0
  170. package/prompts/X-code-simplifier.md +38 -0
  171. package/prompts/X-commit.md +116 -0
  172. package/prompts/X-compound.md +188 -0
  173. package/prompts/X-context-scan.md +101 -0
  174. package/prompts/X-diagnose.md +116 -0
  175. package/prompts/X-grill-with-docs.md +120 -0
  176. package/prompts/X-handoff.md +80 -0
  177. package/prompts/X-health.md +78 -0
  178. package/prompts/X-learn.md +151 -0
  179. package/prompts/X-review.md +417 -0
  180. package/prompts/X-search.md +74 -0
  181. package/prompts/X-strategy.md +95 -0
  182. package/prompts/X-table2xlsx.md +46 -0
  183. package/prompts/X-test-plan.md +436 -0
  184. package/prompts/X-travel-planner.md +22 -0
  185. package/prompts/X-xlsx2sql-insert.md +34 -0
  186. package/prompts/X-xlsx2sql-update.md +40 -0
  187. package/prompts/X-zoom-out.md +119 -0
  188. package/reference/diagnose-loop.md +66 -0
  189. package/reference/gates.md +90 -0
  190. package/reference/git-guardrails.md +71 -0
  191. package/reference/guards.md +275 -0
  192. package/reference/learn.md +100 -0
  193. package/reference/memory.md +190 -0
  194. package/reference/risk.md +74 -0
  195. package/reference/tdd.md +40 -0
  196. package/scripts/svg2png.js +46 -0
  197. package/src/cls/client.js +244 -0
  198. package/src/constants.js +49 -0
  199. package/src/evidence/ai-draft-metrics.js +157 -0
  200. package/src/evidence/canvas.js +184 -0
  201. package/src/evidence/index.js +98 -0
  202. package/src/evidence/sink-events.js +165 -0
  203. package/src/evidence/stage-timing.js +112 -0
  204. package/src/installers/claude.js +79 -0
  205. package/src/installers/codex.js +62 -0
  206. package/src/installers/opencode.js +72 -0
  207. package/src/manifest/ponytail-patterns.json +185 -0
  208. package/src/mcp/cls-mcp-server.js +142 -0
  209. package/src/utils.js +79 -0
  210. package/src/validators/state-validate.js +123 -0
  211. package/templates/.phase-state.json +13 -0
  212. package/templates/adr.md +62 -0
  213. package/templates/changelog.md +21 -0
  214. package/templates/concepts.md +35 -0
  215. package/templates/design.md +47 -0
  216. package/templates/handoff.md +64 -0
  217. package/templates/learn-skill.md +43 -0
  218. package/templates/progress.json +17 -0
  219. package/templates/proposal.md +29 -0
  220. package/templates/solution.md +36 -0
  221. package/templates/strategy.md +39 -0
  222. package/templates/tasks.md +41 -0
@@ -0,0 +1,157 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ function resolveMetricsDir(changeDir) {
6
+ const dir = path.join(changeDir, '.evidence', 'metrics');
7
+ fs.mkdirSync(dir, { recursive: true });
8
+ return dir;
9
+ }
10
+
11
+ function metricsFile(changeDir) {
12
+ return path.join(resolveMetricsDir(changeDir), 'ai-draft-metrics.json');
13
+ }
14
+
15
+ function captureDraftSnapshot(changeDir, label = 'pre-review') {
16
+ let diff;
17
+ try {
18
+ diff = execSync('git diff --unified=0 HEAD', { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
19
+ } catch {
20
+ diff = '';
21
+ }
22
+
23
+ if (!diff) {
24
+ return { label, totalLines: 0, aiLines: 0, adoptionRate: null, note: 'no unstaged changes' };
25
+ }
26
+
27
+ const lines = diff.split('\n');
28
+ let addedLines = 0;
29
+ let removedLines = 0;
30
+
31
+ for (const line of lines) {
32
+ if (line.startsWith('+') && !line.startsWith('+++')) addedLines++;
33
+ if (line.startsWith('-') && !line.startsWith('---')) removedLines++;
34
+ }
35
+
36
+ const totalChanged = addedLines + removedLines;
37
+
38
+ const snapshot = {
39
+ label,
40
+ timestamp: new Date().toISOString(),
41
+ addedLines,
42
+ removedLines,
43
+ totalChanged,
44
+ };
45
+
46
+ const fp = path.join(resolveMetricsDir(changeDir), `snapshot_${label}_${snapshot.timestamp.replace(/[:.]/g, '-')}.json`);
47
+ fs.writeFileSync(fp, JSON.stringify(snapshot, null, 2) + '\n');
48
+
49
+ storeMetric(changeDir, 'draft_snapshot', snapshot);
50
+
51
+ return snapshot;
52
+ }
53
+
54
+ function captureCommitDiff(changeDir, label = 'commit') {
55
+ let diff;
56
+ try {
57
+ diff = execSync('git diff --unified=0 HEAD~1 HEAD', { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
58
+ } catch {
59
+ try {
60
+ diff = execSync('git diff --unified=0 --cached', { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
61
+ } catch {
62
+ diff = '';
63
+ }
64
+ }
65
+
66
+ if (!diff) {
67
+ return { label, committedLines: 0, note: 'no commits to compare' };
68
+ }
69
+
70
+ const lines = diff.split('\n');
71
+ let addedLines = 0;
72
+ let removedLines = 0;
73
+
74
+ for (const line of lines) {
75
+ if (line.startsWith('+') && !line.startsWith('+++')) addedLines++;
76
+ if (line.startsWith('-') && !line.startsWith('---')) removedLines++;
77
+ }
78
+
79
+ const result = {
80
+ label,
81
+ timestamp: new Date().toISOString(),
82
+ committedAdded: addedLines,
83
+ committedRemoved: removedLines,
84
+ };
85
+
86
+ storeMetric(changeDir, 'commit_diff', result);
87
+ return result;
88
+ }
89
+
90
+ function computeAdoptionRate(changeDir) {
91
+ const snapshots = fs.readdirSync(resolveMetricsDir(changeDir))
92
+ .filter(f => f.startsWith('snapshot_'))
93
+ .sort();
94
+
95
+ const commits = fs.readdirSync(resolveMetricsDir(changeDir))
96
+ .filter(f => f.startsWith('commit_') || f.includes('commit_diff'))
97
+ .sort();
98
+
99
+ if (snapshots.length === 0) {
100
+ return { adoptionRate: null, note: 'no snapshots recorded. Run captureDraftSnapshot first.' };
101
+ }
102
+
103
+ const firstSnapshot = JSON.parse(
104
+ fs.readFileSync(path.join(resolveMetricsDir(changeDir), snapshots[0]), 'utf8')
105
+ );
106
+ const lastFile = snapshots[snapshots.length - 1];
107
+ const lastSnapshot = JSON.parse(
108
+ fs.readFileSync(path.join(resolveMetricsDir(changeDir), lastFile), 'utf8')
109
+ );
110
+
111
+ const result = {
112
+ firstDraft: firstSnapshot.addedLines,
113
+ finalDraft: lastSnapshot.addedLines,
114
+ netChange: lastSnapshot.addedLines - firstSnapshot.addedLines,
115
+ snapshots: snapshots.length,
116
+ note: 'adoption rate = committedLines / firstDraftLines. Requires commit data for full picture.',
117
+ };
118
+
119
+ storeMetric(changeDir, 'adoption_rate', result);
120
+ return result;
121
+ }
122
+
123
+ function storeMetric(changeDir, metricType, data) {
124
+ const all = loadAllMetrics(changeDir);
125
+ if (!all[metricType]) all[metricType] = [];
126
+ all[metricType].push(data);
127
+ fs.writeFileSync(metricsFile(changeDir), JSON.stringify(all, null, 2) + '\n');
128
+ }
129
+
130
+ function loadAllMetrics(changeDir) {
131
+ const fp = metricsFile(changeDir);
132
+ if (!fs.existsSync(fp)) return {};
133
+ return JSON.parse(fs.readFileSync(fp, 'utf8'));
134
+ }
135
+
136
+ function metricsReport(changeDir) {
137
+ const metrics = loadAllMetrics(changeDir);
138
+ const report = {};
139
+
140
+ for (const [type, entries] of Object.entries(metrics)) {
141
+ report[type] = {
142
+ count: entries.length,
143
+ latest: entries[entries.length - 1],
144
+ };
145
+ }
146
+
147
+ return report;
148
+ }
149
+
150
+ module.exports = {
151
+ captureDraftSnapshot,
152
+ captureCommitDiff,
153
+ computeAdoptionRate,
154
+ storeMetric,
155
+ loadAllMetrics,
156
+ metricsReport,
157
+ };
@@ -0,0 +1,184 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { eventStats } = require('./sink-events');
4
+ const { stageReport, formatDuration } = require('./stage-timing');
5
+ const { metricsReport } = require('./ai-draft-metrics');
6
+
7
+ function readPhaseState(changeDir) {
8
+ const fp = path.join(changeDir, '.phase-state.json');
9
+ if (!fs.existsSync(fp)) return null;
10
+ return JSON.parse(fs.readFileSync(fp, 'utf8'));
11
+ }
12
+
13
+ function readProgress(changeDir) {
14
+ const fp = path.join(changeDir, '.progress.json');
15
+ if (!fs.existsSync(fp)) return null;
16
+ return JSON.parse(fs.readFileSync(fp, 'utf8'));
17
+ }
18
+
19
+ function generateDashboard(changeDir, opts = {}) {
20
+ const phaseState = readPhaseState(changeDir);
21
+ const progress = readProgress(changeDir);
22
+ const events = eventStats(changeDir);
23
+ const timing = stageReport(changeDir);
24
+ const metrics = metricsReport(changeDir);
25
+
26
+ const change = phaseState ? phaseState.change : path.basename(changeDir);
27
+ const phase = phaseState ? phaseState.phase : 'unknown';
28
+ const mode = phaseState ? phaseState.mode : 'unknown';
29
+
30
+ const eventTypeRows = Object.entries(events.byType)
31
+ .map(([type, count]) => `<tr><td>${type}</td><td>${count}</td></tr>`)
32
+ .join('\n');
33
+
34
+ const stageRows = timing.stages.map(s => `
35
+ <tr>
36
+ <td>${s.stage}</td>
37
+ <td>${s.duration || 'N/A'}</td>
38
+ <td>${s.completedAt ? '✓' : '○'}</td>
39
+ </tr>
40
+ `).join('\n');
41
+
42
+ const gateResults = progress && progress.gateResults ? Object.entries(progress.gateResults)
43
+ .map(([gate, result]) => {
44
+ const icon = result === 'passed' ? '✓' : result === 'failed' ? '✗' : '○';
45
+ return `<span class="gate ${result}">${icon} ${gate}</span>`;
46
+ }).join(' ') : '<span class="gate">no gate data</span>';
47
+
48
+ const taskInfo = progress
49
+ ? `<span>已完成 ${progress.completedTasks?.length || 0} 项</span>`
50
+ : '<span>无任务数据</span>';
51
+
52
+ return `<!DOCTYPE html>
53
+ <html lang="zh-CN">
54
+ <head>
55
+ <meta charset="UTF-8">
56
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
57
+ <title>ATD Canvas — ${change}</title>
58
+ <style>
59
+ * { margin: 0; padding: 0; box-sizing: border-box; }
60
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #c9d1d9; padding: 24px; }
61
+ .header { margin-bottom: 24px; }
62
+ .header h1 { font-size: 24px; color: #58a6ff; }
63
+ .header .meta { color: #8b949e; font-size: 14px; margin-top: 4px; }
64
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
65
+ .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
66
+ .card h2 { font-size: 14px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; border-bottom: 1px solid #21262d; padding-bottom: 8px; }
67
+ .stat { display: flex; justify-content: space-between; align-items: center; padding: 4px 0; }
68
+ .stat .label { color: #8b949e; font-size: 13px; }
69
+ .stat .value { font-size: 18px; font-weight: 600; }
70
+ .stat .value.green { color: #3fb950; }
71
+ .stat .value.yellow { color: #d29922; }
72
+ .stat .value.red { color: #f85149; }
73
+ .stat .value.blue { color: #58a6ff; }
74
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
75
+ th, td { text-align: left; padding: 6px 4px; border-bottom: 1px solid #21262d; }
76
+ th { color: #8b949e; font-weight: 500; }
77
+ .timeline { font-size: 12px; }
78
+ .timeline .entry { padding: 3px 0; border-left: 2px solid #30363d; padding-left: 10px; margin-left: 4px; }
79
+ .timeline .entry .ts { color: #484f58; font-size: 11px; }
80
+ .gate { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; margin: 2px; }
81
+ .gate.passed { background: #1b3826; color: #3fb950; }
82
+ .gate.failed { background: #3d1f1f; color: #f85149; }
83
+ .gate.null { background: #21262d; color: #8b949e; }
84
+ .phase-badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 14px; font-weight: 600; }
85
+ .phase-badge.open { background: #1b3826; color: #3fb950; }
86
+ .phase-badge.design { background: #1a2e3f; color: #58a6ff; }
87
+ .phase-badge.build { background: #3d2e00; color: #d29922; }
88
+ .phase-badge.verify { background: #2b1a3d; color: #bc8cff; }
89
+ .phase-badge.archive { background: #21262d; color: #8b949e; }
90
+ footer { margin-top: 24px; color: #484f58; font-size: 11px; text-align: center; }
91
+ </style>
92
+ </head>
93
+ <body>
94
+ <div class="header">
95
+ <h1>ATD Canvas</h1>
96
+ <div class="meta">
97
+ Change: <strong>${change}</strong> &middot;
98
+ Mode: ${mode} &middot;
99
+ Updated: ${phaseState?.updatedAt || 'N/A'}
100
+ </div>
101
+ </div>
102
+
103
+ <div class="grid">
104
+ <div class="card">
105
+ <h2>当前状态</h2>
106
+ <div class="stat">
107
+ <span class="label">阶段</span>
108
+ <span class="phase-badge ${phase}">${phase}</span>
109
+ </div>
110
+ <div class="stat">
111
+ <span class="label">事件总数</span>
112
+ <span class="value blue">${events.total}</span>
113
+ </div>
114
+ <div class="stat">
115
+ <span class="label">总耗时</span>
116
+ <span class="value">${timing.totalElapsed || 'N/A'}</span>
117
+ </div>
118
+ </div>
119
+
120
+ <div class="card">
121
+ <h2>质量门禁</h2>
122
+ ${gateResults}
123
+ <div style="margin-top: 8px;">${taskInfo}</div>
124
+ </div>
125
+
126
+ <div class="card">
127
+ <h2>阶段耗时</h2>
128
+ <table>
129
+ <tr><th>阶段</th><th>耗时</th><th>完成</th></tr>
130
+ ${stageRows || '<tr><td colspan="3">无阶段数据</td></tr>'}
131
+ </table>
132
+ </div>
133
+
134
+ <div class="card">
135
+ <h2>事件类型分布</h2>
136
+ <table>
137
+ <tr><th>类型</th><th>数量</th></tr>
138
+ ${eventTypeRows || '<tr><td colspan="2">无事件数据</td></tr>'}
139
+ </table>
140
+ </div>
141
+
142
+ <div class="card" style="grid-column: span 2;">
143
+ <h2>事件时间线</h2>
144
+ <div class="timeline">
145
+ ${events.timeline.slice(-20).map(e => `
146
+ <div class="entry">
147
+ <span class="ts">${e.timestamp?.slice(11, 19) || '--:--:--'}</span>
148
+ <strong>${e.type}</strong>
149
+ <span style="color: #8b949e">${e.phase ? ' · ' + e.phase : ''}</span>
150
+ </div>
151
+ `).join('\n')}
152
+ </div>
153
+ </div>
154
+
155
+ ${Object.keys(metrics).length > 0 ? `
156
+ <div class="card">
157
+ <h2>AI Draft Metrics</h2>
158
+ ${Object.entries(metrics).map(([type, info]) => `
159
+ <div class="stat">
160
+ <span class="label">${type}</span>
161
+ <span class="value blue">${info.count} entries</span>
162
+ </div>
163
+ `).join('\n')}
164
+ </div>
165
+ ` : ''}
166
+ </div>
167
+
168
+ <footer>
169
+ ATD Studio Evidence Canvas · Generated ${new Date().toISOString()}
170
+ </footer>
171
+ </body>
172
+ </html>`;
173
+ }
174
+
175
+ function writeCanvas(changeDir, outPath) {
176
+ const html = generateDashboard(changeDir);
177
+ const dest = outPath || path.join(changeDir, '.evidence', 'canvas.html');
178
+ const dir = path.dirname(dest);
179
+ fs.mkdirSync(dir, { recursive: true });
180
+ fs.writeFileSync(dest, html);
181
+ return dest;
182
+ }
183
+
184
+ module.exports = { generateDashboard, writeCanvas };
@@ -0,0 +1,98 @@
1
+ const { writeEvent, listEvents, eventStats, latestPhase, centralEvidenceRoot } = require('./sink-events');
2
+ const { stageStart, stageComplete, getStageDuration, formatDuration, totalElapsed, stageReport } = require('./stage-timing');
3
+ const { captureDraftSnapshot, captureCommitDiff, computeAdoptionRate, metricsReport } = require('./ai-draft-metrics');
4
+ const { generateDashboard, writeCanvas } = require('./canvas');
5
+
6
+ const path = require('path');
7
+
8
+ function resolveChangeDir(changeName) {
9
+ return path.resolve(process.cwd(), 'openspec', 'changes', changeName);
10
+ }
11
+
12
+ function allEvidence(changeName) {
13
+ const dir = resolveChangeDir(changeName);
14
+ return {
15
+ events: eventStats(dir),
16
+ timing: stageReport(dir),
17
+ metrics: metricsReport(dir),
18
+ };
19
+ }
20
+
21
+ function recordPhaseTransition(changeName, from, to, opts = {}) {
22
+ const dir = resolveChangeDir(changeName);
23
+
24
+ stageComplete(dir, from, { reason: `transition to ${to}` });
25
+ stageStart(dir, to, { reason: `transition from ${from}` });
26
+
27
+ writeEvent(dir, 'phase_transition', { from, to }, {
28
+ change: changeName,
29
+ phase: to,
30
+ actorType: opts.actorType || 'agent',
31
+ });
32
+ }
33
+
34
+ function recordIntake(changeName, opts = {}) {
35
+ const dir = resolveChangeDir(changeName);
36
+ stageStart(dir, 'open', { reason: 'intake' });
37
+
38
+ writeEvent(dir, 'intake', {
39
+ mode: opts.mode || 'full',
40
+ risk_level: opts.riskLevel || 'low',
41
+ }, {
42
+ change: changeName,
43
+ phase: 'open',
44
+ actorType: opts.actorType || 'agent',
45
+ });
46
+ }
47
+
48
+ function recordGateCheck(changeName, gate, result, details = {}) {
49
+ const dir = resolveChangeDir(changeName);
50
+ writeEvent(dir, 'gate_check', { gate, result, ...details }, {
51
+ change: changeName,
52
+ phase: 'build',
53
+ });
54
+ }
55
+
56
+ function recordVerification(changeName, result, details = {}) {
57
+ const dir = resolveChangeDir(changeName);
58
+ writeEvent(dir, 'verification', { result, ...details }, {
59
+ change: changeName,
60
+ phase: 'verify',
61
+ });
62
+ }
63
+
64
+ function recordDone(changeName, details = {}) {
65
+ const dir = resolveChangeDir(changeName);
66
+ stageComplete(dir, 'verify', { reason: 'done' });
67
+
68
+ writeEvent(dir, 'done', details, {
69
+ change: changeName,
70
+ phase: 'archive',
71
+ });
72
+ }
73
+
74
+ module.exports = {
75
+ writeEvent,
76
+ listEvents,
77
+ eventStats,
78
+ latestPhase,
79
+ stageStart,
80
+ stageComplete,
81
+ getStageDuration,
82
+ formatDuration,
83
+ totalElapsed,
84
+ stageReport,
85
+ captureDraftSnapshot,
86
+ captureCommitDiff,
87
+ computeAdoptionRate,
88
+ metricsReport,
89
+ generateDashboard,
90
+ writeCanvas,
91
+ allEvidence,
92
+ resolveChangeDir,
93
+ recordPhaseTransition,
94
+ recordIntake,
95
+ recordGateCheck,
96
+ recordVerification,
97
+ recordDone,
98
+ };
@@ -0,0 +1,165 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const os = require('os');
5
+
6
+ const SCHEMA_VERSION = 'atd.evidence.event.v1';
7
+
8
+ const VALID_EVENT_TYPES = new Set([
9
+ 'intake',
10
+ 'phase_transition',
11
+ 'spec_approve',
12
+ 'plan_approve',
13
+ 'task_start',
14
+ 'task_complete',
15
+ 'task_fail',
16
+ 'gate_check',
17
+ 'verification',
18
+ 'done',
19
+ 'guard_block',
20
+ 'agent_action',
21
+ 'risk_assessment',
22
+ 'context_basis',
23
+ 'review_gate',
24
+ 'completion_gate',
25
+ 'stage_timing',
26
+ 'ai_draft_metric',
27
+ ]);
28
+
29
+ function eventId() {
30
+ return crypto.randomUUID();
31
+ }
32
+
33
+ function timestamp() {
34
+ return new Date().toISOString();
35
+ }
36
+
37
+ function _repoName() {
38
+ try {
39
+ const { execSync } = require('child_process');
40
+ const remote = execSync('git remote get-url origin 2>/dev/null || echo ""', { encoding: 'utf8' }).trim();
41
+ if (remote) return remote.replace(/.*\//, '').replace(/\.git$/, '');
42
+ const toplevel = execSync('git rev-parse --show-toplevel 2>/dev/null || echo ""', { encoding: 'utf8' }).trim();
43
+ if (toplevel) return path.basename(toplevel);
44
+ } catch {}
45
+ return path.basename(process.cwd());
46
+ }
47
+
48
+ function centralEvidenceRoot() {
49
+ return path.join(os.homedir(), 'atdc', 'evidence', _repoName());
50
+ }
51
+
52
+ function resolveEvidenceDir(changeDir) {
53
+ const changeName = path.basename(changeDir);
54
+ const central = path.join(centralEvidenceRoot(), 'changes', changeName);
55
+ const eventsDir = path.join(central, 'events');
56
+ fs.mkdirSync(eventsDir, { recursive: true });
57
+
58
+ const localLink = path.join(changeDir, '.evidence');
59
+ if (!fs.existsSync(localLink)) {
60
+ try { fs.symlinkSync(central, localLink, 'dir'); } catch (e) { if (e.code !== 'EEXIST') throw e; }
61
+ } else if (!fs.lstatSync(localLink).isSymbolicLink()) {
62
+ // migrate existing real dir
63
+ try {
64
+ const { execSync } = require('child_process');
65
+ execSync(`cp -rn "${localLink}/" "${central}/" 2>/dev/null || true`);
66
+ fs.rmSync(localLink, { recursive: true, force: true });
67
+ fs.symlinkSync(central, localLink, 'dir');
68
+ } catch {}
69
+ }
70
+ return eventsDir;
71
+ }
72
+
73
+ function writeEvent(changeDir, eventType, payload = {}, opts = {}) {
74
+ if (!VALID_EVENT_TYPES.has(eventType)) {
75
+ throw new Error(`Invalid event type: ${eventType}. Valid: ${[...VALID_EVENT_TYPES].join(', ')}`);
76
+ }
77
+
78
+ const dir = resolveEvidenceDir(changeDir);
79
+ const ts = timestamp();
80
+ const id = eventId();
81
+ const safeType = eventType.replace(/[^a-z0-9_]/g, '_');
82
+
83
+ const event = {
84
+ schema: SCHEMA_VERSION,
85
+ event_id: id,
86
+ event_type: eventType,
87
+ timestamp: ts,
88
+ change: opts.change || path.basename(changeDir),
89
+ phase: opts.phase || 'unknown',
90
+ actor: {
91
+ type: opts.actorType || 'agent',
92
+ identity: opts.actorIdentity || (process.env.USER || 'unknown'),
93
+ },
94
+ payload,
95
+ };
96
+
97
+ const filename = `${ts.replace(/[:.]/g, '-')}_${safeType}_${id.slice(0, 8)}.json`;
98
+ const filepath = path.join(dir, filename);
99
+
100
+ fs.writeFileSync(filepath, JSON.stringify(event, null, 2) + '\n');
101
+ return { event, filepath };
102
+ }
103
+
104
+ function listEvents(changeDir, opts = {}) {
105
+ const dir = path.join(changeDir, '.evidence', 'events');
106
+ if (!fs.existsSync(dir)) return [];
107
+
108
+ let files = fs.readdirSync(dir)
109
+ .filter(f => f.endsWith('.json'))
110
+ .sort();
111
+
112
+ if (opts.type) {
113
+ files = files.filter(f => f.includes(opts.type));
114
+ }
115
+ if (opts.limit && opts.limit > 0) {
116
+ files = files.slice(-opts.limit);
117
+ }
118
+
119
+ return files.map(f => {
120
+ const data = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
121
+ return { file: f, event: data };
122
+ });
123
+ }
124
+
125
+ function eventStats(changeDir) {
126
+ const events = listEvents(changeDir);
127
+ const byType = {};
128
+ const timeline = [];
129
+
130
+ for (const { event } of events) {
131
+ const t = event.event_type;
132
+ byType[t] = (byType[t] || 0) + 1;
133
+ timeline.push({
134
+ type: t,
135
+ timestamp: event.timestamp,
136
+ phase: event.phase,
137
+ });
138
+ }
139
+
140
+ return {
141
+ total: events.length,
142
+ byType,
143
+ timeline,
144
+ firstEvent: timeline[0] || null,
145
+ lastEvent: timeline[timeline.length - 1] || null,
146
+ };
147
+ }
148
+
149
+ function latestPhase(changeDir) {
150
+ const events = listEvents(changeDir, { type: 'phase_transition' });
151
+ if (events.length === 0) return null;
152
+ return events[events.length - 1].event.payload.to || events[events.length - 1].event.payload.phase;
153
+ }
154
+
155
+ module.exports = {
156
+ SCHEMA_VERSION,
157
+ VALID_EVENT_TYPES,
158
+ eventId,
159
+ timestamp,
160
+ centralEvidenceRoot,
161
+ writeEvent,
162
+ listEvents,
163
+ eventStats,
164
+ latestPhase,
165
+ };