pixelcrew 0.2.3 → 0.2.6
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/README.md +389 -315
- package/bin/pixel-agents.js +306 -149
- package/package.json +3 -1
- package/skill/SKILL.md +185 -0
- package/skill/references/anti-ai-rubric.md +30 -0
- package/skill/references/anti-ai-slop-encyclopedia.md +181 -0
- package/skill/references/commands-reference.md +135 -0
- package/skill/references/testing-and-qa-strategy.md +25 -0
- package/skill/references/token-efficiency.md +20 -0
- package/src/adapters/adapter.interface.js +77 -0
- package/src/adapters/antigravity.js +138 -0
- package/src/adapters/claude-code.js +149 -0
- package/src/adapters/codex.js +131 -0
- package/src/adapters/cursor.js +133 -0
- package/src/adapters/generic.js +63 -0
- package/src/adapters/index.js +15 -0
- package/src/adapters/kiro.js +134 -0
- package/src/adapters/registry.js +150 -0
- package/src/backend/analyzer/complexity-analyzer.js +104 -0
- package/src/backend/analyzer/requirement-analyzer.js +213 -0
- package/src/backend/analyzer/threat-analyzer.js +122 -0
- package/src/backend/api/api-designer.js +90 -0
- package/src/backend/api/error-contract.js +132 -0
- package/src/backend/api/route-generator.js +92 -0
- package/src/backend/api/schema-generator.js +64 -0
- package/src/backend/architecture/adr-generator.js +62 -0
- package/src/backend/architecture/architecture-reasoner.js +119 -0
- package/src/backend/architecture/architecture-selector.js +57 -0
- package/src/backend/architecture/tradeoff-analyzer.js +92 -0
- package/src/backend/data/data-modeler.js +114 -0
- package/src/backend/data/index-analyzer.js +93 -0
- package/src/backend/data/query-analyzer.js +41 -0
- package/src/backend/data/schema-generator.js +92 -0
- package/src/backend/data/transaction-analyzer.js +49 -0
- package/src/backend/index.js +67 -0
- package/src/backend/observability/logging-engine.js +80 -0
- package/src/backend/observability/metrics-engine.js +31 -0
- package/src/backend/observability/tracing-engine.js +16 -0
- package/src/backend/patterns/pattern-registry.js +74 -0
- package/src/backend/patterns/pattern-selector.js +39 -0
- package/src/backend/patterns/tradeoff-engine.js +17 -0
- package/src/backend/performance/cache-analyzer.js +31 -0
- package/src/backend/performance/concurrency-analyzer.js +34 -0
- package/src/backend/performance/performance-engine.js +98 -0
- package/src/backend/performance/query-optimizer.js +16 -0
- package/src/backend/reliability/failure-analyzer.js +22 -0
- package/src/backend/reliability/idempotency-engine.js +56 -0
- package/src/backend/reliability/resilience-engine.js +141 -0
- package/src/backend/reliability/retry-analyzer.js +15 -0
- package/src/backend/security/auth-selector.js +33 -0
- package/src/backend/security/authorization-engine.js +90 -0
- package/src/backend/security/policy-engine.js +49 -0
- package/src/backend/security/secret-analyzer.js +39 -0
- package/src/backend/security/security-engine.js +122 -0
- package/src/backend/security/threat-model.js +59 -0
- package/src/backend/synthesis/backend-synthesizer.js +98 -0
- package/src/backend/synthesis/infrastructure-synthesizer.js +108 -0
- package/src/backend/synthesis/module-synthesizer.js +262 -0
- package/src/backend/validation/architecture-validator.js +70 -0
- package/src/backend/validation/backend-quality-validator.js +71 -0
- package/src/backend/validation/dependency-validator.js +20 -0
- package/src/backend/validation/performance-validator.js +38 -0
- package/src/backend/validation/security-validator.js +73 -0
- package/src/commands/adapt.js +105 -0
- package/src/commands/add.js +78 -0
- package/src/commands/animate.js +48 -0
- package/src/commands/assemble.js +57 -0
- package/src/commands/audit.js +93 -0
- package/src/commands/backend.js +125 -0
- package/src/commands/bento.js +25 -0
- package/src/commands/blueprint.js +65 -0
- package/src/commands/bolder.js +48 -0
- package/src/commands/boss-fight.js +60 -0
- package/src/commands/build.js +39 -0
- package/src/commands/calibrate.js +25 -0
- package/src/commands/chromatic.js +25 -0
- package/src/commands/clarify.js +52 -0
- package/src/commands/colorize.js +57 -0
- package/src/commands/command.interface.js +28 -0
- package/src/commands/commands.js +84 -0
- package/src/commands/craft.js +57 -0
- package/src/commands/crew.js +66 -0
- package/src/commands/critique.js +25 -0
- package/src/commands/de-slop.js +25 -0
- package/src/commands/delight.js +46 -0
- package/src/commands/deploy.js +42 -0
- package/src/commands/distill.js +48 -0
- package/src/commands/doctor.js +89 -0
- package/src/commands/document.js +83 -0
- package/src/commands/eight-bit.js +25 -0
- package/src/commands/extract.js +75 -0
- package/src/commands/fix.js +46 -0
- package/src/commands/harden.js +85 -0
- package/src/commands/index.js +67 -0
- package/src/commands/install.js +75 -0
- package/src/commands/layout.js +48 -0
- package/src/commands/live.js +44 -0
- package/src/commands/manifest.js +25 -0
- package/src/commands/office.js +25 -0
- package/src/commands/onboard.js +46 -0
- package/src/commands/oneshot.js +60 -0
- package/src/commands/optimize.js +46 -0
- package/src/commands/overdrive.js +46 -0
- package/src/commands/parser.js +143 -0
- package/src/commands/pixelcrew.js +96 -0
- package/src/commands/plan.js +66 -0
- package/src/commands/polish.js +45 -0
- package/src/commands/quieter.js +48 -0
- package/src/commands/recap.js +358 -0
- package/src/commands/registry.js +353 -0
- package/src/commands/render.js +26 -0
- package/src/commands/retrofit.js +25 -0
- package/src/commands/review.js +118 -0
- package/src/commands/roster.js +25 -0
- package/src/commands/sentinel.js +25 -0
- package/src/commands/shape.js +66 -0
- package/src/commands/skills.js +57 -0
- package/src/commands/status.js +48 -0
- package/src/commands/stop.js +50 -0
- package/src/commands/sync.js +58 -0
- package/src/commands/typeset.js +58 -0
- package/src/commands/warp.js +25 -0
- package/src/core/agent-runtime.js +94 -0
- package/src/core/event-bus.js +98 -0
- package/src/core/index.js +9 -0
- package/src/core/orchestrator.js +234 -0
- package/src/core/scheduler.js +124 -0
- package/src/core/task-graph.js +185 -0
- package/src/dashboard/app.js +1172 -30
- package/src/dashboard/index.html +223 -23
- package/src/dashboard/styles.css +870 -0
- package/src/index.js +16 -0
- package/src/orchestrator/artifact-graph.js +104 -0
- package/src/orchestrator/ast-normalizer.js +147 -0
- package/src/orchestrator/code-generator.js +632 -780
- package/src/orchestrator/design-engine.js +203 -0
- package/src/orchestrator/engine.js +307 -38
- package/src/orchestrator/oneshot.js +367 -380
- package/src/orchestrator/ontology.js +394 -0
- package/src/orchestrator/planner.js +231 -258
- package/src/orchestrator/repair-planner.js +67 -0
- package/src/orchestrator/requirement-contract.js +177 -0
- package/src/orchestrator/requirement-validator.js +39 -0
- package/src/orchestrator/semantic-engine.js +582 -0
- package/src/orchestrator/semantic-reviewer.js +179 -0
- package/src/orchestrator/task-planner.js +111 -0
- package/src/orchestrator/telemetry.js +209 -0
- package/src/orchestrator/ui-synthesizer.js +245 -0
- package/src/orchestrator/visual-critic.js +106 -0
- package/src/policies/architecture/layering.policy.js +14 -0
- package/src/policies/performance/indexing.policy.js +13 -0
- package/src/policies/reliability/resilience.policy.js +13 -0
- package/src/policies/security/auth.policy.js +31 -0
- package/src/protocol/agent.js +124 -0
- package/src/protocol/event.js +78 -0
- package/src/protocol/index.js +8 -0
- package/src/protocol/skill.js +54 -0
- package/src/protocol/task.js +100 -0
- package/src/scaffold/claude-generator.js +60 -0
- package/src/scaffold/commands-catalog.js +321 -0
- package/src/scaffold/cursor-generator.js +74 -0
- package/src/scaffold/ide-rules.js +133 -0
- package/src/scaffold/init.js +415 -91
- package/src/scaffold/installer.js +571 -0
- package/src/scaffold/kiro-generator.js +86 -0
- package/src/scaffold/skills-bundle.js +520 -0
- package/src/scaffold/templates.js +57 -4
- package/src/scaffold/workstations.js +364 -0
- package/src/server/server.js +283 -40
- package/src/utils/fs-safe.js +168 -0
- package/src/utils/markdown-report.js +175 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PIXEL CREW — /recap Command
|
|
3
|
+
*
|
|
4
|
+
* Generates a token-optimized recap of the user's recent git activity.
|
|
5
|
+
* Designed for AI agent consumption: compact output, structured data,
|
|
6
|
+
* no redundant prose. Reads git log, diff stats, and file change status.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { PixelCommand } from './command.interface.js';
|
|
12
|
+
import { MarkdownReportBuilder } from '../utils/markdown-report.js';
|
|
13
|
+
|
|
14
|
+
export class RecapCommand extends PixelCommand {
|
|
15
|
+
constructor() {
|
|
16
|
+
super({
|
|
17
|
+
name: 'recap',
|
|
18
|
+
aliases: ['summary', 'changelog', 'whatdone'],
|
|
19
|
+
description: 'Generate a token-optimized recap of recent git changes',
|
|
20
|
+
usage: '/recap [count=10]',
|
|
21
|
+
category: 'inspection'
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Safely executes a git command in the target directory.
|
|
27
|
+
* Returns empty string on failure (not a git repo, git not installed, etc.)
|
|
28
|
+
*/
|
|
29
|
+
_git(cmd, cwd) {
|
|
30
|
+
try {
|
|
31
|
+
return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
32
|
+
} catch {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async execute(context, args) {
|
|
38
|
+
const cwd = context.targetDir || process.cwd();
|
|
39
|
+
const count = Math.min(Math.max(parseInt(args[0], 10) || 10, 1), 50);
|
|
40
|
+
|
|
41
|
+
// 1. Verify git repo
|
|
42
|
+
const isGit = this._git('rev-parse --is-inside-work-tree', cwd);
|
|
43
|
+
if (isGit !== 'true') {
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
message: 'Not a git repository. Run /recap from a project with git history.',
|
|
47
|
+
output: [
|
|
48
|
+
'╔══════════════════════════════════════════════════════════════════╗',
|
|
49
|
+
'║ PIXEL CREW — SESSION RECAP ║',
|
|
50
|
+
'╚══════════════════════════════════════════════════════════════════╝',
|
|
51
|
+
'',
|
|
52
|
+
'⚠ Not a git repository. No recap available.'
|
|
53
|
+
].join('\n')
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Get total commit count (cap diff range to available commits)
|
|
58
|
+
const totalCommitCount = parseInt(this._git('rev-list --count HEAD', cwd), 10) || 0;
|
|
59
|
+
const effectiveCount = Math.min(count, totalCommitCount);
|
|
60
|
+
|
|
61
|
+
if (effectiveCount === 0) {
|
|
62
|
+
return {
|
|
63
|
+
success: true,
|
|
64
|
+
message: 'No commits found.',
|
|
65
|
+
data: { commits: [], stats: { filesChanged: 0, insertions: 0, deletions: 0 }, fileChanges: [] },
|
|
66
|
+
output: [
|
|
67
|
+
'╔══════════════════════════════════════════════════════════════════╗',
|
|
68
|
+
'║ PIXEL CREW — SESSION RECAP ║',
|
|
69
|
+
'╚══════════════════════════════════════════════════════════════════╝',
|
|
70
|
+
'',
|
|
71
|
+
'No commits found in this repository.'
|
|
72
|
+
].join('\n')
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 3. Fetch commits — compact one-line format (token-optimized)
|
|
77
|
+
const logRaw = this._git(
|
|
78
|
+
`log -${effectiveCount} --pretty=format:"%h|%aI|%an|%s"`,
|
|
79
|
+
cwd
|
|
80
|
+
);
|
|
81
|
+
const commits = logRaw
|
|
82
|
+
.split('\n')
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.map(line => {
|
|
85
|
+
const [hash, date, author, ...msgParts] = line.split('|');
|
|
86
|
+
return { hash, date, author, message: msgParts.join('|') };
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 4. Diff stats (insertions / deletions / files changed)
|
|
90
|
+
// Use --shortstat for minimal token output
|
|
91
|
+
let diffRange;
|
|
92
|
+
if (effectiveCount >= totalCommitCount) {
|
|
93
|
+
// Full repo history — diff from root commit's tree
|
|
94
|
+
const rootHash = this._git('rev-list --max-parents=0 HEAD', cwd).split('\n')[0];
|
|
95
|
+
diffRange = `${rootHash}..HEAD`;
|
|
96
|
+
// If only 1 commit, diff against empty tree
|
|
97
|
+
if (totalCommitCount === 1) {
|
|
98
|
+
diffRange = '--root HEAD';
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
diffRange = `HEAD~${effectiveCount}..HEAD`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// For single-commit repos, use diff-tree which handles root commits
|
|
105
|
+
let shortStat, nameStatus;
|
|
106
|
+
if (totalCommitCount === 1) {
|
|
107
|
+
shortStat = this._git('diff --stat HEAD', cwd).includes('file')
|
|
108
|
+
? this._git('log --oneline --shortstat -1 HEAD', cwd)
|
|
109
|
+
: '';
|
|
110
|
+
nameStatus = this._git('diff-tree --no-commit-id -r --name-status HEAD', cwd);
|
|
111
|
+
} else {
|
|
112
|
+
shortStat = this._git(`diff --shortstat ${diffRange}`, cwd);
|
|
113
|
+
nameStatus = this._git(`diff --name-status ${diffRange}`, cwd);
|
|
114
|
+
}
|
|
115
|
+
const stats = this._parseShortStat(shortStat);
|
|
116
|
+
const fileChanges = this._parseNameStatus(nameStatus);
|
|
117
|
+
const timeRange = commits.length > 0
|
|
118
|
+
? `${this._shortDate(commits[commits.length - 1].date)} → ${this._shortDate(commits[0].date)}`
|
|
119
|
+
: 'N/A';
|
|
120
|
+
|
|
121
|
+
// 5. Synthesize structured summary of work done & key modules
|
|
122
|
+
const summaryData = this._synthesizeSummary(commits, fileChanges, stats);
|
|
123
|
+
|
|
124
|
+
// 6. Token-optimized output — structured summary, clear sections, no redundant prose
|
|
125
|
+
const lines = [
|
|
126
|
+
'╔══════════════════════════════════════════════════════════════════╗',
|
|
127
|
+
'║ PIXEL CREW — SESSION RECAP ║',
|
|
128
|
+
'╚══════════════════════════════════════════════════════════════════╝',
|
|
129
|
+
'',
|
|
130
|
+
`RANGE: ${timeRange} | ${effectiveCount} commit${effectiveCount !== 1 ? 's' : ''} | ${stats.filesChanged} file${stats.filesChanged !== 1 ? 's' : ''} | +${stats.insertions} −${stats.deletions}`
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
// Executive Summary of Work Done
|
|
134
|
+
if (summaryData.summaryBullets.length > 0) {
|
|
135
|
+
lines.push('');
|
|
136
|
+
lines.push('SUMMARY OF WORK DONE:');
|
|
137
|
+
for (const bullet of summaryData.summaryBullets) {
|
|
138
|
+
lines.push(` ${bullet}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Key Modules Touched
|
|
143
|
+
if (summaryData.topModules.length > 0) {
|
|
144
|
+
lines.push('');
|
|
145
|
+
lines.push(`KEY MODULES TOUCHED: ${summaryData.topModules.join(' • ')}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Commits — one line each, no fluff
|
|
149
|
+
lines.push('');
|
|
150
|
+
lines.push('COMMITS:');
|
|
151
|
+
for (const c of commits) {
|
|
152
|
+
lines.push(` ${c.hash} ${this._shortDate(c.date)} ${c.message}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Files — grouped, compact
|
|
156
|
+
if (fileChanges.length > 0) {
|
|
157
|
+
lines.push('');
|
|
158
|
+
lines.push('FILES:');
|
|
159
|
+
const added = fileChanges.filter(f => f.status === 'A');
|
|
160
|
+
const modified = fileChanges.filter(f => f.status === 'M');
|
|
161
|
+
const deleted = fileChanges.filter(f => f.status === 'D');
|
|
162
|
+
const renamed = fileChanges.filter(f => f.status === 'R');
|
|
163
|
+
|
|
164
|
+
if (added.length) lines.push(` + ${added.map(f => f.file).join(', ')}`);
|
|
165
|
+
if (modified.length) lines.push(` ~ ${modified.map(f => f.file).join(', ')}`);
|
|
166
|
+
if (deleted.length) lines.push(` - ${deleted.map(f => f.file).join(', ')}`);
|
|
167
|
+
if (renamed.length) lines.push(` → ${renamed.map(f => f.file).join(', ')}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const reportBuilder = new MarkdownReportBuilder({
|
|
171
|
+
title: 'Session Activity & Git Changelog Recap',
|
|
172
|
+
command: '/recap',
|
|
173
|
+
category: 'Git History & Workspace Activity',
|
|
174
|
+
agent: 'Lead Orchestrator (Floor 42)',
|
|
175
|
+
project: context.engine?.getConfig?.()?.project || path.basename(cwd),
|
|
176
|
+
status: 'VERIFIED_CLEAN',
|
|
177
|
+
summary: `Git activity recap covering **${effectiveCount} commits** across \`${timeRange}\`.\n\nTotal changes: **${stats.filesChanged} files modified** with **+${stats.insertions} additions** and **−${stats.deletions} deletions**.\n\n### Summary of Accomplishments:\n${summaryData.summaryBullets.map(b => `- **${b.replace(/^•\s*/, '')}**`).join('\n')}\n\n**Primary Modules Touched:** ${summaryData.topModules.join(', ')}`,
|
|
178
|
+
metrics: [
|
|
179
|
+
{ name: 'Commits in Scope', target: 'Recent History', value: `${effectiveCount} Commits`, status: 'OK' },
|
|
180
|
+
{ name: 'Files Modified', target: 'Affected Scope', value: `${stats.filesChanged} Files`, status: 'OK' },
|
|
181
|
+
{ name: 'Lines Inserted', target: 'Additions', value: `+${stats.insertions}`, status: 'OK' },
|
|
182
|
+
{ name: 'Lines Deleted', target: 'Deletions', value: `−${stats.deletions}`, status: 'OK' }
|
|
183
|
+
],
|
|
184
|
+
sections: [
|
|
185
|
+
{
|
|
186
|
+
title: 'Workstream Accomplishments & Highlights',
|
|
187
|
+
icon: '🚀',
|
|
188
|
+
items: summaryData.summaryBullets
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
title: 'Recent Commit Timeline',
|
|
192
|
+
icon: '📜',
|
|
193
|
+
table: {
|
|
194
|
+
headers: ['Commit Hash', 'Timestamp', 'Author', 'Commit Message'],
|
|
195
|
+
rows: commits.map(c => [c.hash, this._shortDate(c.date), c.author || 'User', c.message])
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
title: 'Categorized File Modifications',
|
|
200
|
+
icon: '📁',
|
|
201
|
+
table: {
|
|
202
|
+
headers: ['Operation', 'Status', 'File Path'],
|
|
203
|
+
rows: fileChanges.map(f => [
|
|
204
|
+
f.status === 'A' ? 'Added' : (f.status === 'M' ? 'Modified' : (f.status === 'D' ? 'Deleted' : 'Renamed')),
|
|
205
|
+
f.status,
|
|
206
|
+
`\`${f.file}\``
|
|
207
|
+
])
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const reportSaveResult = await reportBuilder.save(cwd, `session-recap-${Date.now()}`);
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
message: `Recap: ${effectiveCount} commits, ${stats.filesChanged} files changed (+${stats.insertions} −${stats.deletions})`,
|
|
218
|
+
data: {
|
|
219
|
+
commits,
|
|
220
|
+
stats,
|
|
221
|
+
fileChanges,
|
|
222
|
+
timeRange,
|
|
223
|
+
count: effectiveCount,
|
|
224
|
+
summary: summaryData,
|
|
225
|
+
reportPath: reportSaveResult.filePath,
|
|
226
|
+
markdown: reportSaveResult.markdown
|
|
227
|
+
},
|
|
228
|
+
output: lines.join('\n')
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Synthesizes a structured, categorized summary of work done across recent commits.
|
|
234
|
+
*/
|
|
235
|
+
_synthesizeSummary(commits, fileChanges, stats) {
|
|
236
|
+
const categories = {
|
|
237
|
+
features: [],
|
|
238
|
+
hardening: [],
|
|
239
|
+
refactors: [],
|
|
240
|
+
docs: [],
|
|
241
|
+
tests: [],
|
|
242
|
+
chores: []
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
for (const c of commits) {
|
|
246
|
+
const msg = c.message.toLowerCase();
|
|
247
|
+
if (msg.startsWith('feat') || msg.includes('add') || msg.includes('implement')) {
|
|
248
|
+
if (msg.includes('anti-ai') || msg.includes('anti-slop') || msg.includes('security') || msg.includes('directive')) {
|
|
249
|
+
categories.hardening.push(c);
|
|
250
|
+
} else {
|
|
251
|
+
categories.features.push(c);
|
|
252
|
+
}
|
|
253
|
+
} else if (msg.startsWith('fix') || msg.includes('bug') || msg.includes('repair')) {
|
|
254
|
+
categories.hardening.push(c);
|
|
255
|
+
} else if (msg.startsWith('refactor') || msg.includes('rename') || msg.includes('standardiz') || msg.includes('clean')) {
|
|
256
|
+
categories.refactors.push(c);
|
|
257
|
+
} else if (msg.startsWith('docs') || msg.includes('readme') || msg.includes('guide')) {
|
|
258
|
+
categories.docs.push(c);
|
|
259
|
+
} else if (msg.startsWith('test') || msg.includes('e2e') || msg.includes('benchmark')) {
|
|
260
|
+
categories.tests.push(c);
|
|
261
|
+
} else {
|
|
262
|
+
categories.chores.push(c);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Identify primary modules/directories touched
|
|
267
|
+
const dirCounts = {};
|
|
268
|
+
for (const f of fileChanges) {
|
|
269
|
+
const parts = f.file.split('/');
|
|
270
|
+
let prefix;
|
|
271
|
+
if (parts[0].startsWith('.')) {
|
|
272
|
+
prefix = parts[0];
|
|
273
|
+
} else if (parts.length > 1) {
|
|
274
|
+
prefix = `${parts[0]}/${parts[1]}`;
|
|
275
|
+
} else {
|
|
276
|
+
prefix = parts[0];
|
|
277
|
+
}
|
|
278
|
+
dirCounts[prefix] = (dirCounts[prefix] || 0) + 1;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const topModules = Object.entries(dirCounts)
|
|
282
|
+
.sort((a, b) => b[1] - a[1])
|
|
283
|
+
.slice(0, 5)
|
|
284
|
+
.map(([dir, count]) => `${dir} (${count} file${count !== 1 ? 's' : ''})`);
|
|
285
|
+
|
|
286
|
+
// Build human-readable bullet summaries
|
|
287
|
+
const summaryBullets = [];
|
|
288
|
+
if (categories.features.length > 0) {
|
|
289
|
+
const sample = categories.features.slice(0, 2).map(c => c.message.replace(/^feat(\([^)]+\))?:\s*/i, '')).join('; ');
|
|
290
|
+
summaryBullets.push(`Features & Capabilities (${categories.features.length}): ${sample}`);
|
|
291
|
+
}
|
|
292
|
+
if (categories.hardening.length > 0) {
|
|
293
|
+
const sample = categories.hardening.slice(0, 2).map(c => c.message.replace(/^(feat|fix)(\([^)]+\))?:\s*/i, '')).join('; ');
|
|
294
|
+
summaryBullets.push(`Quality & Anti-Slop (${categories.hardening.length}): ${sample}`);
|
|
295
|
+
}
|
|
296
|
+
if (categories.refactors.length > 0) {
|
|
297
|
+
const sample = categories.refactors.slice(0, 2).map(c => c.message.replace(/^refactor(\([^)]+\))?:\s*/i, '')).join('; ');
|
|
298
|
+
summaryBullets.push(`Architecture & Refactoring (${categories.refactors.length}): ${sample}`);
|
|
299
|
+
}
|
|
300
|
+
if (categories.docs.length > 0) {
|
|
301
|
+
const sample = categories.docs.slice(0, 2).map(c => c.message.replace(/^docs(\([^)]+\))?:\s*/i, '')).join('; ');
|
|
302
|
+
summaryBullets.push(`Documentation & Brand (${categories.docs.length}): ${sample}`);
|
|
303
|
+
}
|
|
304
|
+
if (categories.tests.length > 0) {
|
|
305
|
+
const sample = categories.tests.slice(0, 2).map(c => c.message.replace(/^test(\([^)]+\))?:\s*/i, '')).join('; ');
|
|
306
|
+
summaryBullets.push(`Testing & Verification (${categories.tests.length}): ${sample}`);
|
|
307
|
+
}
|
|
308
|
+
if (categories.chores.length > 0 && summaryBullets.length === 0) {
|
|
309
|
+
const sample = categories.chores.slice(0, 2).map(c => c.message).join('; ');
|
|
310
|
+
summaryBullets.push(`Maintenance (${categories.chores.length}): ${sample}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
categories,
|
|
315
|
+
topModules,
|
|
316
|
+
summaryBullets
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Parses git diff --shortstat output into structured numbers.
|
|
322
|
+
* Example: " 3 files changed, 45 insertions(+), 12 deletions(-)"
|
|
323
|
+
*/
|
|
324
|
+
_parseShortStat(raw) {
|
|
325
|
+
const result = { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
326
|
+
if (!raw) return result;
|
|
327
|
+
const filesMatch = raw.match(/(\d+) file/);
|
|
328
|
+
const insMatch = raw.match(/(\d+) insertion/);
|
|
329
|
+
const delMatch = raw.match(/(\d+) deletion/);
|
|
330
|
+
if (filesMatch) result.filesChanged = parseInt(filesMatch[1], 10);
|
|
331
|
+
if (insMatch) result.insertions = parseInt(insMatch[1], 10);
|
|
332
|
+
if (delMatch) result.deletions = parseInt(delMatch[1], 10);
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Parses git diff --name-status into file change entries.
|
|
338
|
+
* Example: "M\tsrc/index.js" → { status: 'M', file: 'src/index.js' }
|
|
339
|
+
*/
|
|
340
|
+
_parseNameStatus(raw) {
|
|
341
|
+
if (!raw) return [];
|
|
342
|
+
return raw.split('\n').filter(Boolean).map(line => {
|
|
343
|
+
const [status, ...fileParts] = line.split('\t');
|
|
344
|
+
return { status: status.charAt(0), file: fileParts.join('\t') };
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Formats ISO date to compact "Sep 01 15:14" */
|
|
349
|
+
_shortDate(isoStr) {
|
|
350
|
+
try {
|
|
351
|
+
const d = new Date(isoStr);
|
|
352
|
+
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
353
|
+
return `${months[d.getMonth()]} ${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
|
|
354
|
+
} catch {
|
|
355
|
+
return isoStr || '';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|