@titan-design/session-graph 0.9.0 → 0.10.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/README.md +94 -11
- package/dist/index.d.ts +72 -7
- package/dist/index.js +310 -20
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -254,6 +254,45 @@ function applyEpisodeTranscriptSchema(db) {
|
|
|
254
254
|
addColumnIfMissing(db, EPISODE_TABLE, "end_transcript_id", "INTEGER");
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// src/audit-schema-v7.ts
|
|
258
|
+
var ORIGIN_TASK_LINK_MIGRATION_NAME = "origin task link";
|
|
259
|
+
function applyOriginTaskLinkSchema(db) {
|
|
260
|
+
addColumnIfMissing(db, "session_origin", "task_ids", "TEXT");
|
|
261
|
+
addColumnIfMissing(db, "session_origin", "task_source", "TEXT");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/audit-schema-v8.ts
|
|
265
|
+
var REVIEW_VERDICT_MIGRATION_NAME = "review verdicts";
|
|
266
|
+
var REVIEW_TABLE = "pr_review";
|
|
267
|
+
var REVIEW_DDL = `
|
|
268
|
+
CREATE TABLE IF NOT EXISTS ${REVIEW_TABLE} (
|
|
269
|
+
source_key TEXT PRIMARY KEY,
|
|
270
|
+
surface TEXT NOT NULL,
|
|
271
|
+
verdict TEXT NOT NULL,
|
|
272
|
+
ts TEXT NOT NULL,
|
|
273
|
+
session_id TEXT,
|
|
274
|
+
transcript_id INTEGER,
|
|
275
|
+
repo TEXT,
|
|
276
|
+
repo_hint TEXT,
|
|
277
|
+
cwd_repo TEXT,
|
|
278
|
+
number INTEGER NOT NULL,
|
|
279
|
+
pr_ref TEXT
|
|
280
|
+
);
|
|
281
|
+
CREATE INDEX IF NOT EXISTS idx_${REVIEW_TABLE}_pr_ref ON ${REVIEW_TABLE}(pr_ref);
|
|
282
|
+
CREATE INDEX IF NOT EXISTS idx_${REVIEW_TABLE}_transcript ON ${REVIEW_TABLE}(transcript_id);
|
|
283
|
+
`;
|
|
284
|
+
var PR_COLUMNS = [
|
|
285
|
+
["review_rounds_gh", "INTEGER"],
|
|
286
|
+
["review_rounds_chat", "INTEGER"],
|
|
287
|
+
["commit_times", "TEXT"]
|
|
288
|
+
];
|
|
289
|
+
function applyReviewVerdictSchema(db) {
|
|
290
|
+
db.exec(REVIEW_DDL);
|
|
291
|
+
for (const [column, type] of PR_COLUMNS) addColumnIfMissing(db, "pr", column, type);
|
|
292
|
+
db.exec("UPDATE pr SET review_rounds_gh = review_rounds");
|
|
293
|
+
db.exec("UPDATE pr SET outcome_checked_at = NULL");
|
|
294
|
+
}
|
|
295
|
+
|
|
257
296
|
// src/normalized-schema.ts
|
|
258
297
|
import { conversationRef } from "@titan-design/agent-protocol";
|
|
259
298
|
var NORMALIZED_DDL = `
|
|
@@ -451,6 +490,7 @@ var DERIVED_TABLES = [
|
|
|
451
490
|
"pr",
|
|
452
491
|
"pr_merge_observation",
|
|
453
492
|
"pr_create_observation",
|
|
493
|
+
REVIEW_TABLE,
|
|
454
494
|
"branch",
|
|
455
495
|
"file",
|
|
456
496
|
"task",
|
|
@@ -465,7 +505,9 @@ var MIGRATIONS = [
|
|
|
465
505
|
} },
|
|
466
506
|
{ version: 4, name: AUDIT_MIGRATION_NAME, up: applyAuditSchema },
|
|
467
507
|
{ version: 5, name: ORIGIN_MIGRATION_NAME, up: applyOriginSchema },
|
|
468
|
-
{ version: 6, name: EPISODE_TRANSCRIPT_MIGRATION_NAME, up: applyEpisodeTranscriptSchema }
|
|
508
|
+
{ version: 6, name: EPISODE_TRANSCRIPT_MIGRATION_NAME, up: applyEpisodeTranscriptSchema },
|
|
509
|
+
{ version: 7, name: ORIGIN_TASK_LINK_MIGRATION_NAME, up: applyOriginTaskLinkSchema },
|
|
510
|
+
{ version: 8, name: REVIEW_VERDICT_MIGRATION_NAME, up: applyReviewVerdictSchema }
|
|
469
511
|
];
|
|
470
512
|
|
|
471
513
|
// src/graph.ts
|
|
@@ -626,7 +668,7 @@ function advanceFacet(db, transcriptId, fromByteOffset, indexedTo, version = EXT
|
|
|
626
668
|
}
|
|
627
669
|
|
|
628
670
|
// src/apply.ts
|
|
629
|
-
import { sessionRef } from "@titan-design/session-read";
|
|
671
|
+
import { RELATIONS, sessionRef } from "@titan-design/session-read";
|
|
630
672
|
function applyDelta(graph, transcriptId, delta, source = {}) {
|
|
631
673
|
graph.db.transaction(() => {
|
|
632
674
|
applyFacts(graph.db, transcriptId, delta);
|
|
@@ -635,6 +677,7 @@ function applyDelta(graph, transcriptId, delta, source = {}) {
|
|
|
635
677
|
applyAssets(graph.db, delta);
|
|
636
678
|
applyPhases(graph.db, transcriptId, delta);
|
|
637
679
|
applyLinkedRows(graph, transcriptId, delta);
|
|
680
|
+
applyReviewVerdicts(graph.db, transcriptId, delta);
|
|
638
681
|
applyAudit(graph.db, transcriptId, delta);
|
|
639
682
|
})();
|
|
640
683
|
}
|
|
@@ -702,6 +745,43 @@ function pick(row, sql) {
|
|
|
702
745
|
const names = new Set([...sql.matchAll(/@(\w+)/g)].map((m) => m[1]));
|
|
703
746
|
return Object.fromEntries([...names].map((name) => [name, row[name] ?? null]));
|
|
704
747
|
}
|
|
748
|
+
var UPSERT_CHAT_VERDICT = `
|
|
749
|
+
INSERT INTO ${REVIEW_TABLE} (source_key, surface, verdict, ts, session_id, transcript_id, repo, repo_hint, cwd_repo, number)
|
|
750
|
+
VALUES (@sourceKey, 'chat', @verdict, @ts, @sessionId, @transcriptId, @repo, @repoHint, @cwdRepo, @number)
|
|
751
|
+
ON CONFLICT (source_key) DO UPDATE SET verdict = excluded.verdict, ts = excluded.ts, session_id = excluded.session_id,
|
|
752
|
+
transcript_id = excluded.transcript_id, repo = excluded.repo, repo_hint = excluded.repo_hint,
|
|
753
|
+
cwd_repo = excluded.cwd_repo, number = excluded.number`;
|
|
754
|
+
function applyReviewVerdicts(db, transcriptId, delta) {
|
|
755
|
+
const upsert = db.prepare(UPSERT_CHAT_VERDICT);
|
|
756
|
+
const keys = chatVerdictKeys(delta.reviewVerdicts);
|
|
757
|
+
delta.reviewVerdicts.forEach((v, i) => {
|
|
758
|
+
upsert.run({
|
|
759
|
+
sourceKey: keys[i],
|
|
760
|
+
verdict: v.verdict,
|
|
761
|
+
ts: v.ts,
|
|
762
|
+
sessionId: v.sessionId,
|
|
763
|
+
transcriptId,
|
|
764
|
+
repo: v.repo,
|
|
765
|
+
repoHint: v.repoHint,
|
|
766
|
+
cwdRepo: v.cwdRepo,
|
|
767
|
+
number: v.number
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
var hasOrdinal = (v) => Number.isInteger(v.ordinal) && v.ordinal >= 0;
|
|
772
|
+
function chatVerdictKeys(verdicts) {
|
|
773
|
+
const used = /* @__PURE__ */ new Map();
|
|
774
|
+
const usedBy = (id) => used.get(id) ?? used.set(id, /* @__PURE__ */ new Set()).get(id);
|
|
775
|
+
for (const v of verdicts) if (hasOrdinal(v)) usedBy(v.toolUseId).add(v.ordinal);
|
|
776
|
+
return verdicts.map((v) => {
|
|
777
|
+
if (hasOrdinal(v)) return `chat:${v.toolUseId}:${v.ordinal}`;
|
|
778
|
+
const taken = usedBy(v.toolUseId);
|
|
779
|
+
let n = 0;
|
|
780
|
+
while (taken.has(n)) n += 1;
|
|
781
|
+
taken.add(n);
|
|
782
|
+
return `chat:${v.toolUseId}:${n}`;
|
|
783
|
+
});
|
|
784
|
+
}
|
|
705
785
|
function applyPhases(db, transcriptId, delta) {
|
|
706
786
|
const current = db.prepare("SELECT phase_id, to_mode FROM permission_phase WHERE session_id = ? AND trigger = ? AND t_invalid IS NULL ORDER BY phase_id DESC LIMIT 1");
|
|
707
787
|
const close = db.prepare("UPDATE permission_phase SET t_invalid = ? WHERE phase_id = ?");
|
|
@@ -734,11 +814,16 @@ function applyLinkedRows(graph, transcriptId, delta) {
|
|
|
734
814
|
for (const s of delta.subagents) upsertSubagent.run(s.agentRef, s.sessionId, s.agentType, s.label, s.ts || null, factId(s.byteOffset));
|
|
735
815
|
const linkTranscript = db.prepare("INSERT INTO subagent (agent_ref, child_session_id) VALUES (?, ?) ON CONFLICT (agent_ref) DO UPDATE SET child_session_id = excluded.child_session_id");
|
|
736
816
|
for (const l of delta.subagentTranscripts) linkTranscript.run(l.agentRef, l.childSessionId);
|
|
737
|
-
for (const e of delta.edges) graph
|
|
817
|
+
for (const e of delta.edges) assertTranscriptEdge(graph, { sourceRef: e.sourceRef, relation: e.relation, targetRef: e.targetRef, tValid: e.ts, factId: factId(e.byteOffset) });
|
|
738
818
|
for (const span of delta.spans) {
|
|
739
819
|
graph.spans.index({ ownerRef: sessionRef(span.sessionId), field: span.field, sourceId: transcriptId, byteOffset: span.byteOffset, byteLength: span.byteLength }, span.text);
|
|
740
820
|
}
|
|
741
821
|
}
|
|
822
|
+
function assertTranscriptEdge(graph, edge) {
|
|
823
|
+
const existing = edge.relation === RELATIONS.RAN ? graph.edges.current(edge.sourceRef, edge.relation, edge.targetRef) : void 0;
|
|
824
|
+
if (existing?.attrs?.via === "origin") graph.edges.supersede(edge);
|
|
825
|
+
else graph.edges.assert(edge);
|
|
826
|
+
}
|
|
742
827
|
function factIdFor(db, transcriptId, byteOffset) {
|
|
743
828
|
const row = db.prepare("SELECT fact_id FROM fact WHERE transcript_id = ? AND byte_offset = ?").get(transcriptId, byteOffset);
|
|
744
829
|
return row?.fact_id ?? null;
|
|
@@ -755,7 +840,7 @@ function purgeTranscript(graph, transcriptId) {
|
|
|
755
840
|
graph.db.prepare(`DELETE FROM "${table}" WHERE session_id IN (SELECT session_id FROM session WHERE transcript_id = ?)`).run(transcriptId);
|
|
756
841
|
}
|
|
757
842
|
graph.db.prepare(`DELETE FROM "${KIT.edge}" WHERE fact_id IN (SELECT fact_id FROM fact WHERE transcript_id = ?)`).run(transcriptId);
|
|
758
|
-
for (const table of [...AUDIT_TABLES, FACET_TABLE]) {
|
|
843
|
+
for (const table of [...AUDIT_TABLES, FACET_TABLE, REVIEW_TABLE]) {
|
|
759
844
|
graph.db.prepare(`DELETE FROM "${table}" WHERE transcript_id = ?`).run(transcriptId);
|
|
760
845
|
}
|
|
761
846
|
graph.db.prepare("DELETE FROM fact WHERE transcript_id = ?").run(transcriptId);
|
|
@@ -921,18 +1006,126 @@ import { promises as fs } from "fs";
|
|
|
921
1006
|
import { contentHash, resumePoint } from "@titan-design/locator";
|
|
922
1007
|
import { TranscriptParseError as TranscriptParseError2, extractTranscript as extractTranscript2 } from "@titan-design/session-read";
|
|
923
1008
|
|
|
1009
|
+
// src/review-rounds.ts
|
|
1010
|
+
import { RELATIONS as RELATIONS2 } from "@titan-design/session-read";
|
|
1011
|
+
var isReviewerProfile = (profile) => profile === "reviewer" || profile.endsWith("-reviewer");
|
|
1012
|
+
function countRounds(changesRequestedAt, commitTimes) {
|
|
1013
|
+
const commits = parseCommitTimes(commitTimes);
|
|
1014
|
+
return commits ? answeredHeads(changesRequestedAt, commits).heads.size : null;
|
|
1015
|
+
}
|
|
1016
|
+
function parseCommitTimes(commitTimes) {
|
|
1017
|
+
const commits = commitTimes.map(Date.parse);
|
|
1018
|
+
return commits.some(Number.isNaN) ? null : commits;
|
|
1019
|
+
}
|
|
1020
|
+
function answeredHeads(reviewTimes, commits) {
|
|
1021
|
+
const heads = /* @__PURE__ */ new Set();
|
|
1022
|
+
let ignored = 0;
|
|
1023
|
+
for (const ts of reviewTimes) {
|
|
1024
|
+
const at = Date.parse(ts);
|
|
1025
|
+
if (Number.isNaN(at)) {
|
|
1026
|
+
ignored += 1;
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
const head = commits.filter((c) => c <= at).length;
|
|
1030
|
+
if (head < commits.length) heads.add(head);
|
|
1031
|
+
}
|
|
1032
|
+
return { heads, ignored };
|
|
1033
|
+
}
|
|
1034
|
+
function projectReviewRounds(graph, options = {}) {
|
|
1035
|
+
const isReviewer = options.isReviewerProfile ?? isReviewerProfile;
|
|
1036
|
+
return graph.db.transaction(() => {
|
|
1037
|
+
const resolved = resolveChatVerdicts(graph.db);
|
|
1038
|
+
const invalidTimes = writeRounds(graph.db, isReviewer);
|
|
1039
|
+
const { n } = graph.db.prepare(`SELECT count(*) AS n FROM ${REVIEW_TABLE} WHERE surface = 'chat' AND pr_ref IS NULL`).get();
|
|
1040
|
+
return { resolved, unresolved: n, invalidTimes };
|
|
1041
|
+
})();
|
|
1042
|
+
}
|
|
1043
|
+
function resolveChatVerdicts(db) {
|
|
1044
|
+
const rows = db.prepare(`SELECT source_key, session_id, repo, repo_hint, cwd_repo, number FROM ${REVIEW_TABLE} WHERE surface = 'chat' AND pr_ref IS NULL`).all();
|
|
1045
|
+
const update = db.prepare(`UPDATE ${REVIEW_TABLE} SET pr_ref = ? WHERE source_key = ?`);
|
|
1046
|
+
let resolved = 0;
|
|
1047
|
+
for (const row of rows) {
|
|
1048
|
+
const prRef = resolvePrRef(db, row);
|
|
1049
|
+
if (prRef) resolved += update.run(prRef, row.source_key).changes;
|
|
1050
|
+
}
|
|
1051
|
+
return resolved;
|
|
1052
|
+
}
|
|
1053
|
+
function resolvePrRef(db, row) {
|
|
1054
|
+
const candidates = db.prepare("SELECT pr_ref, repo FROM pr WHERE number = ? AND repo IS NOT NULL").all(row.number);
|
|
1055
|
+
if (row.repo) return candidates.find((c) => c.repo === row.repo)?.pr_ref ?? null;
|
|
1056
|
+
const hinted = candidates.filter((c) => sameRepoName(c.repo, row.repo_hint));
|
|
1057
|
+
if (hinted.length === 1) return hinted[0].pr_ref;
|
|
1058
|
+
const pool = hinted.length > 0 ? hinted : candidates;
|
|
1059
|
+
const linked = familyLinks(db, row.session_id);
|
|
1060
|
+
const bySibling = pool.filter((c) => linked.has(c.pr_ref));
|
|
1061
|
+
if (bySibling.length === 1) return bySibling[0].pr_ref;
|
|
1062
|
+
const byCwd = (bySibling.length > 0 ? bySibling : pool).filter((c) => sameRepoName(c.repo, row.cwd_repo));
|
|
1063
|
+
return byCwd.length === 1 ? byCwd[0].pr_ref : null;
|
|
1064
|
+
}
|
|
1065
|
+
function sameRepoName(repo, name) {
|
|
1066
|
+
return name !== null && repo.split("/").pop().toLowerCase() === name.toLowerCase();
|
|
1067
|
+
}
|
|
1068
|
+
var FAMILY_LINKS = `
|
|
1069
|
+
WITH parent(id) AS (
|
|
1070
|
+
SELECT parent_session_id FROM session_origin WHERE session_id = @sessionId AND parent_session_id IS NOT NULL
|
|
1071
|
+
UNION SELECT session_id FROM subagent WHERE child_session_id = @sessionId AND session_id IS NOT NULL
|
|
1072
|
+
), family(id) AS (
|
|
1073
|
+
SELECT @sessionId UNION SELECT id FROM parent
|
|
1074
|
+
UNION SELECT session_id FROM session_origin WHERE parent_session_id IN (SELECT id FROM parent)
|
|
1075
|
+
UNION SELECT child_session_id FROM subagent WHERE session_id IN (SELECT id FROM parent) AND child_session_id IS NOT NULL
|
|
1076
|
+
UNION SELECT substr(target_ref, 9) FROM "${KIT.edge}" WHERE relation = '${RELATIONS2.SPAWNED}' AND t_expired IS NULL
|
|
1077
|
+
AND target_ref LIKE 'session:%' AND source_ref IN (SELECT 'session:' || id FROM parent)
|
|
1078
|
+
)
|
|
1079
|
+
SELECT DISTINCT target_ref FROM "${KIT.edge}" WHERE relation = '${RELATIONS2.LINKED}' AND t_expired IS NULL
|
|
1080
|
+
AND source_ref IN (SELECT 'session:' || id FROM family)`;
|
|
1081
|
+
function familyLinks(db, sessionId) {
|
|
1082
|
+
if (!sessionId) return /* @__PURE__ */ new Set();
|
|
1083
|
+
const rows = db.prepare(FAMILY_LINKS).all({ sessionId });
|
|
1084
|
+
return new Set(rows.map((r) => r.target_ref));
|
|
1085
|
+
}
|
|
1086
|
+
var CHANGES_REQUESTED = `
|
|
1087
|
+
SELECT r.pr_ref, r.surface, r.ts, o.profile FROM ${REVIEW_TABLE} r LEFT JOIN session_origin o ON o.session_id = r.session_id
|
|
1088
|
+
WHERE r.verdict = 'changes_requested' AND r.pr_ref IS NOT NULL`;
|
|
1089
|
+
function writeRounds(db, isReviewer) {
|
|
1090
|
+
const byPr = /* @__PURE__ */ new Map();
|
|
1091
|
+
for (const review of db.prepare(CHANGES_REQUESTED).all()) {
|
|
1092
|
+
if (review.surface === "chat" && !(review.profile && isReviewer(review.profile))) continue;
|
|
1093
|
+
byPr.set(review.pr_ref, [...byPr.get(review.pr_ref) ?? [], review]);
|
|
1094
|
+
}
|
|
1095
|
+
const update = db.prepare("UPDATE pr SET review_rounds_chat = ?, review_rounds = ? WHERE pr_ref = ?");
|
|
1096
|
+
let invalidTimes = 0;
|
|
1097
|
+
for (const pr of db.prepare("SELECT pr_ref, commit_times, review_rounds_gh FROM pr").all()) {
|
|
1098
|
+
const { chat, total, invalid } = roundsFor(pr, byPr.get(pr.pr_ref) ?? []);
|
|
1099
|
+
update.run(chat, total, pr.pr_ref);
|
|
1100
|
+
invalidTimes += invalid;
|
|
1101
|
+
}
|
|
1102
|
+
return invalidTimes;
|
|
1103
|
+
}
|
|
1104
|
+
function roundsFor(pr, reviews) {
|
|
1105
|
+
const commits = pr.commit_times === null ? null : parseCommitTimes(JSON.parse(pr.commit_times));
|
|
1106
|
+
if (!commits) return { chat: null, total: pr.review_rounds_gh, invalid: pr.commit_times === null ? 0 : 1 };
|
|
1107
|
+
const timesOn = (surface) => reviews.filter((r) => r.surface === surface).map((r) => r.ts);
|
|
1108
|
+
const forge = answeredHeads(timesOn("github"), commits);
|
|
1109
|
+
const chat = answeredHeads(timesOn("chat"), commits);
|
|
1110
|
+
const chatOnly = [...chat.heads].filter((head) => !forge.heads.has(head)).length;
|
|
1111
|
+
return { chat: chat.heads.size, total: (pr.review_rounds_gh ?? 0) + chatOnly, invalid: forge.ignored + chat.ignored };
|
|
1112
|
+
}
|
|
1113
|
+
|
|
924
1114
|
// src/outcomes.ts
|
|
925
1115
|
var NO_PR_OUTCOMES = Object.freeze({ requested: 0, applied: 0, failed: false });
|
|
926
1116
|
var PRS_NEEDING_OUTCOME = `
|
|
927
1117
|
SELECT pr_ref, repo, number FROM pr
|
|
928
|
-
WHERE repo IS NOT NULL AND number IS NOT NULL
|
|
929
|
-
|
|
1118
|
+
WHERE repo IS NOT NULL AND number IS NOT NULL
|
|
1119
|
+
AND (outcome_checked_at IS NULL OR state IS NOT 'merged' OR commit_times IS NULL)
|
|
1120
|
+
ORDER BY CASE WHEN outcome_checked_at IS NULL THEN 0 WHEN state IS NOT 'merged' THEN 1 ELSE 2 END, pr_ref`;
|
|
930
1121
|
var APPLY_OUTCOME = `
|
|
931
1122
|
UPDATE pr SET
|
|
932
1123
|
state = CASE WHEN state = 'merged' THEN state ELSE COALESCE(lower(@state), state) END,
|
|
933
1124
|
merged_at = COALESCE(@mergedAt, merged_at),
|
|
934
1125
|
closed_at = COALESCE(@closedAt, closed_at),
|
|
935
1126
|
review_rounds = COALESCE(@reviewRounds, review_rounds),
|
|
1127
|
+
review_rounds_gh = COALESCE(@reviewRounds, review_rounds_gh),
|
|
1128
|
+
commit_times = COALESCE(@commitTimes, commit_times),
|
|
936
1129
|
outcome_checked_at = @checkedAt
|
|
937
1130
|
WHERE pr_ref = @prRef`;
|
|
938
1131
|
function prsNeedingOutcome(graph) {
|
|
@@ -952,6 +1145,7 @@ async function enrichPrs(graph, resolver) {
|
|
|
952
1145
|
}
|
|
953
1146
|
function write(graph, resolved, checkedAt) {
|
|
954
1147
|
const apply = graph.db.prepare(APPLY_OUTCOME);
|
|
1148
|
+
const stored = graph.db.prepare("SELECT commit_times FROM pr WHERE pr_ref = ?");
|
|
955
1149
|
return graph.db.transaction(() => {
|
|
956
1150
|
let applied = 0;
|
|
957
1151
|
for (const [prRef, fields] of resolved) {
|
|
@@ -962,27 +1156,79 @@ function write(graph, resolved, checkedAt) {
|
|
|
962
1156
|
state: fields.state ?? null,
|
|
963
1157
|
mergedAt: fields.mergedAt ?? null,
|
|
964
1158
|
closedAt: fields.closedAt ?? null,
|
|
965
|
-
reviewRounds: fields
|
|
1159
|
+
reviewRounds: forgeRounds(fields, () => storedCommitTimes(stored.get(prRef))),
|
|
1160
|
+
commitTimes: fields.commitTimes ? JSON.stringify(fields.commitTimes) : null
|
|
966
1161
|
}).changes;
|
|
1162
|
+
if (fields.reviews) replaceForgeReviews(graph, prRef, fields.reviews);
|
|
967
1163
|
}
|
|
968
1164
|
return applied;
|
|
969
1165
|
})();
|
|
970
1166
|
}
|
|
1167
|
+
var isChangesRequested = (review) => review.state.toUpperCase() === "CHANGES_REQUESTED";
|
|
1168
|
+
function forgeRounds(fields, storedTimes) {
|
|
1169
|
+
if (!fields.reviews) return fields.reviewRounds ?? null;
|
|
1170
|
+
const commitTimes = fields.commitTimes ?? storedTimes();
|
|
1171
|
+
const counted = commitTimes ? countRounds(fields.reviews.filter(isChangesRequested).map((r) => r.submittedAt), commitTimes) : null;
|
|
1172
|
+
return counted ?? fields.reviewRounds ?? null;
|
|
1173
|
+
}
|
|
1174
|
+
function storedCommitTimes(row) {
|
|
1175
|
+
const times = row?.commit_times;
|
|
1176
|
+
return times ? JSON.parse(times) : null;
|
|
1177
|
+
}
|
|
1178
|
+
var FORGE_VERDICTS = { APPROVED: "approve", CHANGES_REQUESTED: "changes_requested" };
|
|
1179
|
+
var INSERT_FORGE_REVIEW = `
|
|
1180
|
+
INSERT INTO ${REVIEW_TABLE} (source_key, surface, verdict, ts, repo, number, pr_ref)
|
|
1181
|
+
SELECT 'gh:' || pr_ref || ':' || @ts, 'github', @verdict, @ts, repo, number, pr_ref FROM pr WHERE pr_ref = @prRef
|
|
1182
|
+
ON CONFLICT (source_key) DO UPDATE SET verdict = CASE WHEN verdict = 'changes_requested' THEN verdict ELSE excluded.verdict END`;
|
|
1183
|
+
function replaceForgeReviews(graph, prRef, reviews) {
|
|
1184
|
+
graph.db.prepare(`DELETE FROM ${REVIEW_TABLE} WHERE surface = 'github' AND pr_ref = ?`).run(prRef);
|
|
1185
|
+
const insert = graph.db.prepare(INSERT_FORGE_REVIEW);
|
|
1186
|
+
for (const review of reviews) {
|
|
1187
|
+
const verdict = FORGE_VERDICTS[review.state.toUpperCase()];
|
|
1188
|
+
if (verdict) insert.run({ prRef, verdict, ts: review.submittedAt });
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
971
1191
|
|
|
972
1192
|
// src/origin.ts
|
|
973
|
-
import { RELATIONS, sessionRef as sessionRef3 } from "@titan-design/session-read";
|
|
1193
|
+
import { RELATIONS as RELATIONS3, sessionRef as sessionRef3, taskRef } from "@titan-design/session-read";
|
|
1194
|
+
var NO_TASK_LINK = "none";
|
|
974
1195
|
var NO_ORIGINS = Object.freeze({ requested: 0, applied: 0, events: 0, failed: false });
|
|
975
1196
|
var SESSIONS_NEEDING_ORIGIN = `
|
|
976
1197
|
SELECT s.session_id FROM session s LEFT JOIN session_origin o ON o.session_id = s.session_id
|
|
977
|
-
WHERE o.session_id IS NULL OR o.resolved_at < s.ended_at
|
|
1198
|
+
WHERE o.session_id IS NULL OR o.resolved_at < s.ended_at OR o.task_source IS NULL
|
|
978
1199
|
ORDER BY s.session_id`;
|
|
1200
|
+
var ORIGIN_COLUMNS = [
|
|
1201
|
+
"origin_system",
|
|
1202
|
+
"agent_id",
|
|
1203
|
+
"agent_name",
|
|
1204
|
+
"parent_name",
|
|
1205
|
+
"parent_session_id",
|
|
1206
|
+
"profile",
|
|
1207
|
+
"model_alias",
|
|
1208
|
+
"surface",
|
|
1209
|
+
"isolation",
|
|
1210
|
+
"depth",
|
|
1211
|
+
"origin_kind",
|
|
1212
|
+
"config_dir",
|
|
1213
|
+
"spawn_cwd",
|
|
1214
|
+
"spawned_at",
|
|
1215
|
+
"brief_chars",
|
|
1216
|
+
"brief_excerpt",
|
|
1217
|
+
"brief_path",
|
|
1218
|
+
"launch_args",
|
|
1219
|
+
"resolved_at"
|
|
1220
|
+
];
|
|
1221
|
+
var TASK_LINK_UPDATE = `task_source = COALESCE(excluded.task_source, task_source),
|
|
1222
|
+
task_ids = CASE WHEN excluded.task_source IS NOT NULL THEN excluded.task_ids ELSE task_ids END`;
|
|
979
1223
|
var UPSERT_ORIGIN = `
|
|
980
|
-
INSERT
|
|
1224
|
+
INSERT INTO session_origin (session_id, origin_system, agent_id, agent_name, parent_name, parent_session_id,
|
|
981
1225
|
profile, model_alias, surface, isolation, depth, origin_kind, config_dir, spawn_cwd, spawned_at,
|
|
982
|
-
brief_chars, brief_excerpt, brief_path, launch_args, resolved_at)
|
|
1226
|
+
brief_chars, brief_excerpt, brief_path, launch_args, task_ids, task_source, resolved_at)
|
|
983
1227
|
VALUES (@sessionId, @originSystem, @agentId, @agentName, @parentName, @parentSessionId,
|
|
984
1228
|
@profile, @modelAlias, @surface, @isolation, @depth, @originKind, @configDir, @spawnCwd, @spawnedAt,
|
|
985
|
-
@briefChars, @briefExcerpt, @briefPath, @launchArgs, @resolvedAt)
|
|
1229
|
+
@briefChars, @briefExcerpt, @briefPath, @launchArgs, @taskIds, @taskSource, @resolvedAt)
|
|
1230
|
+
ON CONFLICT (session_id) DO UPDATE SET ${ORIGIN_COLUMNS.map((c) => `${c} = excluded.${c}`).join(", ")},
|
|
1231
|
+
${TASK_LINK_UPDATE}`;
|
|
986
1232
|
var UPSERT_EVENT = `
|
|
987
1233
|
INSERT INTO session_external_event (session_id, ts, origin_system, kind, detail)
|
|
988
1234
|
VALUES (@sessionId, @ts, COALESCE(@originSystem, (SELECT origin_system FROM session_origin WHERE session_id = @sessionId), 'unknown'), @kind, @detail)
|
|
@@ -999,14 +1245,16 @@ function sessionsNeedingOrigin(graph) {
|
|
|
999
1245
|
async function resolveOrigins(graph, resolver) {
|
|
1000
1246
|
if (!resolver) return NO_ORIGINS;
|
|
1001
1247
|
const sessionIds = sessionsNeedingOrigin(graph);
|
|
1248
|
+
let resolved = [];
|
|
1002
1249
|
try {
|
|
1003
1250
|
const resolution = await resolver(sessionIds);
|
|
1004
1251
|
const written = writeResolution(graph, resolution, (/* @__PURE__ */ new Date()).toISOString());
|
|
1252
|
+
resolved = Object.keys(resolution.origins);
|
|
1005
1253
|
return { requested: sessionIds.length, ...written, failed: false };
|
|
1006
1254
|
} catch (err) {
|
|
1007
1255
|
return { requested: sessionIds.length, applied: 0, events: 0, failed: true, error: err instanceof Error ? err.message : String(err) };
|
|
1008
1256
|
} finally {
|
|
1009
|
-
projectOrigins(graph);
|
|
1257
|
+
projectOrigins(graph, resolved);
|
|
1010
1258
|
}
|
|
1011
1259
|
}
|
|
1012
1260
|
function writeResolution(graph, resolution, resolvedAt) {
|
|
@@ -1045,21 +1293,54 @@ function originRow(sessionId, o, resolvedAt) {
|
|
|
1045
1293
|
briefChars: o.briefChars ?? null,
|
|
1046
1294
|
briefExcerpt: o.briefExcerpt ?? null,
|
|
1047
1295
|
briefPath: o.briefPath ?? null,
|
|
1048
|
-
launchArgs: o.launchArgs ?? null
|
|
1296
|
+
launchArgs: o.launchArgs ?? null,
|
|
1297
|
+
...taskLinkColumns(o)
|
|
1049
1298
|
};
|
|
1050
1299
|
}
|
|
1051
|
-
function
|
|
1300
|
+
function taskLinkColumns(o) {
|
|
1301
|
+
if (o.taskIds === void 0) return { taskIds: null, taskSource: null };
|
|
1302
|
+
if (!o.taskIds?.length) return { taskIds: null, taskSource: NO_TASK_LINK };
|
|
1303
|
+
return { taskIds: JSON.stringify(o.taskIds), taskSource: o.taskSource ?? NO_TASK_LINK };
|
|
1304
|
+
}
|
|
1305
|
+
function projectOrigins(graph, resolved) {
|
|
1052
1306
|
const spawned = graph.db.prepare("SELECT session_id, parent_session_id, spawned_at, resolved_at FROM session_origin WHERE parent_session_id IS NOT NULL").all();
|
|
1053
1307
|
graph.db.transaction(() => {
|
|
1054
1308
|
graph.db.prepare(PROJECT_SUBAGENTS).run();
|
|
1055
1309
|
for (const o of spawned) {
|
|
1056
|
-
graph.edges.assert({ sourceRef: sessionRef3(o.parent_session_id), relation:
|
|
1310
|
+
graph.edges.assert({ sourceRef: sessionRef3(o.parent_session_id), relation: RELATIONS3.SPAWNED, targetRef: sessionRef3(o.session_id), tValid: o.spawned_at ?? o.resolved_at });
|
|
1057
1311
|
}
|
|
1312
|
+
projectTaskLinks(graph, resolved);
|
|
1058
1313
|
})();
|
|
1059
1314
|
}
|
|
1315
|
+
var LINK_CONFIDENCE = { name: 1, "name-over-brief": 1, "brief-anchor": 0.9, "brief-paragraph": 0.6 };
|
|
1316
|
+
var LOWEST_CONFIDENCE = 0.6;
|
|
1317
|
+
function projectTaskLinks(graph, resolved) {
|
|
1318
|
+
const insertTask = graph.db.prepare("INSERT OR IGNORE INTO task (task_ref, task_id) VALUES (?, ?)");
|
|
1319
|
+
const select = graph.db.prepare("SELECT session_id, task_ids, task_source, spawned_at, resolved_at FROM session_origin WHERE session_id = ?");
|
|
1320
|
+
for (const sessionId of resolved) {
|
|
1321
|
+
const o = select.get(sessionId);
|
|
1322
|
+
if (!o) continue;
|
|
1323
|
+
const taskIds = o.task_ids ? JSON.parse(o.task_ids) : [];
|
|
1324
|
+
const source = sessionRef3(o.session_id);
|
|
1325
|
+
const attrs = { via: "origin", source: o.task_source };
|
|
1326
|
+
const confidence = LINK_CONFIDENCE[o.task_source ?? ""] ?? LOWEST_CONFIDENCE;
|
|
1327
|
+
for (const taskId of taskIds) {
|
|
1328
|
+
insertTask.run(taskRef(taskId), taskId);
|
|
1329
|
+
graph.edges.assert({ sourceRef: source, relation: RELATIONS3.RAN, targetRef: taskRef(taskId), tValid: o.spawned_at ?? o.resolved_at, attrs, confidence });
|
|
1330
|
+
}
|
|
1331
|
+
expireStaleTaskLinks(graph, source, new Set(taskIds.map(taskRef)));
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
function expireStaleTaskLinks(graph, source, linked) {
|
|
1335
|
+
for (const edge of graph.edges.from(source)) {
|
|
1336
|
+
if (edge.relation === RELATIONS3.RAN && edge.attrs?.via === "origin" && !linked.has(edge.targetRef)) {
|
|
1337
|
+
graph.edges.expire(source, RELATIONS3.RAN, edge.targetRef);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1060
1341
|
|
|
1061
1342
|
// src/tasks.ts
|
|
1062
|
-
import { taskRef } from "@titan-design/session-read";
|
|
1343
|
+
import { taskRef as taskRef2 } from "@titan-design/session-read";
|
|
1063
1344
|
var NO_ENRICHMENT = Object.freeze({ requested: 0, applied: 0, failed: false });
|
|
1064
1345
|
var UPSERT_TASK = `
|
|
1065
1346
|
INSERT INTO task (task_ref, task_id, initiative, title, status, estimate) VALUES (@taskRef, @taskId, @initiative, @title, @status, @estimate)
|
|
@@ -1088,7 +1369,7 @@ function write2(graph, resolved) {
|
|
|
1088
1369
|
for (const [taskId, fields] of resolved) {
|
|
1089
1370
|
if (!fields) continue;
|
|
1090
1371
|
applied += upsert.run({
|
|
1091
|
-
taskRef:
|
|
1372
|
+
taskRef: taskRef2(taskId),
|
|
1092
1373
|
taskId,
|
|
1093
1374
|
initiative: fields.initiative ?? null,
|
|
1094
1375
|
title: fields.title ?? null,
|
|
@@ -1143,7 +1424,7 @@ async function indexTranscript(graph, transcript, options = {}) {
|
|
|
1143
1424
|
}
|
|
1144
1425
|
}
|
|
1145
1426
|
async function refreshCorpus(graph, transcripts, options = {}) {
|
|
1146
|
-
const { resolveTasks, resolveOrigins: originResolver, resolvePrs, full, facetLimit, ...perTranscript } = options;
|
|
1427
|
+
const { resolveTasks, resolveOrigins: originResolver, resolvePrs, isReviewerProfile: isReviewerProfile2, full, facetLimit, ...perTranscript } = options;
|
|
1147
1428
|
const counts = { indexed: 0, unchanged: 0, rewound: 0, missing: 0, quarantined: 0 };
|
|
1148
1429
|
const touched = [];
|
|
1149
1430
|
let facts = 0;
|
|
@@ -1159,10 +1440,11 @@ async function refreshCorpus(graph, transcripts, options = {}) {
|
|
|
1159
1440
|
const origins = await resolveOrigins(graph, originResolver);
|
|
1160
1441
|
const reconciled = reconcile(graph);
|
|
1161
1442
|
const prs = await enrichPrs(graph, resolvePrs);
|
|
1443
|
+
const reviews = projectReviewRounds(graph, { isReviewerProfile: isReviewerProfile2 });
|
|
1162
1444
|
const tasks = await enrichTasks(graph, resolveTasks, allTaskIds(graph));
|
|
1163
1445
|
const markedMissing = await markMissing(graph, transcripts);
|
|
1164
1446
|
const facetSummary = { facetsBackfilled: facets.backfilled, facetBacklog: facets.backlog };
|
|
1165
|
-
return { transcripts: transcripts.length, ...counts, facts, turnsRolledUp, reconciled, tasks, origins, prs, markedMissing, ...facetSummary };
|
|
1447
|
+
return { transcripts: transcripts.length, ...counts, facts, turnsRolledUp, reconciled, tasks, origins, prs, reviews, markedMissing, ...facetSummary };
|
|
1166
1448
|
}
|
|
1167
1449
|
async function markMissing(graph, discovered) {
|
|
1168
1450
|
const present = new Set(discovered.map((t) => t.displayPath));
|
|
@@ -1465,22 +1747,30 @@ export {
|
|
|
1465
1747
|
NO_ENRICHMENT,
|
|
1466
1748
|
NO_ORIGINS,
|
|
1467
1749
|
NO_PR_OUTCOMES,
|
|
1750
|
+
NO_TASK_LINK,
|
|
1468
1751
|
ORIGIN_DDL,
|
|
1469
1752
|
ORIGIN_MIGRATION_NAME,
|
|
1470
1753
|
ORIGIN_TABLES,
|
|
1754
|
+
ORIGIN_TASK_LINK_MIGRATION_NAME,
|
|
1471
1755
|
ORIGIN_VIEWS,
|
|
1756
|
+
REVIEW_DDL,
|
|
1757
|
+
REVIEW_TABLE,
|
|
1758
|
+
REVIEW_VERDICT_MIGRATION_NAME,
|
|
1472
1759
|
allSessionIds,
|
|
1473
1760
|
allTaskIds,
|
|
1474
1761
|
applyAudit,
|
|
1475
1762
|
applyDelta,
|
|
1476
1763
|
backfillFacets,
|
|
1764
|
+
countRounds,
|
|
1477
1765
|
enrichPrs,
|
|
1478
1766
|
enrichTasks,
|
|
1479
1767
|
indexCodexSource,
|
|
1480
1768
|
indexTranscript,
|
|
1769
|
+
isReviewerProfile,
|
|
1481
1770
|
normalizedSessions,
|
|
1482
1771
|
normalizedUsage,
|
|
1483
1772
|
openSessionGraph,
|
|
1773
|
+
projectReviewRounds,
|
|
1484
1774
|
prsNeedingOutcome,
|
|
1485
1775
|
purgeTranscript,
|
|
1486
1776
|
readIndexedText,
|