Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist/knowledge-graph.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions dist/knowledge-graph.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/knowledge-graph.js.map

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions scripts/audit/baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -451,25 +451,25 @@
"reason": "readSettings throws on unparseable input; absent hooks truthfully means none wired",
"triaged": "2026-08-04; re-keyed 2026-08-13 (agentic-orchestration removal shifted lines up; same statement, classification unchanged); re-keyed 2026-08-13 (review-fix commit shifted lines; same statement, classification unchanged — verified); re-keyed 2026-08-16 (merge of memesh-setup onto main: +6 README Step 1.5 / +3 install-hooks export comment / +2 cli imports; byte-identical vs origin/main — NOTE C7 339 slid onto a colliding key, whole set re-verified); re-keyed 2026-08-16 (release-prep: settingsHaveMemeshHooks added above in install-hooks / wireUserHooks helper above in cli; byte-identical vs HEAD); re-keyed 2026-08-24 (uninstallHooks parses settings before deleting the rule file; same statement, classification unchanged); re-keyed 2026-08-30 454→451 after install-hooks relocation comments changed above; statement byte-identical; re-keyed 2026-08-30 451→455 after mixed-registry plugin detection added four lines above; statement byte-identical"
},
"C5 src/knowledge-graph.ts:754": {
"C5 src/knowledge-graph.ts:769": {
"class": "SAFE-ACCUMULATOR",
"reason": "batch-hydrate lookups; confidence bump fails closed",
"triaged": "2026-08-09 (re-keyed: review fixes — busy_timeout, vector-index guards and the .mcp.json entry check — shifted these lines; same statements, same classification); re-keyed 2026-08-10 (review pass 2: redactUserPaths moved to core/paths, namespace breadcrumb, dreamer retirement); re-keyed 2026-08-14 (UX-1 title column added lines above; same statement — verified against the line content); re-keyed 2026-08-15 631→649 (merge main shifted lines +18; same statement — verified); re-keyed 2026-08-24 (cleanup pass; statement re-located in file order, classification unchanged); re-keyed 2026-09-04 683→754 (rebase onto main after createEntity dedup-guard scope widened to any entity; same statement — verified against actual C5 regex hit)"
"triaged": "2026-08-09 (re-keyed: review fixes — busy_timeout, vector-index guards and the .mcp.json entry check — shifted these lines; same statements, same classification); re-keyed 2026-08-10 (review pass 2: redactUserPaths moved to core/paths, namespace breadcrumb, dreamer retirement); re-keyed 2026-08-14 (UX-1 title column added lines above; same statement — verified against the line content); re-keyed 2026-08-15 631→649 (merge main shifted lines +18; same statement — verified); re-keyed 2026-08-24 (cleanup pass; statement re-located in file order, classification unchanged); re-keyed 2026-09-04 683→754 (rebase onto main after createEntity dedup-guard scope widened to any entity; same statement — verified against actual C5 regex hit); re-keyed 2026-09-04 754→769 (fix/lesson-dedup-guard-trusts-stored-type added a comment block above explaining the security-review fix; same statement — verified against actual C5 regex hit)"
},
"C5 src/knowledge-graph.ts:755": {
"C5 src/knowledge-graph.ts:770": {
"class": "SAFE-ACCUMULATOR",
"reason": "batch-hydrate lookups; confidence bump fails closed",
"triaged": "2026-08-09 (re-keyed: review fixes — busy_timeout, vector-index guards and the .mcp.json entry check — shifted these lines; same statements, same classification); re-keyed 2026-08-10 (review pass 2: redactUserPaths moved to core/paths, namespace breadcrumb, dreamer retirement); re-keyed 2026-08-14 (UX-1 title column added lines above; same statement — verified against the line content); re-keyed 2026-08-15 629→647 (merge main shifted lines +18; same statement — verified); re-keyed 2026-08-24 (cleanup pass; statement re-located in file order, classification unchanged); re-keyed 2026-09-04 684→755 (rebase onto main after createEntity dedup-guard scope widened to any entity; same statement — verified against actual C5 regex hit)"
"triaged": "2026-08-09 (re-keyed: review fixes — busy_timeout, vector-index guards and the .mcp.json entry check — shifted these lines; same statements, same classification); re-keyed 2026-08-10 (review pass 2: redactUserPaths moved to core/paths, namespace breadcrumb, dreamer retirement); re-keyed 2026-08-14 (UX-1 title column added lines above; same statement — verified against the line content); re-keyed 2026-08-15 629→647 (merge main shifted lines +18; same statement — verified); re-keyed 2026-08-24 (cleanup pass; statement re-located in file order, classification unchanged); re-keyed 2026-09-04 684→755 (rebase onto main after createEntity dedup-guard scope widened to any entity; same statement — verified against actual C5 regex hit); re-keyed 2026-09-04 755→770 (fix/lesson-dedup-guard-trusts-stored-type added a comment block above explaining the security-review fix; same statement — verified against actual C5 regex hit)"
},
"C5 src/knowledge-graph.ts:756": {
"C5 src/knowledge-graph.ts:771": {
"class": "SAFE-ACCUMULATOR",
"reason": "batch-hydrate lookups; confidence bump fails closed",
"triaged": "2026-08-09 (re-keyed: review fixes — busy_timeout, vector-index guards and the .mcp.json entry check — shifted these lines; same statements, same classification); re-keyed 2026-08-10 (review pass 2: redactUserPaths moved to core/paths, namespace breadcrumb, dreamer retirement); re-keyed 2026-08-14 (UX-1 title column added lines above; same statement — verified against the line content); re-keyed 2026-08-15 630→648 (merge main shifted lines +18; same statement — verified); re-keyed 2026-08-24 (cleanup pass; statement re-located in file order, classification unchanged); re-keyed 2026-09-04 685→756 (rebase onto main after createEntity dedup-guard scope widened to any entity; same statement — verified against actual C5 regex hit)"
"triaged": "2026-08-09 (re-keyed: review fixes — busy_timeout, vector-index guards and the .mcp.json entry check — shifted these lines; same statements, same classification); re-keyed 2026-08-10 (review pass 2: redactUserPaths moved to core/paths, namespace breadcrumb, dreamer retirement); re-keyed 2026-08-14 (UX-1 title column added lines above; same statement — verified against the line content); re-keyed 2026-08-15 630→648 (merge main shifted lines +18; same statement — verified); re-keyed 2026-08-24 (cleanup pass; statement re-located in file order, classification unchanged); re-keyed 2026-09-04 685→756 (rebase onto main after createEntity dedup-guard scope widened to any entity; same statement — verified against actual C5 regex hit); re-keyed 2026-09-04 756→771 (fix/lesson-dedup-guard-trusts-stored-type added a comment block above explaining the security-review fix; same statement — verified against actual C5 regex hit)"
},
"C5 src/knowledge-graph.ts:803": {
"C5 src/knowledge-graph.ts:818": {
"class": "SAFE-BY-CONSTRUCTION",
"reason": "`opts?.countAsAccess ?? true` in search(): the default is the CONSERVATIVE direction, not the convenient one. Absent means 'this is an ordinary recall, count it' — the behaviour every caller had before the option existed. Only the one read that must NOT count (exportMemories: a backup is not a use) says so explicitly, and tests/core/reads-that-were-writes.test.ts pins both directions.",
"triaged": "2026-08-24 (full-codebase review, R9-a); re-keyed 2026-08-24 (cleanup pass; statement re-located in file order, classification unchanged); re-keyed 2026-09-04 732→803 (rebase onto main after createEntity dedup-guard scope widened to any entity; same statement — verified against actual C5 regex hit)"
"triaged": "2026-08-24 (full-codebase review, R9-a); re-keyed 2026-08-24 (cleanup pass; statement re-located in file order, classification unchanged); re-keyed 2026-09-04 732→803 (rebase onto main after createEntity dedup-guard scope widened to any entity; same statement — verified against actual C5 regex hit); re-keyed 2026-09-04 803→818 (fix/lesson-dedup-guard-trusts-stored-type added a comment block above explaining the security-review fix; same statement — verified against actual C5 regex hit)"
},
"C5 src/core/serializer.ts:44": {
"class": "SAFE-BY-CONSTRUCTION",
Expand Down
21 changes: 18 additions & 3 deletions src/knowledge-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,8 @@ export class KnowledgeGraph {
const isNewEntity = insertResult.changes > 0;

const row = this.db
.prepare('SELECT id, status, namespace, title FROM entities WHERE name = ?')
.get(name) as { id: number; status: string; namespace: string | null; title: string | null };
.prepare('SELECT id, status, namespace, title, type FROM entities WHERE name = ?')
.get(name) as { id: number; status: string; namespace: string | null; title: string | null; type: string };
const entityId = row.id;

// Title update on an EXISTING entity — the INSERT OR IGNORE above never
Expand Down Expand Up @@ -534,11 +534,26 @@ export class KnowledgeGraph {
// first, drop it, and fuse both blocks into one — silently discarding
// `Fix: A`. Every OTHER type's reader selects `content` alone with no
// ordering, so a repeat there is genuinely inert.
//
// The membership check reads the entity's STORED type (`row.type`) for
// an existing entity, never the incoming `type` argument: `INSERT OR
// IGNORE` above is a no-op on a name collision, so the row's real type
// never changes on re-remember (pinned by "should ... preserve original
// type on duplicate entity" above) — but `type` still holds whatever
// string this call passed. A caller is free to pass any 1-100 char
// string (`transports/schemas.ts`'s `type: z.string().min(1).max(100)`,
// no enum), so re-remembering an existing lesson under some OTHER type
// string would have made this check take the non-lesson branch and run
// content dedup against ordered lesson blocks anyway — silently dropping
// a repeated `Root cause:`/`Fix:`/`Prevention:` line the lesson family is
// exempted specifically to keep. `isNewEntity` still uses the incoming
// `type` because there is no stored type yet to read.
if (opts?.observations?.length) {
const insertObs = this.db.prepare(
'INSERT INTO observations (entity_id, content) VALUES (?, ?)'
);
const isLessonFamily = type === 'lesson_learned' || type === 'lesson' || type === 'mistake';
const effectiveType = isNewEntity ? type : row.type;
const isLessonFamily = effectiveType === 'lesson_learned' || effectiveType === 'lesson' || effectiveType === 'mistake';
if (isLessonFamily) {
for (const obs of opts.observations) {
insertObs.run(entityId, obs);
Expand Down
32 changes: 32 additions & 0 deletions tests/knowledge-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,38 @@ describe('Feature: Knowledge Graph', () => {
}
});

it('re-remembering a lesson entity under a different type string still skips content dedup', () => {
// The lesson-family exemption above must key on the entity's STORED
// type, not the type string this particular call happens to pass.
// `type` here is caller-controlled (schemas.ts: any 1-100 char string,
// no enum) and, per the test below, re-remembering an existing entity
// under a different type never changes its stored type — so a second
// call naming some other type must still be treated as the lesson it
// actually is, or the ordered-block guard above is bypassable.
kg.createEntity('lesson-mismatched-type-fixture', 'lesson_learned', {
observations: [
'Error: X',
'Root cause: Not specified',
'Fix: A',
'Prevention: Review similar code paths',
],
});
kg.createEntity('lesson-mismatched-type-fixture', 'note', {
observations: [
'Error: X',
'Root cause: Not specified',
'Fix: B',
'Prevention: Review similar code paths',
],
});

const entity = kg.getEntity('lesson-mismatched-type-fixture');
expect(entity!.type).toBe('lesson_learned');
expect(entity!.observations.filter((o) => o === 'Error: X')).toHaveLength(2);
expect(entity!.observations).toContain('Fix: A');
expect(entity!.observations).toContain('Fix: B');
});

it('should dedupe tags and preserve original type on duplicate entity', () => {
kg.createEntity('TypeScript', 'language', {
tags: ['frontend'],
Expand Down
Loading