Skip to content
Merged
2 changes: 1 addition & 1 deletion docs/guide/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Local feedback affects ranking on the next index rebuild (for example, `teamai p

Index rebuilds reuse unchanged files' parsed tokens. Changes, deletions, source selection and votes still take effect on every build. `~/.teamai/debug.log` reports how many files were reused, rebuilt or dropped. Older indexes and a different Node/ICU version trigger a full parse once.

When the log has runs, `teamai stats` lists the last 10 sessions that ran recall.
When the log has runs, `teamai stats` lists the last 10 sessions that ran recall: their runs, the docs recalled and adopted, and how many runs the session opened a `Related:` line's file from.

### Enabling / Disabling Recall

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/zh-CN/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ teamai recall feedback --negative <docId>

重建索引会复用未变文件的解析和分词结果;文件修改、删除、来源选择和票数仍在每次构建时生效。`~/.teamai/debug.log` 会记录复用、重建和移除的文件数。旧索引或不同的 Node/ICU 版本会触发一次完整解析。

`teamai stats` 在有记录时,会列出最近 10 个跑过 recall 的会话。
`teamai stats` 在有记录时,会列出最近 10 个跑过 recall 的会话:各会话的运行次数、召回与采纳的文档数,以及会话从 `Related:` 行打开相关文件的运行次数。

### 开启 / 关闭 Recall

Expand Down
2 changes: 1 addition & 1 deletion skill-data/core/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ Generated: do not edit by hand. Regenerate with
- `teamai codebase` — Inspect and maintain the teamwiki that `teamai import` writes
- `--project <name>` (hidden) — Codebase slug under teamwiki/evidence/code/, required for --deep-enrich
- `--upgrade-wiki` (hidden) — Migrate docs/team-codebase/ to teamwiki/ graph format
- `--lint` — Run global consistency lint over the teamwiki knowledge graph
- `--lint` — Run global consistency lint over the teamwiki knowledge graph, and count the member's asset relations (assets, relations, without a relation, unresolved)
- `--reconcile` — Reconcile product and code knowledge in teamwiki
- `--deep-enrich` — Generate deep knowledge docs from extracted evidence
- `--fix` (hidden) — Deprecated: teamwiki lint has no autofix; runs lint in report-only mode
Expand Down
48 changes: 47 additions & 1 deletion src/__tests__/asset-relations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
import type { RelationSeverity, StoredRelation } from '../asset-relations.js';
import { indexInMemory } from '../utils/search-index.js';
import { localLinkTargets } from '../utils/markdown-links.js';
import { buildRelationContext, collectCodePages, formatResults, relationLines, computeDerivedRelations } from '../recall.js';
import { buildRelationContext, collectCodePages, formatResults, relationLines, computeDerivedRelations, relatedRefsOf } from '../recall.js';
import type { RelationContext } from '../recall.js';
import { matchGlob } from '../utils/glob.js';
import type { SearchIndex, SearchIndexEntry } from '../types.js';
Expand Down Expand Up @@ -533,6 +533,52 @@ describe('formatResults — Related: lines', () => {
});
});

describe('relatedRefsOf — the run line records what stdout shows (#1562)', () => {
it('returns one ref per printed Related: line, in print order, with direction', () => {
const docEntry = entry({
filename: 'overview.md', treePath: 'docs/overview.md', path: '/repo/docs/overview.md', type: 'docs',
});
const ruleHit = {
entry: entry({
filename: 'payments.md', treePath: 'rules/payments.md', path: '/repo/rules/payments.md',
related: [{ relation: 'SUPERSEDES', target: 'docs/overview.md', via: 'declared' }],
}),
score: 5.1, scope: 'user' as const,
};
const docHit = { entry: docEntry, score: 5.0, scope: 'user' as const };
const relations = new Map<string, RelationContext>([['user', ctx({
byTreePath: new Map([
['docs/overview.md', docEntry],
['rules/payments.md', ruleHit.entry],
]),
incoming: new Map([['docs/overview.md', [{ from: 'rules/payments.md', relation: 'SUPERSEDES', via: 'declared' }]]]),
})]]);

const output = formatResults([ruleHit, docHit], undefined, relations);
const refs = relatedRefsOf([ruleHit, docHit], relations);

const printed = output.split('\n').filter((l) => l.startsWith('Related:'));
expect(printed).toHaveLength(2);
expect(refs).toHaveLength(2);
expect(refs[0]).toEqual([{ relation: 'SUPERSEDES', direction: 'out', path: '/repo/docs/overview.md' }]);
expect(refs[1]).toEqual([{ relation: 'SUPERSEDES', direction: 'in', path: '/repo/rules/payments.md' }]);
// Each ref rebuilds exactly the line stdout showed.
const lineOf = (r: { relation: string; direction: string; path: string }): string =>
`Related: ${r.direction === 'out' ? '→' : '←'} ${r.relation} ${r.path}`;
expect(refs.flat().map(lineOf)).toEqual(printed);
});

it('returns no refs for a hit with no printed relations', () => {
const hit = {
entry: entry({ filename: 'overview.md', treePath: 'docs/overview.md', path: '/repo/docs/overview.md', type: 'docs' }),
score: 5.0, scope: 'user' as const,
};
const relations = new Map<string, RelationContext>([['user', ctx()]]);
expect(relatedRefsOf([hit], relations)).toEqual([[]]);
expect(formatResults([hit], undefined, relations)).not.toContain('Related:');
});
});

describe('ASSET_RELATIONS', () => {
it('is the four declarable relation names', () => {
expect(ASSET_RELATIONS).toEqual(['MAPS_TO', 'REFERENCES', 'SUPERSEDES', 'CONFLICTS_WITH']);
Expand Down
8 changes: 8 additions & 0 deletions src/__tests__/dashboard-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ describe('unified dashboard', () => {
totalEntries: 1, totalRecalls: 0, overallCoveragePct: 0, contributorCount: 1,
coverage: [{ type: 'docs', total: 1, covered: 0, coveragePct: 0 }], topRecalled: [],
silent: [{ docId: 'x', title: '<img src=x onerror=alert(1)>', author: '<script>bad</script>', type: 'docs', date: '', tags: [], recalledCount: 0, upvotedCount: 0, lastRecalledAt: null }],
relations: {
assetCount: 2, relationCount: 1, withoutRelation: ['docs/<script>lonely</script>'],
unresolved: ['rules/x.md REFERENCES docs/<script>missing</script>'], silentAndWithoutRelation: [],
},
trend: [], authors: [{ author: '<script>bad</script>', entries: 1, totalRecalled: 0 }],
maintenance: { promote: [], prune: [], stale: [] },
};
Expand Down Expand Up @@ -111,6 +115,10 @@ describe('unified dashboard', () => {
coverage: [{ type: 'docs', total: 3, covered: 2, coveragePct: 60 }],
topRecalled: [{ docId: 'a', title: 'A', author: 'x', type: 'docs', date: '', tags: [], recalledCount: 5, upvotedCount: 1, lastRecalledAt: null }],
silent: [{ docId: 'b', title: 'B', author: 'x', type: 'docs', date: '', tags: [], recalledCount: 0, upvotedCount: 0, lastRecalledAt: null }],
relations: {
assetCount: 3, relationCount: 2, withoutRelation: ['docs/b.md'],
unresolved: ['rules/a.md MAPS_TO teamwiki/evidence/code/x/m.md'], silentAndWithoutRelation: ['docs/b.md'],
},
trend: [{ period: '2026-08', count: 1 }, { period: '2026-09', count: 2 }],
authors: [{ author: 'x', entries: 3, totalRecalled: 5 }],
maintenance: {
Expand Down
Loading
Loading