@tonycasey/lisa 2.25.4 → 2.26.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/lib/application/handlers/SessionStartHandler.d.ts +15 -0
- package/dist/lib/application/handlers/SessionStartHandler.d.ts.map +1 -1
- package/dist/lib/application/handlers/SessionStartHandler.js +116 -5
- package/dist/lib/application/handlers/SessionStartHandler.js.map +1 -1
- package/dist/lib/application/services/GitTriageService.d.ts +45 -0
- package/dist/lib/application/services/GitTriageService.d.ts.map +1 -0
- package/dist/lib/application/services/GitTriageService.js +360 -0
- package/dist/lib/application/services/GitTriageService.js.map +1 -0
- package/dist/lib/application/services/SessionContextFormatter.d.ts +11 -1
- package/dist/lib/application/services/SessionContextFormatter.d.ts.map +1 -1
- package/dist/lib/application/services/SessionContextFormatter.js +59 -3
- package/dist/lib/application/services/SessionContextFormatter.js.map +1 -1
- package/dist/lib/application/services/index.d.ts +1 -0
- package/dist/lib/application/services/index.d.ts.map +1 -1
- package/dist/lib/application/services/index.js +3 -1
- package/dist/lib/application/services/index.js.map +1 -1
- package/dist/lib/domain/interfaces/ICommitEnricher.d.ts +81 -0
- package/dist/lib/domain/interfaces/ICommitEnricher.d.ts.map +1 -0
- package/dist/lib/domain/interfaces/ICommitEnricher.js +33 -0
- package/dist/lib/domain/interfaces/ICommitEnricher.js.map +1 -0
- package/dist/lib/domain/interfaces/IGitClient.d.ts +86 -0
- package/dist/lib/domain/interfaces/IGitClient.d.ts.map +1 -1
- package/dist/lib/domain/interfaces/IGitTriageService.d.ts +180 -0
- package/dist/lib/domain/interfaces/IGitTriageService.d.ts.map +1 -0
- package/dist/lib/domain/interfaces/IGitTriageService.js +12 -0
- package/dist/lib/domain/interfaces/IGitTriageService.js.map +1 -0
- package/dist/lib/domain/interfaces/ILisaServices.d.ts +6 -0
- package/dist/lib/domain/interfaces/ILisaServices.d.ts.map +1 -1
- package/dist/lib/domain/interfaces/index.d.ts +3 -1
- package/dist/lib/domain/interfaces/index.d.ts.map +1 -1
- package/dist/lib/domain/interfaces/index.js +4 -1
- package/dist/lib/domain/interfaces/index.js.map +1 -1
- package/dist/lib/infrastructure/adapters/claude/session-start.d.ts +1 -0
- package/dist/lib/infrastructure/adapters/claude/session-start.d.ts.map +1 -1
- package/dist/lib/infrastructure/adapters/claude/session-start.js +18 -4
- package/dist/lib/infrastructure/adapters/claude/session-start.js.map +1 -1
- package/dist/lib/infrastructure/di/ServiceFactory.d.ts +2 -0
- package/dist/lib/infrastructure/di/ServiceFactory.d.ts.map +1 -1
- package/dist/lib/infrastructure/di/bootstrap.d.ts.map +1 -1
- package/dist/lib/infrastructure/di/bootstrap.js +11 -1
- package/dist/lib/infrastructure/di/bootstrap.js.map +1 -1
- package/dist/lib/infrastructure/di/tokens.d.ts +2 -0
- package/dist/lib/infrastructure/di/tokens.d.ts.map +1 -1
- package/dist/lib/infrastructure/di/tokens.js +1 -0
- package/dist/lib/infrastructure/di/tokens.js.map +1 -1
- package/dist/lib/infrastructure/git/GitClient.d.ts +11 -1
- package/dist/lib/infrastructure/git/GitClient.d.ts.map +1 -1
- package/dist/lib/infrastructure/git/GitClient.js +137 -0
- package/dist/lib/infrastructure/git/GitClient.js.map +1 -1
- package/dist/lib/infrastructure/logging/Logger.d.ts +13 -1
- package/dist/lib/infrastructure/logging/Logger.d.ts.map +1 -1
- package/dist/lib/infrastructure/logging/Logger.js +25 -7
- package/dist/lib/infrastructure/logging/Logger.js.map +1 -1
- package/dist/lib/infrastructure/logging/factory.d.ts +5 -1
- package/dist/lib/infrastructure/logging/factory.d.ts.map +1 -1
- package/dist/lib/infrastructure/logging/factory.js +3 -0
- package/dist/lib/infrastructure/logging/factory.js.map +1 -1
- package/dist/lib/infrastructure/logging/index.d.ts +1 -1
- package/dist/lib/infrastructure/logging/index.d.ts.map +1 -1
- package/dist/lib/infrastructure/logging/index.js.map +1 -1
- package/dist/lib/infrastructure/services/CommitEnricher.d.ts +26 -0
- package/dist/lib/infrastructure/services/CommitEnricher.d.ts.map +1 -0
- package/dist/lib/infrastructure/services/CommitEnricher.js +214 -0
- package/dist/lib/infrastructure/services/CommitEnricher.js.map +1 -0
- package/dist/lib/infrastructure/services/prompts/commit-extraction.d.ts +26 -0
- package/dist/lib/infrastructure/services/prompts/commit-extraction.d.ts.map +1 -0
- package/dist/lib/infrastructure/services/prompts/commit-extraction.js +151 -0
- package/dist/lib/infrastructure/services/prompts/commit-extraction.js.map +1 -0
- package/dist/opencode/lisa.js +907 -18
- package/dist/package.json +1 -1
- package/dist/project/.lisa/rules/shared/git-rules.md +12 -3
- package/package.json +1 -1
package/dist/opencode/lisa.js
CHANGED
|
@@ -2379,6 +2379,27 @@ var init_ITranscriptEnricher = __esm({
|
|
|
2379
2379
|
}
|
|
2380
2380
|
});
|
|
2381
2381
|
|
|
2382
|
+
// src/lib/domain/interfaces/ICommitEnricher.ts
|
|
2383
|
+
function isValidCommitFactType(value) {
|
|
2384
|
+
return COMMIT_FACT_TYPE_VALUES.includes(value);
|
|
2385
|
+
}
|
|
2386
|
+
var COMMIT_FACT_TYPE_VALUES;
|
|
2387
|
+
var init_ICommitEnricher = __esm({
|
|
2388
|
+
"src/lib/domain/interfaces/ICommitEnricher.ts"() {
|
|
2389
|
+
"use strict";
|
|
2390
|
+
COMMIT_FACT_TYPE_VALUES = [
|
|
2391
|
+
"feature",
|
|
2392
|
+
"decision",
|
|
2393
|
+
"refactor",
|
|
2394
|
+
"migration",
|
|
2395
|
+
"bugfix",
|
|
2396
|
+
"breaking-change",
|
|
2397
|
+
"convention",
|
|
2398
|
+
"dependency"
|
|
2399
|
+
];
|
|
2400
|
+
}
|
|
2401
|
+
});
|
|
2402
|
+
|
|
2382
2403
|
// src/lib/domain/interfaces/INlCurationService.ts
|
|
2383
2404
|
function isValidNlCurationIntent(value) {
|
|
2384
2405
|
return NL_CURATION_INTENT_VALUES.includes(value);
|
|
@@ -2693,6 +2714,7 @@ var init_interfaces = __esm({
|
|
|
2693
2714
|
init_ILlmService();
|
|
2694
2715
|
init_ILlmUsageTracker();
|
|
2695
2716
|
init_ITranscriptEnricher();
|
|
2717
|
+
init_ICommitEnricher();
|
|
2696
2718
|
init_INlCurationService();
|
|
2697
2719
|
init_events();
|
|
2698
2720
|
init_types2();
|
|
@@ -6903,13 +6925,14 @@ var init_clients = __esm({
|
|
|
6903
6925
|
});
|
|
6904
6926
|
|
|
6905
6927
|
// src/lib/application/services/SessionContextFormatter.ts
|
|
6906
|
-
var RECENT_HOURS, MAX_RECENT_MEMORIES, GROUP_WINDOW_MINUTES, EXCLUDED_RELATIONSHIPS, SessionContextFormatter;
|
|
6928
|
+
var RECENT_HOURS, MAX_RECENT_MEMORIES, GROUP_WINDOW_MINUTES, MAX_HOTSPOTS, EXCLUDED_RELATIONSHIPS, SessionContextFormatter;
|
|
6907
6929
|
var init_SessionContextFormatter = __esm({
|
|
6908
6930
|
"src/lib/application/services/SessionContextFormatter.ts"() {
|
|
6909
6931
|
"use strict";
|
|
6910
6932
|
RECENT_HOURS = 24;
|
|
6911
6933
|
MAX_RECENT_MEMORIES = 5;
|
|
6912
6934
|
GROUP_WINDOW_MINUTES = 5;
|
|
6935
|
+
MAX_HOTSPOTS = 3;
|
|
6913
6936
|
EXCLUDED_RELATIONSHIPS = /* @__PURE__ */ new Set([
|
|
6914
6937
|
"USER_SUBMITS_DIRECTION",
|
|
6915
6938
|
"DIRECTION_IS_TOPIC",
|
|
@@ -6957,7 +6980,7 @@ var init_SessionContextFormatter = __esm({
|
|
|
6957
6980
|
/**
|
|
6958
6981
|
* Format the full context content for injection into session context.
|
|
6959
6982
|
*/
|
|
6960
|
-
formatContextContent(trigger, memories, tasks, taskCounts, context, gitCommits = [], querySince) {
|
|
6983
|
+
formatContextContent(trigger, memories, tasks, taskCounts, context, gitCommits = [], querySince, gitTriage) {
|
|
6961
6984
|
const { projectName, userName, folderType, projectRoot, branch } = context;
|
|
6962
6985
|
const lines = [];
|
|
6963
6986
|
lines.push(this.getTriggerMessage(trigger, memories.timedOut));
|
|
@@ -6976,7 +6999,10 @@ var init_SessionContextFormatter = __esm({
|
|
|
6976
6999
|
lines.push(` ${memories.initReview}`);
|
|
6977
7000
|
lines.push("");
|
|
6978
7001
|
}
|
|
6979
|
-
if (
|
|
7002
|
+
if (gitTriage && gitTriage.totalCommits > 0) {
|
|
7003
|
+
lines.push("");
|
|
7004
|
+
lines.push(...this.formatGitTriageSummary(gitTriage));
|
|
7005
|
+
} else if (gitCommits.length > 0) {
|
|
6980
7006
|
lines.push("");
|
|
6981
7007
|
lines.push(`Recent commits (${gitCommits.length}):`);
|
|
6982
7008
|
gitCommits.slice(0, 5).forEach((c) => {
|
|
@@ -7155,6 +7181,46 @@ var init_SessionContextFormatter = __esm({
|
|
|
7155
7181
|
return `${month} ${day} ${time}`;
|
|
7156
7182
|
}
|
|
7157
7183
|
}
|
|
7184
|
+
/**
|
|
7185
|
+
* Format git triage results as a compact summary.
|
|
7186
|
+
* Shows aggregate counts rather than individual commits.
|
|
7187
|
+
*/
|
|
7188
|
+
formatGitTriageSummary(triage) {
|
|
7189
|
+
const lines = [];
|
|
7190
|
+
lines.push(`Git history: scanned ${triage.totalCommits.toLocaleString()} commits`);
|
|
7191
|
+
if (triage.belowThreshold > 0) {
|
|
7192
|
+
lines.push(` \u2192 ${triage.belowThreshold.toLocaleString()} below threshold (skipped)`);
|
|
7193
|
+
}
|
|
7194
|
+
if (triage.minorInterest > 0) {
|
|
7195
|
+
lines.push(` \u2192 ${triage.minorInterest.toLocaleString()} minor interest`);
|
|
7196
|
+
}
|
|
7197
|
+
const highCount = triage.highInterest.length;
|
|
7198
|
+
if (highCount > 0) {
|
|
7199
|
+
lines.push(` \u2192 ${highCount.toLocaleString()} high interest`);
|
|
7200
|
+
if (triage.linkedToPRs > 0) {
|
|
7201
|
+
lines.push(` \u2192 ${triage.linkedToPRs} linked to PRs`);
|
|
7202
|
+
}
|
|
7203
|
+
if (triage.linkedToTags > 0) {
|
|
7204
|
+
lines.push(` \u2192 ${triage.linkedToTags} linked to tags/releases`);
|
|
7205
|
+
}
|
|
7206
|
+
}
|
|
7207
|
+
if (triage.hotspots.length > 0) {
|
|
7208
|
+
const topHotspots = triage.hotspots.slice(0, MAX_HOTSPOTS);
|
|
7209
|
+
const hotspotList = topHotspots.map((h) => this.formatHotspotPath(h.path)).join(", ");
|
|
7210
|
+
lines.push(` Hotspots: ${hotspotList}`);
|
|
7211
|
+
}
|
|
7212
|
+
return lines;
|
|
7213
|
+
}
|
|
7214
|
+
/**
|
|
7215
|
+
* Format a file path for hotspot display (truncate long paths).
|
|
7216
|
+
*/
|
|
7217
|
+
formatHotspotPath(path15) {
|
|
7218
|
+
const parts = path15.split("/");
|
|
7219
|
+
if (parts.length <= 2) {
|
|
7220
|
+
return path15;
|
|
7221
|
+
}
|
|
7222
|
+
return `.../${parts.slice(-2).join("/")}`;
|
|
7223
|
+
}
|
|
7158
7224
|
};
|
|
7159
7225
|
}
|
|
7160
7226
|
});
|
|
@@ -7357,6 +7423,300 @@ var init_MemoryContextLoader = __esm({
|
|
|
7357
7423
|
}
|
|
7358
7424
|
});
|
|
7359
7425
|
|
|
7426
|
+
// src/lib/application/services/GitTriageService.ts
|
|
7427
|
+
var DEFAULT_THRESHOLD, DEFAULT_DAYS, CONVENTIONAL_PREFIXES, CONVENTIONAL_COMMIT_REGEX, DECISION_KEYWORDS, SCORES, TAG_ADJACENT_DISTANCE, STAT_FETCH_MARGIN, GitTriageService;
|
|
7428
|
+
var init_GitTriageService = __esm({
|
|
7429
|
+
"src/lib/application/services/GitTriageService.ts"() {
|
|
7430
|
+
"use strict";
|
|
7431
|
+
DEFAULT_THRESHOLD = 3;
|
|
7432
|
+
DEFAULT_DAYS = 90;
|
|
7433
|
+
CONVENTIONAL_PREFIXES = [
|
|
7434
|
+
"feat",
|
|
7435
|
+
"fix",
|
|
7436
|
+
"refactor",
|
|
7437
|
+
"docs",
|
|
7438
|
+
"test",
|
|
7439
|
+
"chore",
|
|
7440
|
+
"style",
|
|
7441
|
+
"perf",
|
|
7442
|
+
"ci",
|
|
7443
|
+
"build",
|
|
7444
|
+
"revert"
|
|
7445
|
+
];
|
|
7446
|
+
CONVENTIONAL_COMMIT_REGEX = new RegExp(
|
|
7447
|
+
`^(${CONVENTIONAL_PREFIXES.join("|")})(\\(.+\\))?[!]?:`,
|
|
7448
|
+
"i"
|
|
7449
|
+
);
|
|
7450
|
+
DECISION_KEYWORDS = [
|
|
7451
|
+
"migrate",
|
|
7452
|
+
"replace",
|
|
7453
|
+
"rewrite",
|
|
7454
|
+
"deprecate",
|
|
7455
|
+
"breaking",
|
|
7456
|
+
"workaround",
|
|
7457
|
+
"decision",
|
|
7458
|
+
"architecture",
|
|
7459
|
+
"redesign",
|
|
7460
|
+
"overhaul"
|
|
7461
|
+
];
|
|
7462
|
+
SCORES = {
|
|
7463
|
+
largeDiffFiles: 3,
|
|
7464
|
+
// 10+ files changed
|
|
7465
|
+
largeDiffLines: 2,
|
|
7466
|
+
// 500+ lines changed
|
|
7467
|
+
mergeCommitWithPR: 3,
|
|
7468
|
+
// merge commit with PR reference
|
|
7469
|
+
conventionalPrefix: 1,
|
|
7470
|
+
// has conventional commit prefix
|
|
7471
|
+
decisionKeywords: 2,
|
|
7472
|
+
// contains decision keywords
|
|
7473
|
+
createsNewDirectory: 2,
|
|
7474
|
+
// creates new top-level dir
|
|
7475
|
+
tagAdjacent: 2,
|
|
7476
|
+
// within N commits of a tag
|
|
7477
|
+
longMessageBody: 1
|
|
7478
|
+
// multi-line body
|
|
7479
|
+
};
|
|
7480
|
+
TAG_ADJACENT_DISTANCE = 3;
|
|
7481
|
+
STAT_FETCH_MARGIN = 2;
|
|
7482
|
+
GitTriageService = class {
|
|
7483
|
+
constructor(git) {
|
|
7484
|
+
this.git = git;
|
|
7485
|
+
}
|
|
7486
|
+
async triage(options) {
|
|
7487
|
+
const startTime = Date.now();
|
|
7488
|
+
const threshold = options?.threshold ?? DEFAULT_THRESHOLD;
|
|
7489
|
+
const cwd = options?.cwd;
|
|
7490
|
+
const since = options?.since ?? this.getDefaultSince();
|
|
7491
|
+
const until = options?.until ?? /* @__PURE__ */ new Date();
|
|
7492
|
+
const tags = this.git.listTags(cwd);
|
|
7493
|
+
const tagShas = new Set(tags.map((t) => t.sha));
|
|
7494
|
+
const tagInfo = tags.map((t) => ({
|
|
7495
|
+
name: t.name,
|
|
7496
|
+
sha: t.sha,
|
|
7497
|
+
isVersionTag: this.isVersionTag(t.name)
|
|
7498
|
+
}));
|
|
7499
|
+
const rawCommits = this.git.logDetailed({
|
|
7500
|
+
since: since.toISOString(),
|
|
7501
|
+
until: until.toISOString(),
|
|
7502
|
+
maxCount: options?.maxCommits,
|
|
7503
|
+
cwd
|
|
7504
|
+
});
|
|
7505
|
+
if (rawCommits.length === 0) {
|
|
7506
|
+
return this.emptyResult(startTime, tagInfo);
|
|
7507
|
+
}
|
|
7508
|
+
const shaToPosition = /* @__PURE__ */ new Map();
|
|
7509
|
+
for (let i = 0; i < rawCommits.length; i++) {
|
|
7510
|
+
shaToPosition.set(rawCommits[i].sha, i);
|
|
7511
|
+
}
|
|
7512
|
+
const tagPositions = /* @__PURE__ */ new Set();
|
|
7513
|
+
for (const tag of tags) {
|
|
7514
|
+
const pos = shaToPosition.get(tag.sha);
|
|
7515
|
+
if (pos !== void 0) {
|
|
7516
|
+
for (let i = Math.max(0, pos - TAG_ADJACENT_DISTANCE); i <= pos + TAG_ADJACENT_DISTANCE; i++) {
|
|
7517
|
+
tagPositions.add(i);
|
|
7518
|
+
}
|
|
7519
|
+
}
|
|
7520
|
+
}
|
|
7521
|
+
const fileStats = /* @__PURE__ */ new Map();
|
|
7522
|
+
const scoredCommits = [];
|
|
7523
|
+
const belowThreshold = [];
|
|
7524
|
+
const minorInterest = [];
|
|
7525
|
+
for (let i = 0; i < rawCommits.length; i++) {
|
|
7526
|
+
const raw = rawCommits[i];
|
|
7527
|
+
const commit = this.parseCommit(raw);
|
|
7528
|
+
const isTagAdjacent = tagPositions.has(i);
|
|
7529
|
+
const quickSignals = this.detectSignals(commit, null, tagShas, isTagAdjacent);
|
|
7530
|
+
const quickScore = this.calculateScore(quickSignals);
|
|
7531
|
+
let stats = null;
|
|
7532
|
+
let finalSignals = quickSignals;
|
|
7533
|
+
let finalScore = quickScore;
|
|
7534
|
+
if (quickScore >= threshold - STAT_FETCH_MARGIN || options?.fetchAllStats) {
|
|
7535
|
+
const rawStats = this.git.getCommitStats(commit.sha, cwd);
|
|
7536
|
+
stats = this.parseStats(rawStats);
|
|
7537
|
+
finalSignals = this.detectSignals(commit, stats, tagShas, isTagAdjacent);
|
|
7538
|
+
finalScore = this.calculateScore(finalSignals);
|
|
7539
|
+
for (const entry of rawStats) {
|
|
7540
|
+
const existing = fileStats.get(entry.path) ?? { commits: 0, lines: 0 };
|
|
7541
|
+
existing.commits++;
|
|
7542
|
+
existing.lines += (entry.added ?? 0) + (entry.deleted ?? 0);
|
|
7543
|
+
fileStats.set(entry.path, existing);
|
|
7544
|
+
}
|
|
7545
|
+
}
|
|
7546
|
+
const scored = {
|
|
7547
|
+
commit,
|
|
7548
|
+
stats,
|
|
7549
|
+
signals: finalSignals,
|
|
7550
|
+
score: finalScore,
|
|
7551
|
+
passedTriage: finalScore >= threshold
|
|
7552
|
+
};
|
|
7553
|
+
if (finalScore >= threshold) {
|
|
7554
|
+
scoredCommits.push(scored);
|
|
7555
|
+
} else if (finalScore > 0) {
|
|
7556
|
+
minorInterest.push(scored);
|
|
7557
|
+
} else {
|
|
7558
|
+
belowThreshold.push(scored);
|
|
7559
|
+
}
|
|
7560
|
+
}
|
|
7561
|
+
scoredCommits.sort((a, b) => b.score - a.score);
|
|
7562
|
+
const hotspots = Array.from(fileStats.entries()).map(([path15, data]) => ({
|
|
7563
|
+
path: path15,
|
|
7564
|
+
commitCount: data.commits,
|
|
7565
|
+
totalLinesChanged: data.lines
|
|
7566
|
+
})).sort((a, b) => b.commitCount - a.commitCount || b.totalLinesChanged - a.totalLinesChanged).slice(0, 20);
|
|
7567
|
+
const linkedToPRs = scoredCommits.filter((s) => s.signals.prNumber !== null).length;
|
|
7568
|
+
const linkedToTags = scoredCommits.filter((s) => s.signals.isTagAdjacent).length;
|
|
7569
|
+
return {
|
|
7570
|
+
totalCommits: rawCommits.length,
|
|
7571
|
+
belowThreshold: belowThreshold.length,
|
|
7572
|
+
minorInterest: minorInterest.length,
|
|
7573
|
+
highInterest: scoredCommits,
|
|
7574
|
+
linkedToPRs,
|
|
7575
|
+
linkedToTags,
|
|
7576
|
+
hotspots,
|
|
7577
|
+
tags: tagInfo,
|
|
7578
|
+
durationMs: Date.now() - startTime
|
|
7579
|
+
};
|
|
7580
|
+
}
|
|
7581
|
+
scoreCommit(commit, stats, tagShas, isTagAdjacent) {
|
|
7582
|
+
const tagAdjacent = isTagAdjacent ?? tagShas.has(commit.sha);
|
|
7583
|
+
const signals = this.detectSignals(commit, stats, tagShas, tagAdjacent);
|
|
7584
|
+
const score = this.calculateScore(signals);
|
|
7585
|
+
return {
|
|
7586
|
+
commit,
|
|
7587
|
+
stats,
|
|
7588
|
+
signals,
|
|
7589
|
+
score,
|
|
7590
|
+
passedTriage: score >= DEFAULT_THRESHOLD
|
|
7591
|
+
};
|
|
7592
|
+
}
|
|
7593
|
+
/**
|
|
7594
|
+
* Parse raw git log commit into domain type.
|
|
7595
|
+
*/
|
|
7596
|
+
parseCommit(raw) {
|
|
7597
|
+
const refs = raw.refNames.split(",").map((r) => r.trim()).filter((r) => r && r !== "HEAD");
|
|
7598
|
+
return {
|
|
7599
|
+
sha: raw.sha,
|
|
7600
|
+
shortSha: raw.shortSha,
|
|
7601
|
+
subject: raw.subject,
|
|
7602
|
+
body: raw.body,
|
|
7603
|
+
parentCount: raw.parentShas.length,
|
|
7604
|
+
author: raw.authorName,
|
|
7605
|
+
authorEmail: raw.authorEmail,
|
|
7606
|
+
timestamp: new Date(raw.authorTimestamp * 1e3),
|
|
7607
|
+
refs
|
|
7608
|
+
};
|
|
7609
|
+
}
|
|
7610
|
+
/**
|
|
7611
|
+
* Parse raw stat entries into domain type.
|
|
7612
|
+
*/
|
|
7613
|
+
parseStats(entries) {
|
|
7614
|
+
let insertions = 0;
|
|
7615
|
+
let deletions = 0;
|
|
7616
|
+
const filesAdded = [];
|
|
7617
|
+
const filesDeleted = [];
|
|
7618
|
+
const directories = /* @__PURE__ */ new Set();
|
|
7619
|
+
for (const entry of entries) {
|
|
7620
|
+
insertions += entry.added ?? 0;
|
|
7621
|
+
deletions += entry.deleted ?? 0;
|
|
7622
|
+
if (entry.isNew) {
|
|
7623
|
+
filesAdded.push(entry.path);
|
|
7624
|
+
const dir = entry.path.split("/")[0];
|
|
7625
|
+
if (dir && dir !== entry.path) {
|
|
7626
|
+
directories.add(dir);
|
|
7627
|
+
}
|
|
7628
|
+
}
|
|
7629
|
+
if (entry.isDeleted) {
|
|
7630
|
+
filesDeleted.push(entry.path);
|
|
7631
|
+
}
|
|
7632
|
+
}
|
|
7633
|
+
return {
|
|
7634
|
+
filesChanged: entries.length,
|
|
7635
|
+
insertions,
|
|
7636
|
+
deletions,
|
|
7637
|
+
filesAdded,
|
|
7638
|
+
filesDeleted,
|
|
7639
|
+
directoriesCreated: Array.from(directories)
|
|
7640
|
+
};
|
|
7641
|
+
}
|
|
7642
|
+
/**
|
|
7643
|
+
* Detect interest signals for a commit.
|
|
7644
|
+
*/
|
|
7645
|
+
detectSignals(commit, stats, _tagShas, isTagAdjacent) {
|
|
7646
|
+
const largeDiffFiles = stats !== null && stats.filesChanged >= 10;
|
|
7647
|
+
const largeDiffLines = stats !== null && stats.insertions + stats.deletions >= 500;
|
|
7648
|
+
const prMatch = commit.subject.match(/Merge pull request #(\d+)/i) ?? commit.subject.match(/\(#(\d+)\)/) ?? commit.body.match(/PR[:\s#]+(\d+)/i);
|
|
7649
|
+
const prNumber = prMatch ? parseInt(prMatch[1], 10) : null;
|
|
7650
|
+
const mergeCommitWithPR = commit.parentCount >= 2 && prNumber !== null;
|
|
7651
|
+
const conventionalMatch = commit.subject.match(CONVENTIONAL_COMMIT_REGEX);
|
|
7652
|
+
const hasConventionalPrefix = conventionalMatch !== null;
|
|
7653
|
+
const conventionalType = conventionalMatch ? conventionalMatch[1].toLowerCase() : null;
|
|
7654
|
+
const messageText = `${commit.subject} ${commit.body}`.toLowerCase();
|
|
7655
|
+
const hasDecisionKeywords = DECISION_KEYWORDS.some((kw) => messageText.includes(kw));
|
|
7656
|
+
const createsNewDirectory = stats !== null && stats.directoriesCreated.length > 0;
|
|
7657
|
+
const hasLongMessageBody = commit.body.split("\n").filter((l) => l.trim()).length >= 2;
|
|
7658
|
+
return {
|
|
7659
|
+
largeDiffFiles,
|
|
7660
|
+
largeDiffLines,
|
|
7661
|
+
mergeCommitWithPR,
|
|
7662
|
+
hasConventionalPrefix,
|
|
7663
|
+
conventionalType,
|
|
7664
|
+
hasDecisionKeywords,
|
|
7665
|
+
createsNewDirectory,
|
|
7666
|
+
isTagAdjacent,
|
|
7667
|
+
hasLongMessageBody,
|
|
7668
|
+
prNumber
|
|
7669
|
+
};
|
|
7670
|
+
}
|
|
7671
|
+
/**
|
|
7672
|
+
* Calculate interest score from signals.
|
|
7673
|
+
*/
|
|
7674
|
+
calculateScore(signals) {
|
|
7675
|
+
let score = 0;
|
|
7676
|
+
if (signals.largeDiffFiles) score += SCORES.largeDiffFiles;
|
|
7677
|
+
if (signals.largeDiffLines) score += SCORES.largeDiffLines;
|
|
7678
|
+
if (signals.mergeCommitWithPR) score += SCORES.mergeCommitWithPR;
|
|
7679
|
+
if (signals.hasConventionalPrefix) score += SCORES.conventionalPrefix;
|
|
7680
|
+
if (signals.hasDecisionKeywords) score += SCORES.decisionKeywords;
|
|
7681
|
+
if (signals.createsNewDirectory) score += SCORES.createsNewDirectory;
|
|
7682
|
+
if (signals.isTagAdjacent) score += SCORES.tagAdjacent;
|
|
7683
|
+
if (signals.hasLongMessageBody) score += SCORES.longMessageBody;
|
|
7684
|
+
return score;
|
|
7685
|
+
}
|
|
7686
|
+
/**
|
|
7687
|
+
* Get default "since" date (3 months ago).
|
|
7688
|
+
*/
|
|
7689
|
+
getDefaultSince() {
|
|
7690
|
+
const date = /* @__PURE__ */ new Date();
|
|
7691
|
+
date.setDate(date.getDate() - DEFAULT_DAYS);
|
|
7692
|
+
return date;
|
|
7693
|
+
}
|
|
7694
|
+
/**
|
|
7695
|
+
* Check if a tag name looks like a version tag.
|
|
7696
|
+
*/
|
|
7697
|
+
isVersionTag(name) {
|
|
7698
|
+
return /^v?\d+\.\d+(\.\d+)?/.test(name);
|
|
7699
|
+
}
|
|
7700
|
+
/**
|
|
7701
|
+
* Create an empty result (for repos with no commits in range).
|
|
7702
|
+
*/
|
|
7703
|
+
emptyResult(startTime, tags) {
|
|
7704
|
+
return {
|
|
7705
|
+
totalCommits: 0,
|
|
7706
|
+
belowThreshold: 0,
|
|
7707
|
+
minorInterest: 0,
|
|
7708
|
+
highInterest: [],
|
|
7709
|
+
linkedToPRs: 0,
|
|
7710
|
+
linkedToTags: 0,
|
|
7711
|
+
hotspots: [],
|
|
7712
|
+
tags,
|
|
7713
|
+
durationMs: Date.now() - startTime
|
|
7714
|
+
};
|
|
7715
|
+
}
|
|
7716
|
+
};
|
|
7717
|
+
}
|
|
7718
|
+
});
|
|
7719
|
+
|
|
7360
7720
|
// src/lib/infrastructure/git/GitClient.ts
|
|
7361
7721
|
var GitClient_exports = {};
|
|
7362
7722
|
__export(GitClient_exports, {
|
|
@@ -7367,7 +7727,7 @@ var init_GitClient = __esm({
|
|
|
7367
7727
|
"src/lib/infrastructure/git/GitClient.ts"() {
|
|
7368
7728
|
"use strict";
|
|
7369
7729
|
import_child_process6 = require("child_process");
|
|
7370
|
-
GitClient = class {
|
|
7730
|
+
GitClient = class _GitClient {
|
|
7371
7731
|
log(options) {
|
|
7372
7732
|
const args = ["log"];
|
|
7373
7733
|
if (options.since) args.push(`--since=${options.since}`);
|
|
@@ -7425,6 +7785,154 @@ var init_GitClient = __esm({
|
|
|
7425
7785
|
return false;
|
|
7426
7786
|
}
|
|
7427
7787
|
}
|
|
7788
|
+
static {
|
|
7789
|
+
/**
|
|
7790
|
+
* Record separator for parsing detailed log output.
|
|
7791
|
+
* Using ASCII RS (Record Separator) to avoid conflicts with commit content.
|
|
7792
|
+
*/
|
|
7793
|
+
this.RECORD_SEP = "";
|
|
7794
|
+
}
|
|
7795
|
+
static {
|
|
7796
|
+
this.FIELD_SEP = "";
|
|
7797
|
+
}
|
|
7798
|
+
logDetailed(options) {
|
|
7799
|
+
const format = [
|
|
7800
|
+
"%H",
|
|
7801
|
+
// full sha
|
|
7802
|
+
"%h",
|
|
7803
|
+
// short sha
|
|
7804
|
+
"%P",
|
|
7805
|
+
// parent shas (space-separated)
|
|
7806
|
+
"%s",
|
|
7807
|
+
// subject
|
|
7808
|
+
"%b",
|
|
7809
|
+
// body
|
|
7810
|
+
"%an",
|
|
7811
|
+
// author name
|
|
7812
|
+
"%ae",
|
|
7813
|
+
// author email
|
|
7814
|
+
"%at",
|
|
7815
|
+
// author timestamp (unix)
|
|
7816
|
+
"%D"
|
|
7817
|
+
// ref names
|
|
7818
|
+
].join(_GitClient.FIELD_SEP);
|
|
7819
|
+
const args = [
|
|
7820
|
+
"log",
|
|
7821
|
+
`--format=${_GitClient.RECORD_SEP}${format}`
|
|
7822
|
+
];
|
|
7823
|
+
if (options.since) args.push(`--since=${options.since}`);
|
|
7824
|
+
if (options.until) args.push(`--until=${options.until}`);
|
|
7825
|
+
if (options.maxCount) args.push(`-n${options.maxCount}`);
|
|
7826
|
+
try {
|
|
7827
|
+
const output = (0, import_child_process6.execFileSync)("git", args, {
|
|
7828
|
+
encoding: "utf8",
|
|
7829
|
+
cwd: options.cwd,
|
|
7830
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
7831
|
+
maxBuffer: 50 * 1024 * 1024
|
|
7832
|
+
// 50MB buffer for large repos
|
|
7833
|
+
}).trim();
|
|
7834
|
+
if (!output) return [];
|
|
7835
|
+
const records = output.split(_GitClient.RECORD_SEP).filter((r) => r.trim());
|
|
7836
|
+
return records.map((record) => {
|
|
7837
|
+
const fields = record.split(_GitClient.FIELD_SEP);
|
|
7838
|
+
return {
|
|
7839
|
+
sha: fields[0] || "",
|
|
7840
|
+
shortSha: fields[1] || "",
|
|
7841
|
+
parentShas: (fields[2] || "").split(" ").filter((s) => s),
|
|
7842
|
+
subject: fields[3] || "",
|
|
7843
|
+
body: (fields[4] || "").trim(),
|
|
7844
|
+
authorName: fields[5] || "",
|
|
7845
|
+
authorEmail: fields[6] || "",
|
|
7846
|
+
authorTimestamp: parseInt(fields[7] || "0", 10),
|
|
7847
|
+
refNames: fields[8] || ""
|
|
7848
|
+
};
|
|
7849
|
+
});
|
|
7850
|
+
} catch {
|
|
7851
|
+
return [];
|
|
7852
|
+
}
|
|
7853
|
+
}
|
|
7854
|
+
getCommitStats(sha, cwd) {
|
|
7855
|
+
try {
|
|
7856
|
+
const output = (0, import_child_process6.execFileSync)(
|
|
7857
|
+
"git",
|
|
7858
|
+
["show", "--numstat", "--format=", sha],
|
|
7859
|
+
{
|
|
7860
|
+
encoding: "utf8",
|
|
7861
|
+
cwd,
|
|
7862
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
7863
|
+
}
|
|
7864
|
+
).trim();
|
|
7865
|
+
if (!output) return [];
|
|
7866
|
+
const statusOutput = (0, import_child_process6.execFileSync)(
|
|
7867
|
+
"git",
|
|
7868
|
+
["show", "--name-status", "--format=", sha],
|
|
7869
|
+
{
|
|
7870
|
+
encoding: "utf8",
|
|
7871
|
+
cwd,
|
|
7872
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
7873
|
+
}
|
|
7874
|
+
).trim();
|
|
7875
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
7876
|
+
for (const line of statusOutput.split("\n")) {
|
|
7877
|
+
const match = line.match(/^([AMDRC])\t(.+)$/);
|
|
7878
|
+
if (match) {
|
|
7879
|
+
statusMap.set(match[2], match[1]);
|
|
7880
|
+
}
|
|
7881
|
+
}
|
|
7882
|
+
return output.split("\n").filter((line) => line.trim()).map((line) => {
|
|
7883
|
+
const parts = line.split(" ");
|
|
7884
|
+
const added = parts[0] === "-" ? null : parseInt(parts[0], 10);
|
|
7885
|
+
const deleted = parts[1] === "-" ? null : parseInt(parts[1], 10);
|
|
7886
|
+
const path15 = parts[2] || "";
|
|
7887
|
+
const status = statusMap.get(path15) || "M";
|
|
7888
|
+
return {
|
|
7889
|
+
added,
|
|
7890
|
+
deleted,
|
|
7891
|
+
path: path15,
|
|
7892
|
+
isNew: status === "A",
|
|
7893
|
+
isDeleted: status === "D"
|
|
7894
|
+
};
|
|
7895
|
+
});
|
|
7896
|
+
} catch {
|
|
7897
|
+
return [];
|
|
7898
|
+
}
|
|
7899
|
+
}
|
|
7900
|
+
listTags(cwd) {
|
|
7901
|
+
try {
|
|
7902
|
+
const output = (0, import_child_process6.execFileSync)(
|
|
7903
|
+
"git",
|
|
7904
|
+
["tag", "-l", "--format=%(refname:short) %(objectname)"],
|
|
7905
|
+
{
|
|
7906
|
+
encoding: "utf8",
|
|
7907
|
+
cwd,
|
|
7908
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
7909
|
+
}
|
|
7910
|
+
).trim();
|
|
7911
|
+
if (!output) return [];
|
|
7912
|
+
return output.split("\n").filter((line) => line.trim()).map((line) => {
|
|
7913
|
+
const [name, sha] = line.split(" ");
|
|
7914
|
+
return { name: name || "", sha: sha || "" };
|
|
7915
|
+
});
|
|
7916
|
+
} catch {
|
|
7917
|
+
return [];
|
|
7918
|
+
}
|
|
7919
|
+
}
|
|
7920
|
+
countCommits(cwd) {
|
|
7921
|
+
try {
|
|
7922
|
+
const output = (0, import_child_process6.execFileSync)(
|
|
7923
|
+
"git",
|
|
7924
|
+
["rev-list", "--count", "HEAD"],
|
|
7925
|
+
{
|
|
7926
|
+
encoding: "utf8",
|
|
7927
|
+
cwd,
|
|
7928
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
7929
|
+
}
|
|
7930
|
+
).trim();
|
|
7931
|
+
return parseInt(output, 10) || 0;
|
|
7932
|
+
} catch {
|
|
7933
|
+
return 0;
|
|
7934
|
+
}
|
|
7935
|
+
}
|
|
7428
7936
|
};
|
|
7429
7937
|
}
|
|
7430
7938
|
});
|
|
@@ -7438,6 +7946,7 @@ var init_SessionStartHandler = __esm({
|
|
|
7438
7946
|
init_SessionContextFormatter();
|
|
7439
7947
|
init_GitIntrospectionService();
|
|
7440
7948
|
init_MemoryContextLoader();
|
|
7949
|
+
init_GitTriageService();
|
|
7441
7950
|
RECENT_HOURS2 = 24;
|
|
7442
7951
|
SessionStartHandler = class {
|
|
7443
7952
|
constructor(contextOrServices, memoryOrGitClient, tasks, mcp, router, logger, githubSync, gitClient) {
|
|
@@ -7451,6 +7960,7 @@ var init_SessionStartHandler = __esm({
|
|
|
7451
7960
|
this.router = services.router;
|
|
7452
7961
|
this.logger = services.logger;
|
|
7453
7962
|
this.githubSync = services.githubSync;
|
|
7963
|
+
this.commitEnricher = services.commitEnricher;
|
|
7454
7964
|
resolvedGitClient = memoryOrGitClient;
|
|
7455
7965
|
} else {
|
|
7456
7966
|
this.context = contextOrServices;
|
|
@@ -7475,6 +7985,7 @@ var init_SessionStartHandler = __esm({
|
|
|
7475
7985
|
this.router,
|
|
7476
7986
|
this.logger
|
|
7477
7987
|
);
|
|
7988
|
+
this.triageService = new GitTriageService(resolvedGitClient);
|
|
7478
7989
|
}
|
|
7479
7990
|
/**
|
|
7480
7991
|
* Handle a session start request.
|
|
@@ -7483,13 +7994,19 @@ var init_SessionStartHandler = __esm({
|
|
|
7483
7994
|
const { hierarchicalGroupIds, projectAliases, branch, projectName, userName, folderType, projectRoot } = this.context;
|
|
7484
7995
|
await this.syncGitHubOnStartup(request.trigger, hierarchicalGroupIds, projectName);
|
|
7485
7996
|
const dateOptions = this.computeDateOptions(request.trigger);
|
|
7486
|
-
const memories = await
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7997
|
+
const [memories, gitTriage] = await Promise.all([
|
|
7998
|
+
this.memoryLoader.loadMemory(
|
|
7999
|
+
hierarchicalGroupIds,
|
|
8000
|
+
projectAliases,
|
|
8001
|
+
branch,
|
|
8002
|
+
dateOptions
|
|
8003
|
+
),
|
|
8004
|
+
this.runGitTriage(dateOptions.since, projectRoot)
|
|
8005
|
+
]);
|
|
8006
|
+
const gitCommits = gitTriage ? [] : await this.gitService.loadGitCommits(dateOptions.since, projectRoot);
|
|
8007
|
+
if (gitTriage && request.trigger === "startup") {
|
|
8008
|
+
this.enrichAndSaveCommits(gitTriage.highInterest, hierarchicalGroupIds[0]).catch((err) => this.logger?.warn("Commit enrichment failed", { error: err.message }));
|
|
8009
|
+
}
|
|
7493
8010
|
const tasks = this.processTasks(memories.tasks);
|
|
7494
8011
|
const taskCounts = this.countTasks(tasks);
|
|
7495
8012
|
const contextContent = this.formatter.formatContextContent(
|
|
@@ -7499,7 +8016,8 @@ var init_SessionStartHandler = __esm({
|
|
|
7499
8016
|
taskCounts,
|
|
7500
8017
|
{ projectName, userName, folderType, projectRoot, branch },
|
|
7501
8018
|
gitCommits,
|
|
7502
|
-
dateOptions.since
|
|
8019
|
+
dateOptions.since,
|
|
8020
|
+
gitTriage
|
|
7503
8021
|
);
|
|
7504
8022
|
const message = this.formatter.getTriggerMessage(request.trigger, memories.timedOut);
|
|
7505
8023
|
return {
|
|
@@ -7536,6 +8054,96 @@ var init_SessionStartHandler = __esm({
|
|
|
7536
8054
|
this.logger?.warn("GitHub sync failed", { error: error.message });
|
|
7537
8055
|
}
|
|
7538
8056
|
}
|
|
8057
|
+
/**
|
|
8058
|
+
* Run git triage to analyze commit history.
|
|
8059
|
+
* Returns null if triage fails (non-blocking).
|
|
8060
|
+
*/
|
|
8061
|
+
async runGitTriage(since, projectRoot) {
|
|
8062
|
+
try {
|
|
8063
|
+
const result = await this.triageService.triage({
|
|
8064
|
+
since,
|
|
8065
|
+
cwd: projectRoot
|
|
8066
|
+
});
|
|
8067
|
+
this.logger?.debug("Git triage completed", {
|
|
8068
|
+
total: result.totalCommits,
|
|
8069
|
+
highInterest: result.highInterest.length,
|
|
8070
|
+
durationMs: result.durationMs
|
|
8071
|
+
});
|
|
8072
|
+
return result;
|
|
8073
|
+
} catch (error) {
|
|
8074
|
+
this.logger?.warn("Git triage failed", { error: error.message });
|
|
8075
|
+
return null;
|
|
8076
|
+
}
|
|
8077
|
+
}
|
|
8078
|
+
/**
|
|
8079
|
+
* Enrich high-interest commits and save facts to memory (fire-and-forget).
|
|
8080
|
+
*/
|
|
8081
|
+
async enrichAndSaveCommits(commits, groupId) {
|
|
8082
|
+
if (!this.commitEnricher || commits.length === 0) return;
|
|
8083
|
+
try {
|
|
8084
|
+
const existingShas = await this.checkEnrichedCommits(groupId);
|
|
8085
|
+
const toEnrich = commits.filter((c) => !existingShas.has(c.commit.shortSha));
|
|
8086
|
+
if (toEnrich.length === 0) {
|
|
8087
|
+
this.logger?.debug("All commits already enriched, skipping");
|
|
8088
|
+
return;
|
|
8089
|
+
}
|
|
8090
|
+
this.logger?.debug("Enriching commits", { count: toEnrich.length });
|
|
8091
|
+
const result = await this.commitEnricher.enrich(toEnrich, { maxCommits: 5 });
|
|
8092
|
+
if (result.facts.length === 0) {
|
|
8093
|
+
this.logger?.debug("No facts extracted from commits");
|
|
8094
|
+
return;
|
|
8095
|
+
}
|
|
8096
|
+
const shaToShort = /* @__PURE__ */ new Map();
|
|
8097
|
+
for (const commit of toEnrich) {
|
|
8098
|
+
if (commit.commit.sha && commit.commit.shortSha) {
|
|
8099
|
+
shaToShort.set(commit.commit.sha, commit.commit.shortSha);
|
|
8100
|
+
shaToShort.set(commit.commit.shortSha, commit.commit.shortSha);
|
|
8101
|
+
}
|
|
8102
|
+
}
|
|
8103
|
+
for (const fact of result.facts) {
|
|
8104
|
+
const commitSha = shaToShort.get(fact.commitSha) ?? fact.commitSha;
|
|
8105
|
+
const tags = [
|
|
8106
|
+
"type:commit-enrichment",
|
|
8107
|
+
`commit:${commitSha}`,
|
|
8108
|
+
`factType:${fact.type}`,
|
|
8109
|
+
`confidence:${fact.confidence}`,
|
|
8110
|
+
"source:git-enrichment",
|
|
8111
|
+
...fact.tags.map((t) => `tag:${t}`)
|
|
8112
|
+
];
|
|
8113
|
+
await this.memory.addFact(groupId, fact.text, tags);
|
|
8114
|
+
}
|
|
8115
|
+
this.logger?.info("Commit enrichment complete", {
|
|
8116
|
+
processed: result.commitsProcessed,
|
|
8117
|
+
facts: result.facts.length,
|
|
8118
|
+
inputTokens: result.usage.inputTokens,
|
|
8119
|
+
outputTokens: result.usage.outputTokens
|
|
8120
|
+
});
|
|
8121
|
+
} catch (error) {
|
|
8122
|
+
this.logger?.warn("Commit enrichment error", { error: error.message });
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
8125
|
+
/**
|
|
8126
|
+
* Check which commits have already been enriched (by short SHA tag in memory).
|
|
8127
|
+
*/
|
|
8128
|
+
async checkEnrichedCommits(groupId) {
|
|
8129
|
+
const enrichedShas = /* @__PURE__ */ new Set();
|
|
8130
|
+
try {
|
|
8131
|
+
const existingFacts = await this.memory.searchFacts(
|
|
8132
|
+
[groupId],
|
|
8133
|
+
"type:commit-enrichment",
|
|
8134
|
+
100
|
|
8135
|
+
);
|
|
8136
|
+
for (const fact of existingFacts) {
|
|
8137
|
+
const shaTag = fact.tags?.find((t) => t.startsWith("commit:"));
|
|
8138
|
+
if (shaTag) {
|
|
8139
|
+
enrichedShas.add(shaTag.replace("commit:", ""));
|
|
8140
|
+
}
|
|
8141
|
+
}
|
|
8142
|
+
} catch (error) {
|
|
8143
|
+
this.logger?.debug("Could not check existing enrichments", { error: error.message });
|
|
8144
|
+
}
|
|
8145
|
+
return enrichedShas;
|
|
8146
|
+
}
|
|
7539
8147
|
/**
|
|
7540
8148
|
* Compute date options based on trigger type.
|
|
7541
8149
|
*/
|
|
@@ -10561,6 +11169,7 @@ var INFRA_TOKENS = {
|
|
|
10561
11169
|
LlmGuard: /* @__PURE__ */ Symbol.for("Lisa.LlmGuard"),
|
|
10562
11170
|
SummarizationService: /* @__PURE__ */ Symbol.for("Lisa.SummarizationService"),
|
|
10563
11171
|
TranscriptEnricher: /* @__PURE__ */ Symbol.for("Lisa.TranscriptEnricher"),
|
|
11172
|
+
CommitEnricher: /* @__PURE__ */ Symbol.for("Lisa.CommitEnricher"),
|
|
10564
11173
|
LlmDeduplicationEnhancer: /* @__PURE__ */ Symbol.for("Lisa.LlmDeduplicationEnhancer"),
|
|
10565
11174
|
NlCurationService: /* @__PURE__ */ Symbol.for("Lisa.NlCurationService")
|
|
10566
11175
|
};
|
|
@@ -11152,6 +11761,7 @@ var Logger = class _Logger {
|
|
|
11152
11761
|
this.getCorrelationId = getCorrelationId2 ?? (() => void 0);
|
|
11153
11762
|
this.bindings = bindings ?? {};
|
|
11154
11763
|
this.boundContext = boundContext ?? {};
|
|
11764
|
+
this.asyncWrites = options.asyncWrites ?? false;
|
|
11155
11765
|
const logDir = import_path2.default.resolve(options.logDir);
|
|
11156
11766
|
this.ensureLogDir(logDir);
|
|
11157
11767
|
this.logFile = import_path2.default.join(logDir, `lisa-${getDateString()}.log`);
|
|
@@ -11187,6 +11797,7 @@ var Logger = class _Logger {
|
|
|
11187
11797
|
}
|
|
11188
11798
|
/**
|
|
11189
11799
|
* Write a log entry.
|
|
11800
|
+
* Uses async (fire-and-forget) writes when asyncWrites is enabled.
|
|
11190
11801
|
*/
|
|
11191
11802
|
writeLog(level, levelStr, message, context) {
|
|
11192
11803
|
if (!this.shouldLog(level)) return;
|
|
@@ -11195,9 +11806,18 @@ var Logger = class _Logger {
|
|
|
11195
11806
|
if (this.options.enableFile) {
|
|
11196
11807
|
const fileLine = `${timestamp} ${levelStr.padEnd(5)} ${message}${contextStr}
|
|
11197
11808
|
`;
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11809
|
+
if (this.asyncWrites) {
|
|
11810
|
+
void (async () => {
|
|
11811
|
+
try {
|
|
11812
|
+
await import_fs.default.promises.appendFile(this.logFile, fileLine);
|
|
11813
|
+
} catch {
|
|
11814
|
+
}
|
|
11815
|
+
})();
|
|
11816
|
+
} else {
|
|
11817
|
+
try {
|
|
11818
|
+
import_fs.default.appendFileSync(this.logFile, fileLine);
|
|
11819
|
+
} catch {
|
|
11820
|
+
}
|
|
11201
11821
|
}
|
|
11202
11822
|
}
|
|
11203
11823
|
if (this.options.enableConsole) {
|
|
@@ -11226,7 +11846,7 @@ var Logger = class _Logger {
|
|
|
11226
11846
|
}
|
|
11227
11847
|
child(bindings) {
|
|
11228
11848
|
return new _Logger(
|
|
11229
|
-
this.options,
|
|
11849
|
+
{ ...this.options, asyncWrites: this.asyncWrites },
|
|
11230
11850
|
void 0,
|
|
11231
11851
|
this.getCorrelationId,
|
|
11232
11852
|
{ ...this.bindings, ...bindings },
|
|
@@ -11295,7 +11915,7 @@ var Logger = class _Logger {
|
|
|
11295
11915
|
*/
|
|
11296
11916
|
withContext(context) {
|
|
11297
11917
|
return new _Logger(
|
|
11298
|
-
this.options,
|
|
11918
|
+
{ ...this.options, asyncWrites: this.asyncWrites },
|
|
11299
11919
|
void 0,
|
|
11300
11920
|
this.getCorrelationId,
|
|
11301
11921
|
this.bindings,
|
|
@@ -14649,6 +15269,266 @@ function extractJson3(text) {
|
|
|
14649
15269
|
return null;
|
|
14650
15270
|
}
|
|
14651
15271
|
|
|
15272
|
+
// src/lib/infrastructure/services/CommitEnricher.ts
|
|
15273
|
+
init_ICommitEnricher();
|
|
15274
|
+
init_IMemoryQuality();
|
|
15275
|
+
|
|
15276
|
+
// src/lib/infrastructure/services/prompts/commit-extraction.ts
|
|
15277
|
+
var DEFAULT_MAX_FACTS3 = 10;
|
|
15278
|
+
function buildCommitExtractionPrompt(commits, options = {}) {
|
|
15279
|
+
const extractTypes = options.extractTypes;
|
|
15280
|
+
const maxFacts = Math.min(commits.length * 2, DEFAULT_MAX_FACTS3);
|
|
15281
|
+
const validTypes = extractTypes && extractTypes.length > 0 ? extractTypes : [
|
|
15282
|
+
"feature",
|
|
15283
|
+
"decision",
|
|
15284
|
+
"refactor",
|
|
15285
|
+
"migration",
|
|
15286
|
+
"bugfix",
|
|
15287
|
+
"breaking-change",
|
|
15288
|
+
"convention",
|
|
15289
|
+
"dependency"
|
|
15290
|
+
];
|
|
15291
|
+
const system = [
|
|
15292
|
+
"You are analyzing git commits to extract structured facts for project memory.",
|
|
15293
|
+
"Your task is to identify important information worth remembering long-term.",
|
|
15294
|
+
"",
|
|
15295
|
+
"Fact types to extract:",
|
|
15296
|
+
"- feature: New functionality or capability added to the project",
|
|
15297
|
+
"- decision: Architecture or technology choice evident from the commit",
|
|
15298
|
+
"- refactor: Code restructuring without behavior change",
|
|
15299
|
+
"- migration: Data migration, API migration, or technology migration",
|
|
15300
|
+
"- bugfix: Bug fix with root cause (what was broken and why)",
|
|
15301
|
+
"- breaking-change: Changes that break backward compatibility",
|
|
15302
|
+
"- convention: New patterns or coding conventions introduced",
|
|
15303
|
+
"- dependency: Significant dependency additions, updates, or removals",
|
|
15304
|
+
"",
|
|
15305
|
+
"Confidence levels:",
|
|
15306
|
+
"- high: Clear from commit message, subject, or explicit description",
|
|
15307
|
+
"- medium: Reasonably inferred from commit context and signals",
|
|
15308
|
+
"- low: Tentative inference, may need verification",
|
|
15309
|
+
"",
|
|
15310
|
+
"Guidelines:",
|
|
15311
|
+
"- Each fact should be self-contained and understandable without the commit context.",
|
|
15312
|
+
"- Include relevant file paths, technologies, or components in tags.",
|
|
15313
|
+
"- For bugfixes, try to identify the root cause if evident from the message.",
|
|
15314
|
+
"- Focus on facts that would be valuable for future developers.",
|
|
15315
|
+
"- Do not extract trivial changes (formatting, typos, version bumps).",
|
|
15316
|
+
`- Extract at most ${maxFacts} facts total across all commits.`,
|
|
15317
|
+
extractTypes && extractTypes.length > 0 ? `- Only extract facts of these types: ${extractTypes.join(", ")}` : "- Extract facts of any type listed above.",
|
|
15318
|
+
"",
|
|
15319
|
+
"Respond with ONLY a valid JSON object in this exact format:",
|
|
15320
|
+
"{",
|
|
15321
|
+
' "facts": [',
|
|
15322
|
+
" {",
|
|
15323
|
+
' "text": "Clear description of the fact",',
|
|
15324
|
+
` "type": "one of: ${validTypes.join(", ")}",`,
|
|
15325
|
+
' "confidence": "high | medium | low",',
|
|
15326
|
+
' "tags": ["relevant", "tags"],',
|
|
15327
|
+
' "commitSha": "short SHA of the source commit",',
|
|
15328
|
+
' "rationale": "Why this fact is worth remembering"',
|
|
15329
|
+
" }",
|
|
15330
|
+
" ]",
|
|
15331
|
+
"}"
|
|
15332
|
+
].join("\n");
|
|
15333
|
+
const user = formatCommitsForPrompt(commits);
|
|
15334
|
+
return { system, user };
|
|
15335
|
+
}
|
|
15336
|
+
function formatCommitsForPrompt(commits) {
|
|
15337
|
+
const parts = [];
|
|
15338
|
+
parts.push("## Commits to Analyze");
|
|
15339
|
+
parts.push("");
|
|
15340
|
+
for (let i = 0; i < commits.length; i++) {
|
|
15341
|
+
const { commit, stats, signals } = commits[i];
|
|
15342
|
+
parts.push(`### Commit ${i + 1}: ${commit.shortSha}`);
|
|
15343
|
+
parts.push(`**Subject:** ${commit.subject}`);
|
|
15344
|
+
if (commit.body && commit.body.trim().length > 0) {
|
|
15345
|
+
const truncatedBody = commit.body.length > 500 ? commit.body.slice(0, 500) + "..." : commit.body;
|
|
15346
|
+
parts.push(`**Body:** ${truncatedBody}`);
|
|
15347
|
+
}
|
|
15348
|
+
parts.push(`**Timestamp:** ${commit.timestamp.toISOString()}`);
|
|
15349
|
+
parts.push(`**Author:** ${commit.author}`);
|
|
15350
|
+
if (stats) {
|
|
15351
|
+
parts.push(`**Files changed:** ${stats.filesChanged}`);
|
|
15352
|
+
parts.push(`**Lines:** +${stats.insertions} -${stats.deletions}`);
|
|
15353
|
+
if (stats.directoriesCreated.length > 0) {
|
|
15354
|
+
parts.push(`**New directories:** ${stats.directoriesCreated.join(", ")}`);
|
|
15355
|
+
}
|
|
15356
|
+
}
|
|
15357
|
+
const signalList = formatSignals(signals);
|
|
15358
|
+
if (signalList.length > 0) {
|
|
15359
|
+
parts.push(`**Signals:** ${signalList.join(", ")}`);
|
|
15360
|
+
}
|
|
15361
|
+
parts.push("");
|
|
15362
|
+
}
|
|
15363
|
+
return parts.join("\n");
|
|
15364
|
+
}
|
|
15365
|
+
function formatSignals(signals) {
|
|
15366
|
+
const result = [];
|
|
15367
|
+
if (signals.largeDiffFiles) {
|
|
15368
|
+
result.push("large-diff-files");
|
|
15369
|
+
}
|
|
15370
|
+
if (signals.largeDiffLines) {
|
|
15371
|
+
result.push("large-diff-lines");
|
|
15372
|
+
}
|
|
15373
|
+
if (signals.mergeCommitWithPR && signals.prNumber) {
|
|
15374
|
+
result.push(`merge-PR-#${signals.prNumber}`);
|
|
15375
|
+
}
|
|
15376
|
+
if (signals.hasConventionalPrefix && signals.conventionalType) {
|
|
15377
|
+
result.push(`conventional:${signals.conventionalType}`);
|
|
15378
|
+
}
|
|
15379
|
+
if (signals.hasDecisionKeywords) {
|
|
15380
|
+
result.push("decision-keywords");
|
|
15381
|
+
}
|
|
15382
|
+
if (signals.createsNewDirectory) {
|
|
15383
|
+
result.push("new-directory");
|
|
15384
|
+
}
|
|
15385
|
+
if (signals.isTagAdjacent) {
|
|
15386
|
+
result.push("tag-adjacent");
|
|
15387
|
+
}
|
|
15388
|
+
if (signals.hasLongMessageBody) {
|
|
15389
|
+
result.push("detailed-message");
|
|
15390
|
+
}
|
|
15391
|
+
return result;
|
|
15392
|
+
}
|
|
15393
|
+
|
|
15394
|
+
// src/lib/infrastructure/services/CommitEnricher.ts
|
|
15395
|
+
var DEFAULT_MAX_COMMITS = 5;
|
|
15396
|
+
var DEFAULT_MAX_TOKENS = 4096;
|
|
15397
|
+
var EMPTY_USAGE2 = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
15398
|
+
function createCommitEnricher(llmGuard, logger) {
|
|
15399
|
+
return {
|
|
15400
|
+
async enrich(commits, options = {}) {
|
|
15401
|
+
try {
|
|
15402
|
+
const skipShas = new Set(options.skipShas ?? []);
|
|
15403
|
+
const filteredCommits = commits.filter((c) => !skipShas.has(c.commit.shortSha));
|
|
15404
|
+
if (filteredCommits.length === 0) {
|
|
15405
|
+
return {
|
|
15406
|
+
facts: [],
|
|
15407
|
+
commitsProcessed: 0,
|
|
15408
|
+
commitsSkipped: commits.length,
|
|
15409
|
+
usage: EMPTY_USAGE2
|
|
15410
|
+
};
|
|
15411
|
+
}
|
|
15412
|
+
const maxCommits = Math.max(0, options.maxCommits ?? DEFAULT_MAX_COMMITS);
|
|
15413
|
+
const toProcess = filteredCommits.slice(0, maxCommits);
|
|
15414
|
+
if (toProcess.length === 0) {
|
|
15415
|
+
return {
|
|
15416
|
+
facts: [],
|
|
15417
|
+
commitsProcessed: 0,
|
|
15418
|
+
commitsSkipped: commits.length,
|
|
15419
|
+
usage: EMPTY_USAGE2
|
|
15420
|
+
};
|
|
15421
|
+
}
|
|
15422
|
+
const skipped = commits.length - toProcess.length;
|
|
15423
|
+
const allowedShas = new Set(
|
|
15424
|
+
toProcess.flatMap((c) => [c.commit.sha, c.commit.shortSha].filter(Boolean))
|
|
15425
|
+
);
|
|
15426
|
+
const prompt = buildCommitExtractionPrompt(toProcess, options);
|
|
15427
|
+
const response = await llmGuard.complete(prompt.user, "extraction", {
|
|
15428
|
+
systemPrompt: prompt.system,
|
|
15429
|
+
temperature: 0.3,
|
|
15430
|
+
maxTokens: options.maxTokens ?? DEFAULT_MAX_TOKENS
|
|
15431
|
+
});
|
|
15432
|
+
const parsed = parseCommitExtractionResponse(response.text, options, allowedShas);
|
|
15433
|
+
return {
|
|
15434
|
+
facts: parsed.facts,
|
|
15435
|
+
commitsProcessed: toProcess.length,
|
|
15436
|
+
commitsSkipped: skipped,
|
|
15437
|
+
usage: response.usage
|
|
15438
|
+
};
|
|
15439
|
+
} catch (error) {
|
|
15440
|
+
const errorMsg = error instanceof Error ? error.message.split("\n")[0] : "Unknown error";
|
|
15441
|
+
logger?.warn("Commit enrichment failed, returning empty result", {
|
|
15442
|
+
error: errorMsg
|
|
15443
|
+
});
|
|
15444
|
+
return {
|
|
15445
|
+
facts: [],
|
|
15446
|
+
commitsProcessed: 0,
|
|
15447
|
+
commitsSkipped: commits.length,
|
|
15448
|
+
usage: EMPTY_USAGE2
|
|
15449
|
+
};
|
|
15450
|
+
}
|
|
15451
|
+
}
|
|
15452
|
+
};
|
|
15453
|
+
}
|
|
15454
|
+
function parseCommitExtractionResponse(responseText, options = {}, allowedShas) {
|
|
15455
|
+
const jsonText = extractJson4(responseText);
|
|
15456
|
+
if (!jsonText) {
|
|
15457
|
+
return { facts: [] };
|
|
15458
|
+
}
|
|
15459
|
+
let parsed;
|
|
15460
|
+
try {
|
|
15461
|
+
parsed = JSON.parse(jsonText);
|
|
15462
|
+
} catch {
|
|
15463
|
+
return { facts: [] };
|
|
15464
|
+
}
|
|
15465
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
15466
|
+
return { facts: [] };
|
|
15467
|
+
}
|
|
15468
|
+
const obj = parsed;
|
|
15469
|
+
const rawFacts = Array.isArray(obj.facts) ? obj.facts : [];
|
|
15470
|
+
const allowedTypes = options.extractTypes;
|
|
15471
|
+
const facts = [];
|
|
15472
|
+
for (const raw of rawFacts) {
|
|
15473
|
+
const fact = validateCommitFact(raw, allowedTypes, allowedShas);
|
|
15474
|
+
if (fact) {
|
|
15475
|
+
facts.push(fact);
|
|
15476
|
+
}
|
|
15477
|
+
}
|
|
15478
|
+
return { facts };
|
|
15479
|
+
}
|
|
15480
|
+
function extractJson4(text) {
|
|
15481
|
+
const trimmed = text.trim();
|
|
15482
|
+
let start = trimmed.indexOf("{");
|
|
15483
|
+
while (start !== -1) {
|
|
15484
|
+
for (let end = trimmed.lastIndexOf("}"); end > start; end = trimmed.lastIndexOf("}", end - 1)) {
|
|
15485
|
+
const candidate = trimmed.slice(start, end + 1);
|
|
15486
|
+
try {
|
|
15487
|
+
JSON.parse(candidate);
|
|
15488
|
+
return candidate;
|
|
15489
|
+
} catch {
|
|
15490
|
+
}
|
|
15491
|
+
}
|
|
15492
|
+
start = trimmed.indexOf("{", start + 1);
|
|
15493
|
+
}
|
|
15494
|
+
return null;
|
|
15495
|
+
}
|
|
15496
|
+
function validateCommitFact(raw, allowedTypes, allowedShas) {
|
|
15497
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
15498
|
+
const obj = raw;
|
|
15499
|
+
if (typeof obj.text !== "string" || obj.text.trim().length === 0) return null;
|
|
15500
|
+
if (typeof obj.type !== "string" || !isValidCommitFactType(obj.type)) return null;
|
|
15501
|
+
if (allowedTypes && allowedTypes.length > 0 && !allowedTypes.includes(obj.type)) {
|
|
15502
|
+
return null;
|
|
15503
|
+
}
|
|
15504
|
+
if (typeof obj.commitSha !== "string" || obj.commitSha.trim().length === 0) return null;
|
|
15505
|
+
const commitSha = obj.commitSha.trim();
|
|
15506
|
+
if (allowedShas && !allowedShas.has(commitSha)) return null;
|
|
15507
|
+
let confidence = "medium";
|
|
15508
|
+
if (typeof obj.confidence === "string") {
|
|
15509
|
+
if (isValidConfidence(obj.confidence)) {
|
|
15510
|
+
confidence = obj.confidence;
|
|
15511
|
+
}
|
|
15512
|
+
}
|
|
15513
|
+
const tags = [];
|
|
15514
|
+
if (Array.isArray(obj.tags)) {
|
|
15515
|
+
for (const tag of obj.tags) {
|
|
15516
|
+
if (typeof tag === "string" && tag.trim().length > 0) {
|
|
15517
|
+
tags.push(tag.trim());
|
|
15518
|
+
}
|
|
15519
|
+
}
|
|
15520
|
+
}
|
|
15521
|
+
const rationale = typeof obj.rationale === "string" && obj.rationale.trim().length > 0 ? obj.rationale.trim() : void 0;
|
|
15522
|
+
return {
|
|
15523
|
+
text: obj.text.trim(),
|
|
15524
|
+
type: obj.type,
|
|
15525
|
+
confidence,
|
|
15526
|
+
tags,
|
|
15527
|
+
commitSha,
|
|
15528
|
+
rationale
|
|
15529
|
+
};
|
|
15530
|
+
}
|
|
15531
|
+
|
|
14652
15532
|
// src/lib/domain/interfaces/dal/types.ts
|
|
14653
15533
|
var DEFAULT_QUERY_OPTIONS = {
|
|
14654
15534
|
limit: 10,
|
|
@@ -16375,7 +17255,7 @@ async function bootstrapContainer(config = {}) {
|
|
|
16375
17255
|
container.registerInstance(TOKENS.ApiKey, apiKey);
|
|
16376
17256
|
}
|
|
16377
17257
|
container.registerInstance(TOKENS.ServiceConfig, config);
|
|
16378
|
-
const logger = config.logger ?? (config.disableLogging ? createNullLogger() : createLogger());
|
|
17258
|
+
const logger = config.logger ?? (config.disableLogging ? createNullLogger() : createLogger({ asyncWrites: config.asyncLogging }));
|
|
16379
17259
|
container.registerInstance(TOKENS.Logger, logger);
|
|
16380
17260
|
const context = ContextDetector.detect(projectRoot);
|
|
16381
17261
|
container.registerInstance(TOKENS.Context, context);
|
|
@@ -16438,6 +17318,15 @@ async function bootstrapContainer(config = {}) {
|
|
|
16438
17318
|
},
|
|
16439
17319
|
"transient"
|
|
16440
17320
|
);
|
|
17321
|
+
container.register(
|
|
17322
|
+
TOKENS.CommitEnricher,
|
|
17323
|
+
async () => {
|
|
17324
|
+
const guard = await container.resolve(TOKENS.LlmGuard);
|
|
17325
|
+
const log = logger.child({ service: "commit-enricher" });
|
|
17326
|
+
return createCommitEnricher(guard, log);
|
|
17327
|
+
},
|
|
17328
|
+
"transient"
|
|
17329
|
+
);
|
|
16441
17330
|
container.register(
|
|
16442
17331
|
TOKENS.SessionCaptureService,
|
|
16443
17332
|
async () => {
|