refactor(graph_overview): 概览各列表加数量上限,止住上下文膨胀

用"最新一窗 + 计数/pull 兜底"替换 hot/cold 拆分与 balancedCap:
- recent_facts 20、recent_done_intents 12、open_intents 30(按 priority)
- cold_digests 15,按最新成员时间(max member id)降序,溢出的更旧 digest
  只给裸 id(cold_digests_more),仍可 expand_digest 展开
- 新增 CountOpenIntents,frontier_open 报开放意图真实总数(与截断视图解耦)
- 删除 balancedCap/unfoldedCap/unfolded_truncated 及其测试

掉出窗口的节点不丢:facts/done_intents_total/frontier_open/findings_total
计数在,list_facts/list_findings/node_detail/expand_digest 可按需回捞
This commit is contained in:
Autumn-27 committed 2026-09-24 13:05:27 -04:00
1 parent 43d1743dc0
commit 3aac6754a8
4 files changed
+93 -99

No files matched your search

+38 -64
View File
@@ -12,28 +12,6 @@ import (
actool "github.com/Autumn-27/norma/tool"
)
// balancedCap decides how many done-intents and facts to keep when the combined
// unfolded settled/cold region exceeds cap (§6.3). The smaller side is kept whole;
// the larger side takes the remaining budget; neither is starved below cap/2.
// Returns the keep counts and whether truncation applied. Newest-first slices, so
// callers keep the head.
func balancedCap(done, facts, cap int) (keepDone, keepFacts int, truncated bool) {
if done+facts <= cap {
return done, facts, false
}
half := cap / 2
keepDone, keepFacts = done, facts
switch {
case done > half && facts > half:
keepDone, keepFacts = half, cap-half
case done > half:
keepDone = cap - facts // facts fit in ≤half; intents take the rest
default:
keepFacts = cap - done // intents fit in ≤half; facts take the rest
}
return keepDone, keepFacts, true
}
// compactIntents distills intents to {id, summary, state, asset_ids, parents,
// yields} so the planner sees both the direction and its LINEAGE — parents (the
// upstream nodes it derived from: facts/intents/findings) and yields (the facts/
@@ -421,16 +399,17 @@ func (t *ToolSet) graphOverviewData() map[string]any {
// intent derived from it) must reappear this round — so `hidden` folds a member out
// only when it is covered AND still cold.
covered, _ := t.ts.CoveredMembers()
// Hot set at render time serves two §6 needs: (1) a covered member that revived
// (now hot) must reappear this round; (2) the 60-cap must never truncate hot
// (active-context) nodes — only the cold-but-unfolded region is cappable (§6.3).
// Computed every round (cheap for real graph sizes); nil map degrades safely.
// Render-time hot set (ancestor of a live intent / fact under a live intent).
// §6 revival check: a covered member that revived (now hot) must NOT stay folded
// — hidden() only folds a member out when it is covered AND still cold. Computed
// every round (cheap for real graph sizes); nil map degrades safely.
var hotAtRender map[int64]bool
if cg, _, err := loadColdGraph(t.ts); err == nil {
hotAtRender = cg.hotSet()
}
hidden := func(id int64) bool { _, c := covered[id]; return c && !hotAtRender[id] }
fr, _ := t.ts.Frontier(100)
const openIntentsCap = 30
fr, _ := t.ts.Frontier(openIntentsCap) // priority DESC, id ASC —— 优先级最高的前 N 条;真实总数见 frontier_open
out["open_intents"] = compactIntents(fr, parentsOf, yieldsOf)
all, _ := t.ts.ListByKind(db.KindIntent, 300)
var running, recentDone []*db.Node
@@ -442,19 +421,22 @@ func (t *ToolSet) graphOverviewData() map[string]any {
if hidden(n.ID) {
continue // in a cold_digest and still cold — shown via cold_digests (§6.2)
}
recentDone = append(recentDone, n) // §6: no 15-cap; folded ones are gone, cap applied below
recentDone = append(recentDone, n) // 最新在前(all 按 id 降序);折叠的已剔除,输出时截最新 N
}
}
out["running_intents"] = compactIntents(running, parentsOf, yieldsOf)
// recent_done_intents is finalized further below (after facts are known) so the
// 60-cap can balance the two lists and protect hot nodes (§6.3).
// done_intents_total:已结束意图(done/blocked/exhausted)总数,与 recent_done_intents
// 平行命名——后者只是它的最新窗口(≤15)截断视图。两键并排即自描述:"看到的是 N/总数",
// 平行命名——后者只是它的最新窗口截断视图。两键并排即自描述:"看到的是 N/总数",
// 让 planner 去重时别把"没显示"当成"没派过",无需在提示词里另行解释。
if dt, err := t.ts.CountFinishedIntents(); err == nil {
out["done_intents_total"] = dt
}
out["frontier_open"] = len(fr)
// frontier_open:开放意图真实总数(open_intents 只是其中优先级最高的前 N 条截断视图)。
if fo, err := t.ts.CountOpenIntents(); err == nil {
out["frontier_open"] = fo
} else {
out["frontier_open"] = len(fr)
}
// findings (confirmed vulns) and facts (worker exploration results) are
// now distinct node kinds. recent_facts surfaces fact summaries (esp.
// negative results) so the planner sees them in one call; full content
@@ -482,12 +464,17 @@ func (t *ToolSet) graphOverviewData() map[string]any {
findingList = append(findingList, m)
}
out["finding_list"] = findingList
// Build facts, split hot (active context — a fact under a live intent) from cold
// (not-yet-folded). Hidden (folded & still cold) ones are surfaced via cold_digests.
var recentFactsHot, recentFactsCold []map[string]any
// recent_facts:非折叠事实里最新的一窗(≤N,factNodes 按 id 降序即最新在前)。已折进
// digest 且仍冷的(hidden)走 cold_digests,不在此重复。每条 {id, summary, from_intent?,
// confidence?};evidence 等详情用 node_detail(id)。更早的用 list_facts 翻。
const recentFactsCap = 20
recentFacts := make([]map[string]any, 0, recentFactsCap)
for _, n := range factNodes {
if len(recentFacts) >= recentFactsCap {
break
}
if hidden(n.ID) {
continue // in a cold_digest and still cold — surfaced via cold_digests (§6.2)
continue // 已折进 digest 且仍冷 —— 见 cold_digests
}
m := compactNode(n)
if from := factFrom[n.ID]; from > 0 {
@@ -501,37 +488,24 @@ func (t *ToolSet) graphOverviewData() map[string]any {
m["confidence"] = c
}
}
if hotAtRender[n.ID] {
recentFactsHot = append(recentFactsHot, m)
} else {
recentFactsCold = append(recentFactsCold, m)
}
recentFacts = append(recentFacts, m)
}
// Split the settled intents the same way: hot = ancestor of a live intent (active
// context); cold = not-yet-folded settled.
var doneHot, doneCold []*db.Node
for _, n := range recentDone {
if hotAtRender[n.ID] {
doneHot = append(doneHot, n)
} else {
doneCold = append(doneCold, n)
}
out["recent_facts"] = recentFacts
// recent_done_intents:非折叠的已结束意图里最新的一窗(≤N,recentDone 已按 id 降序)。
// 更早的看 done_intents_total 计数 + node_detail(id)。
const recentDoneCap = 12
if len(recentDone) > recentDoneCap {
recentDone = recentDone[:recentDoneCap]
}
// §6.3 兜底截断:hot(活跃探索上下文——live 前沿的祖先链、live 意图下的新事实)【永不】
// 被截;60-cap 只约束【冷但未折】的残余(压缩滞后时才增长)。live(open/running) 另有字段、
// 同样不受影响。冷区两侧均衡保留:较少一侧全留、较多一侧填余额,各自保底 cap/2,绝不饿到 0。
const unfoldedCap = 60
keepColdDone, keepColdFacts, truncated := balancedCap(len(doneCold), len(recentFactsCold), unfoldedCap)
if truncated {
out["unfolded_truncated"] = true // 正常不触发;触发说明压缩滞后
}
out["recent_done_intents"] = append(
compactIntents(doneHot, parentsOf, yieldsOf),
compactIntents(doneCold[:keepColdDone], parentsOf, yieldsOf)...)
out["recent_facts"] = append(recentFactsHot, recentFactsCold[:keepColdFacts]...) // {id, summary, from_intent, confidence?};详情用 node_detail(id)
// cold-digest §6.1: the folded cold region as flat digest bodies.
if cds := activeDigestBodies(t.ts); len(cds) > 0 {
out["recent_done_intents"] = compactIntents(recentDone, parentsOf, yieldsOf)
// cold-digest §6.1: 折叠冷区的 digest body,按最新成员时间降序取前 N;被截的更旧 digest
// 只给裸 id(仍可 expand_digest 展开),避免冷区唯一出口被无限拉长。
const coldDigestsCap = 15
if cds, more := t.coldDigestsRecent(coldDigestsCap); len(cds) > 0 {
out["cold_digests"] = cds // [{id, body, member_count}] —— 直接读 body (§6.1)
if len(more) > 0 {
out["cold_digests_more"] = more // 被截断的更旧 digest 的 id;用 expand_digest(id) 展开
}
}
// the original task (root) so the planner always has it, not just the
// decomposed goals.