fix(mnemopi): dropped object-shaped fact coercion

- Unwrapped extractor fact items from known text fields instead of coercing arbitrary objects with String().

- Dropped unrecognized object entries so derived fact indexes cannot persist literal [object Object] rows.

- Added a parseFacts regression covering mixed strings, wrapped objects, and junk timeline objects.

Fixes #4649
This commit is contained in:
roboomp
2026-07-06 00:56:44 +00:00
parent 79a397e97a
commit 4086aab83e
3 changed files with 35 additions and 2 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed extractor JSON parsing so object-shaped fact, instruction, preference, and timeline items are unwrapped from known text fields or dropped instead of persisting literal `[object Object]` rows. ([#4649](https://github.com/can1357/oh-my-pi/issues/4649))
## [16.3.7] - 2026-07-05
### Added
+15 -2
View File
@@ -85,6 +85,7 @@ function stripFence(raw: string): string {
const FLAT_FACT_LIMIT = 5;
const STRUCTURED_CATEGORY_LIMIT = 5;
const STRING_CATEGORY_KEYS = ["facts", "instructions", "preferences", "timelines"] as const;
const FACT_TEXT_FIELD_KEYS = ["fact", "text", "content", "value", "statement"] as const;
/** Parsed knowledge-graph edge emitted by the extractor LLM. */
export interface ExtractedKgTriple {
@@ -118,8 +119,20 @@ function normalizeFactArray(items: unknown): string[] {
}
const out: string[] = [];
for (const item of items) {
if (item !== null && item !== undefined && String(item).trim() !== "") {
const normalized = normalizeFact(String(item));
let text: string | null = null;
if (typeof item === "string") {
text = item.trim();
} else if (isRecord(item)) {
for (const key of FACT_TEXT_FIELD_KEYS) {
const candidate = item[key];
if (typeof candidate === "string" && candidate.trim() !== "") {
text = candidate.trim();
break;
}
}
}
if (text !== null && text !== "") {
const normalized = normalizeFact(text);
if (normalized !== "") {
out.push(normalized);
if (out.length >= STRUCTURED_CATEGORY_LIMIT) break;
+16
View File
@@ -52,6 +52,22 @@ describe("structured extraction", () => {
expect(parseFacts("NO_FACTS")).toEqual([]);
});
it("drops unrecognized object facts instead of stringifying them", () => {
const modelJson = JSON.stringify({
facts: [{ fact: "The user prefers tabs over spaces" }, { nested: {} }, "The user likes concise replies."],
instructions: [{ text: "Always include verification details" }],
preferences: [{ value: "Prefers dark mode" }],
timelines: [{ subject: "release", predicate: "on", object: "2026-08-01" }],
});
expect(parseFacts(modelJson)).toEqual([
"The user prefers tabs over spaces",
"The user likes concise replies",
"Always include verification details",
"Prefers dark mode",
]);
});
it("treats a valid empty structured extraction as no facts", () => {
expect(parseFacts('{"facts": [], "instructions": [], "preferences": [], "timelines": [], "kg": []}')).toEqual([]);
expect(