docs(ops): add persona store audit and lot tracking
This commit is contained in:
@@ -11,3 +11,14 @@ Fichiers attendus:
|
||||
- `outputs/`
|
||||
|
||||
L'orchestrateur n'écrit pas dans les données métier du runtime; il tient seulement le suivi des lots V2.
|
||||
|
||||
Scripts ops utiles:
|
||||
- `node ops/v2/health-check.js`
|
||||
- `node ops/v2/persona-manager.js`
|
||||
- `node ops/v2/persona-store-audit.js`
|
||||
|
||||
Audit store personas:
|
||||
- `node ops/v2/persona-store-audit.js --json`
|
||||
- `node ops/v2/persona-store-audit.js --archive`
|
||||
- Log de travail: `ops/v2/logs/persona-store-audit.jsonl`
|
||||
- Trace durable a consigner dans `ops/v2/personas-runtime-20260325/outputs/`
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env node
|
||||
// Persona store audit TUI — inspects v2-local per-file store, legacy leftovers,
|
||||
// and can archive safe v2 aggregate files after migration.
|
||||
// Usage: node ops/v2/persona-store-audit.js [--json] [--archive] [--target-dir DIR] [--legacy-data-dir DIR]
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const NO_COLOR = !!process.env.NO_COLOR;
|
||||
const c = {
|
||||
green: (s) => NO_COLOR ? s : `\x1b[32m${s}\x1b[0m`,
|
||||
red: (s) => NO_COLOR ? s : `\x1b[31m${s}\x1b[0m`,
|
||||
yellow: (s) => NO_COLOR ? s : `\x1b[33m${s}\x1b[0m`,
|
||||
cyan: (s) => NO_COLOR ? s : `\x1b[36m${s}\x1b[0m`,
|
||||
bold: (s) => NO_COLOR ? s : `\x1b[1m${s}\x1b[0m`,
|
||||
dim: (s) => NO_COLOR ? s : `\x1b[2m${s}\x1b[0m`,
|
||||
};
|
||||
|
||||
function stripAnsi(s) {
|
||||
return String(s || "").replace(/\x1b\[[0-9;]*m/g, "");
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
json: false,
|
||||
archive: false,
|
||||
noLog: false,
|
||||
targetDir: null,
|
||||
legacyDataDir: null,
|
||||
};
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const token = argv[i];
|
||||
if (token === "--json") {
|
||||
args.json = true;
|
||||
continue;
|
||||
}
|
||||
if (token === "--archive") {
|
||||
args.archive = true;
|
||||
continue;
|
||||
}
|
||||
if (token === "--no-log") {
|
||||
args.noLog = true;
|
||||
continue;
|
||||
}
|
||||
if (token === "--help" || token === "-h") {
|
||||
args.help = true;
|
||||
continue;
|
||||
}
|
||||
if (token.startsWith("--target-dir=")) {
|
||||
args.targetDir = token.slice("--target-dir=".length);
|
||||
continue;
|
||||
}
|
||||
if (token === "--target-dir" && argv[i + 1]) {
|
||||
args.targetDir = argv[++i];
|
||||
continue;
|
||||
}
|
||||
if (token.startsWith("--legacy-data-dir=")) {
|
||||
args.legacyDataDir = token.slice("--legacy-data-dir=".length);
|
||||
continue;
|
||||
}
|
||||
if (token === "--legacy-data-dir" && argv[i + 1]) {
|
||||
args.legacyDataDir = argv[++i];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function safeReadJson(filePath, fallback) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function safeReadJsonl(filePath) {
|
||||
try {
|
||||
return fs.readFileSync(filePath, "utf8")
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function listFiles(directory, extension) {
|
||||
try {
|
||||
return fs.readdirSync(directory)
|
||||
.filter((entry) => entry.endsWith(extension))
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function countPerPersonaJson(directory, valueToRowCount) {
|
||||
const files = listFiles(directory, ".json");
|
||||
let rows = 0;
|
||||
|
||||
for (const entry of files) {
|
||||
const payload = safeReadJson(path.join(directory, entry), null);
|
||||
rows += valueToRowCount(payload);
|
||||
}
|
||||
|
||||
return {
|
||||
directory,
|
||||
files: files.length,
|
||||
rows,
|
||||
ids: files.map((entry) => entry.replace(/\.json$/, "")),
|
||||
};
|
||||
}
|
||||
|
||||
function countV1Jsonl(directory) {
|
||||
const files = listFiles(directory, ".jsonl");
|
||||
let rows = 0;
|
||||
|
||||
for (const entry of files) {
|
||||
rows += safeReadJsonl(path.join(directory, entry)).length;
|
||||
}
|
||||
|
||||
return {
|
||||
directory,
|
||||
files: files.length,
|
||||
rows,
|
||||
};
|
||||
}
|
||||
|
||||
function uniqueSorted(values) {
|
||||
return [...new Set(values.filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function analyzeV2GlobalFile(filePath, kind, perFileIds) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return {
|
||||
path: filePath,
|
||||
exists: false,
|
||||
kind,
|
||||
invalid: false,
|
||||
ids: [],
|
||||
count: 0,
|
||||
missingIds: [],
|
||||
safeToArchive: false,
|
||||
archivedTo: null,
|
||||
};
|
||||
}
|
||||
|
||||
const payload = safeReadJson(filePath, null);
|
||||
const invalid = payload === null;
|
||||
let ids = [];
|
||||
|
||||
if (kind === "personas") {
|
||||
ids = Array.isArray(payload) ? payload.map((row) => String(row?.id || "").trim()) : [];
|
||||
} else if (kind === "sources") {
|
||||
const rows = Array.isArray(payload) ? payload : Object.values(payload || {});
|
||||
ids = rows.map((row) => String(row?.personaId || row?.id || "").trim());
|
||||
} else {
|
||||
ids = Array.isArray(payload) ? payload.map((row) => String(row?.personaId || "").trim()) : [];
|
||||
}
|
||||
|
||||
ids = uniqueSorted(ids);
|
||||
const perFileIdSet = new Set(perFileIds);
|
||||
const missingIds = ids.filter((id) => !perFileIdSet.has(id));
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
exists: true,
|
||||
kind,
|
||||
invalid,
|
||||
ids,
|
||||
count: ids.length,
|
||||
missingIds,
|
||||
safeToArchive: !invalid && missingIds.length === 0,
|
||||
archivedTo: null,
|
||||
};
|
||||
}
|
||||
|
||||
function formatTable(headers, rows) {
|
||||
const widths = headers.map((header, index) => {
|
||||
return Math.max(header.length, ...rows.map((row) => stripAnsi(row[index] || "").length));
|
||||
});
|
||||
|
||||
function pad(text, width) {
|
||||
const visible = stripAnsi(text).length;
|
||||
return text + " ".repeat(Math.max(0, width - visible));
|
||||
}
|
||||
|
||||
const head = headers.map((header, index) => pad(header, widths[index])).join(" ");
|
||||
const sep = widths.map((width) => "─".repeat(width)).join("──");
|
||||
const body = rows.map((row) => row.map((cell, index) => pad(cell || "", widths[index])).join(" ")).join("\n");
|
||||
return [c.bold(head), sep, body].join("\n");
|
||||
}
|
||||
|
||||
function drawBox(title, lines, width) {
|
||||
const innerWidth = width - 2;
|
||||
|
||||
function padLine(line) {
|
||||
const visible = stripAnsi(line).length;
|
||||
return `║ ${line}${" ".repeat(Math.max(0, innerWidth - visible - 1))}║`;
|
||||
}
|
||||
|
||||
const titleVisible = stripAnsi(title).length;
|
||||
const left = Math.floor((innerWidth - titleVisible) / 2);
|
||||
const right = innerWidth - titleVisible - left;
|
||||
|
||||
return [
|
||||
`╔${"═".repeat(innerWidth)}╗`,
|
||||
`║${" ".repeat(left)}${title}${" ".repeat(right)}║`,
|
||||
`╠${"═".repeat(innerWidth)}╣`,
|
||||
...lines.map(padLine),
|
||||
`╚${"═".repeat(innerWidth)}╝`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function timestampForFile() {
|
||||
return new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
||||
}
|
||||
|
||||
function archiveSafeV2GlobalFiles(items) {
|
||||
const archived = [];
|
||||
const skipped = [];
|
||||
const stamp = timestampForFile();
|
||||
|
||||
for (const item of items) {
|
||||
if (!item.exists) continue;
|
||||
if (!item.safeToArchive) {
|
||||
skipped.push({ path: item.path, reason: "missing-per-file-ids", missingIds: item.missingIds });
|
||||
continue;
|
||||
}
|
||||
const archivedTo = `${item.path}.migrated-${stamp}.bak`;
|
||||
fs.renameSync(item.path, archivedTo);
|
||||
item.archivedTo = archivedTo;
|
||||
archived.push({ from: item.path, to: archivedTo });
|
||||
}
|
||||
|
||||
return { archived, skipped };
|
||||
}
|
||||
|
||||
function writeMachineLog(logPath, payload) {
|
||||
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
||||
fs.appendFileSync(logPath, `${JSON.stringify(payload)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function renderTui(result) {
|
||||
const perFileRows = [
|
||||
["personas", String(result.perFile.personas.files), String(result.perFile.personas.rows)],
|
||||
["sources", String(result.perFile.sources.files), String(result.perFile.sources.rows)],
|
||||
["feedback", String(result.perFile.feedback.files), String(result.perFile.feedback.rows)],
|
||||
["proposals", String(result.perFile.proposals.files), String(result.perFile.proposals.rows)],
|
||||
];
|
||||
|
||||
const legacyRows = Object.entries(result.v2GlobalLegacy).map(([name, item]) => {
|
||||
let status = c.dim("absent");
|
||||
if (item.exists && item.safeToArchive) status = c.green("safe");
|
||||
else if (item.exists) status = c.yellow(`missing:${item.missingIds.length}`);
|
||||
|
||||
let archive = c.dim("--");
|
||||
if (item.archivedTo) archive = c.green("archived");
|
||||
else if (item.exists && item.safeToArchive) archive = c.cyan("ready");
|
||||
|
||||
return [name, item.exists ? String(item.count) : "0", status, archive];
|
||||
});
|
||||
|
||||
const v1Rows = [
|
||||
["personas.overrides.json", result.v1Legacy.personasOverrides.exists ? "present" : "absent", c.dim("--")],
|
||||
["persona-sources/*.json", String(result.v1Legacy.sources.files), String(result.v1Legacy.sources.rows)],
|
||||
["persona-feedback/*.jsonl", String(result.v1Legacy.feedback.files), String(result.v1Legacy.feedback.rows)],
|
||||
["persona-proposals/*.jsonl", String(result.v1Legacy.proposals.files), String(result.v1Legacy.proposals.rows)],
|
||||
];
|
||||
|
||||
const blocks = [
|
||||
`Target: ${c.cyan(result.targetDir)}`,
|
||||
`Legacy: ${c.cyan(result.legacyDataDir)}`,
|
||||
"",
|
||||
formatTable(["Per-file", "Files", "Rows"], perFileRows),
|
||||
"",
|
||||
formatTable(["V2 Global", "IDs", "Status", "Archive"], legacyRows),
|
||||
"",
|
||||
formatTable(["V1 Legacy", "Files", "Rows"], v1Rows),
|
||||
"",
|
||||
`Summary: ${result.summary.globalFilesPresent} global files present, ${result.summary.globalFilesSafeToArchive} safe to archive, ${result.summary.globalFilesNeedMerge} still need merge`,
|
||||
];
|
||||
|
||||
const lines = [];
|
||||
for (const block of blocks) {
|
||||
for (const line of String(block).split("\n")) {
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return drawBox(c.bold("Persona Store Audit"), lines, 88);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (args.help) {
|
||||
console.log(`
|
||||
${c.bold("KXKM Persona Store Audit")}
|
||||
|
||||
Usage: node ops/v2/persona-store-audit.js [options]
|
||||
|
||||
Options:
|
||||
--json Output raw JSON
|
||||
--archive Archive v2 legacy aggregate files only when safe
|
||||
--target-dir DIR Target per-file store (default: env KXKM_LOCAL_DATA_DIR or data/v2-local)
|
||||
--legacy-data-dir Legacy V1 data dir (default: data)
|
||||
--no-log Do not append a JSONL audit log
|
||||
--help Show this help
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const rootDir = path.resolve(__dirname, "../..");
|
||||
const targetDir = path.resolve(rootDir, args.targetDir || process.env.KXKM_LOCAL_DATA_DIR || "data/v2-local");
|
||||
const legacyDataDir = path.resolve(rootDir, args.legacyDataDir || "data");
|
||||
const logPath = path.join(rootDir, "ops/v2/logs/persona-store-audit.jsonl");
|
||||
|
||||
const perFile = {
|
||||
personas: countPerPersonaJson(path.join(targetDir, "personas"), (value) => (value && !Array.isArray(value) ? 1 : 0)),
|
||||
sources: countPerPersonaJson(path.join(targetDir, "persona-sources"), (value) => (value && !Array.isArray(value) ? 1 : 0)),
|
||||
feedback: countPerPersonaJson(path.join(targetDir, "persona-feedback"), (value) => Array.isArray(value) ? value.length : (value ? 1 : 0)),
|
||||
proposals: countPerPersonaJson(path.join(targetDir, "persona-proposals"), (value) => Array.isArray(value) ? value.length : (value ? 1 : 0)),
|
||||
};
|
||||
|
||||
const v2GlobalLegacy = {
|
||||
personas: analyzeV2GlobalFile(path.join(targetDir, "personas.json"), "personas", perFile.personas.ids),
|
||||
sources: analyzeV2GlobalFile(path.join(targetDir, "persona-sources.json"), "sources", perFile.sources.ids),
|
||||
feedback: analyzeV2GlobalFile(path.join(targetDir, "persona-feedback.json"), "feedback", perFile.feedback.ids),
|
||||
proposals: analyzeV2GlobalFile(path.join(targetDir, "persona-proposals.json"), "proposals", perFile.proposals.ids),
|
||||
};
|
||||
|
||||
let archiveResult = { archived: [], skipped: [] };
|
||||
if (args.archive) {
|
||||
archiveResult = archiveSafeV2GlobalFiles(Object.values(v2GlobalLegacy));
|
||||
}
|
||||
|
||||
const v1Legacy = {
|
||||
personasOverrides: {
|
||||
path: path.join(legacyDataDir, "personas.overrides.json"),
|
||||
exists: fs.existsSync(path.join(legacyDataDir, "personas.overrides.json")),
|
||||
},
|
||||
sources: countPerPersonaJson(path.join(legacyDataDir, "persona-sources"), (value) => (value && !Array.isArray(value) ? 1 : 0)),
|
||||
feedback: countV1Jsonl(path.join(legacyDataDir, "persona-feedback")),
|
||||
proposals: countV1Jsonl(path.join(legacyDataDir, "persona-proposals")),
|
||||
};
|
||||
|
||||
const globalItems = Object.values(v2GlobalLegacy);
|
||||
const summary = {
|
||||
globalFilesPresent: globalItems.filter((item) => item.exists).length,
|
||||
globalFilesSafeToArchive: globalItems.filter((item) => item.safeToArchive).length,
|
||||
globalFilesNeedMerge: globalItems.filter((item) => item.exists && item.missingIds.length > 0).length,
|
||||
v1LegacyFilesPresent:
|
||||
(v1Legacy.personasOverrides.exists ? 1 : 0) +
|
||||
v1Legacy.sources.files +
|
||||
v1Legacy.feedback.files +
|
||||
v1Legacy.proposals.files,
|
||||
archivedFiles: archiveResult.archived.length,
|
||||
};
|
||||
|
||||
const result = {
|
||||
ok: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
targetDir,
|
||||
legacyDataDir,
|
||||
perFile,
|
||||
v2GlobalLegacy,
|
||||
v1Legacy,
|
||||
archive: archiveResult,
|
||||
summary,
|
||||
};
|
||||
|
||||
if (!args.noLog) {
|
||||
writeMachineLog(logPath, {
|
||||
ts: result.timestamp,
|
||||
targetDir: result.targetDir,
|
||||
legacyDataDir: result.legacyDataDir,
|
||||
archiveRequested: args.archive,
|
||||
archivedFiles: result.archive.archived.length,
|
||||
globalFilesPresent: result.summary.globalFilesPresent,
|
||||
globalFilesNeedMerge: result.summary.globalFilesNeedMerge,
|
||||
v1LegacyFilesPresent: result.summary.v1LegacyFilesPresent,
|
||||
});
|
||||
}
|
||||
|
||||
if (args.json) {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(renderTui(result));
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(`[persona-store-audit] ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# PLAN (kxkm-personas-runtime-20260325)
|
||||
|
||||
Updated: 2026-03-25T06:02:30Z
|
||||
Updated: 2026-03-25T19:34:59Z
|
||||
|
||||
## lot-201-runtime-hardening [done]
|
||||
- Description: Durcir le runtime personas local et basculer le store actif en per-file v2-local
|
||||
@@ -33,3 +33,19 @@ Updated: 2026-03-25T06:02:30Z
|
||||
- Execution: manual
|
||||
- Checks: docs-reviewed
|
||||
- Summary: Veille documentee et plan benchmark poses; spike OpenCharacter/PCL encore pending.
|
||||
|
||||
## lot-205-store-audit [done]
|
||||
- Description: Ajouter un audit TUI du store personas per-file, verifier les reliquats legacy et encadrer l archivage des fichiers globaux
|
||||
- Depends on: lot-201-runtime-hardening
|
||||
- Owner: Ops/TUI
|
||||
- Execution: manual
|
||||
- Checks: node --check ops/v2/persona-store-audit.js, node ops/v2/persona-store-audit.js --json
|
||||
- Summary: Done: script TUI/JSON ajoute, fixture d archivage validee, audit du store reel documente, logs de travail lus puis purges.
|
||||
|
||||
## lot-206-feedback-convergence [done]
|
||||
- Description: Converger le runtime feedback personas vers le repo per-file et supprimer le doublon DPO legacy
|
||||
- Depends on: lot-201-runtime-hardening
|
||||
- Owner: Backend API
|
||||
- Execution: manual
|
||||
- Checks: npm run check, npm run test:v2
|
||||
- Summary: Done: votes structures, route feedback repo-backed, export DPO unique, compat query alias, frontend vote/signal normalise, tests de non-regression.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# EXECUTION STATUS (kxkm-personas-runtime-20260325)
|
||||
|
||||
Updated: 2026-03-25T06:02:30Z
|
||||
Updated: 2026-03-25T19:34:59Z
|
||||
|
||||
## lot-201-runtime-hardening
|
||||
- Status: done
|
||||
@@ -38,3 +38,17 @@ Updated: 2026-03-25T06:02:30Z
|
||||
- letta-langgraph-patterns [done] (P2, Veille OSS)
|
||||
- mem0-benchmark-plan [done] (P2, Veille OSS)
|
||||
- opencharacter-pcl-spike [pending] (P3, Training)
|
||||
|
||||
## lot-205-store-audit
|
||||
- Status: done
|
||||
- Owner: Ops/TUI
|
||||
- Execution: manual
|
||||
- Checks: node --check ops/v2/persona-store-audit.js, node ops/v2/persona-store-audit.js --json
|
||||
- Open tasks: none
|
||||
|
||||
## lot-206-feedback-convergence
|
||||
- Status: done
|
||||
- Owner: Backend API
|
||||
- Execution: manual
|
||||
- Checks: npm run check, npm run test:v2
|
||||
- Open tasks: none
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# TODO (kxkm-personas-runtime-20260325)
|
||||
|
||||
Updated: 2026-03-25T06:02:30Z
|
||||
Updated: 2026-03-25T19:34:59Z
|
||||
|
||||
## lot-201-runtime-hardening
|
||||
- [x] per-file-store (done) | owner: Personas | severity: P1
|
||||
@@ -25,3 +25,16 @@ Updated: 2026-03-25T06:02:30Z
|
||||
- [x] letta-langgraph-patterns (done) | owner: Veille OSS | severity: P2
|
||||
- [x] mem0-benchmark-plan (done) | owner: Veille OSS | severity: P2
|
||||
- [ ] opencharacter-pcl-spike (pending) | owner: Training | severity: P3
|
||||
|
||||
## lot-205-store-audit
|
||||
- [x] audit-script (done) | owner: Ops/TUI | severity: P1
|
||||
- [x] real-store-audit (done) | owner: Ops/TUI | severity: P1
|
||||
- [x] fixture-archive-check (done) | owner: Ops/TUI | severity: P2
|
||||
- [x] log-purge (done) | owner: Ops/TUI | severity: P2
|
||||
|
||||
## lot-206-feedback-convergence
|
||||
- [x] structured-vote-payload (done) | owner: Personas | severity: P1
|
||||
- [x] repo-feedback-route (done) | owner: Backend API | severity: P1
|
||||
- [x] drop-legacy-dpo-duplication (done) | owner: Backend API | severity: P1
|
||||
- [x] frontend-signal-normalization (done) | owner: Frontend | severity: P1
|
||||
- [x] regression-tests (done) | owner: Backend API | severity: P1
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# lot-205-store-audit
|
||||
|
||||
- Date: 2026-03-25T19:34:59Z
|
||||
- Owner: Ops/TUI
|
||||
- Status: done
|
||||
|
||||
## Commandes
|
||||
|
||||
```bash
|
||||
node ops/v2/persona-store-audit.js --json
|
||||
node ops/v2/persona-store-audit.js --json --archive --target-dir <fixture>/data/v2-local --legacy-data-dir <fixture>/data
|
||||
```
|
||||
|
||||
## Constat
|
||||
|
||||
- Store actif detecte sous `data/v2-local` en per-file:
|
||||
- `personas`: 17 fichiers / 17 enregistrements
|
||||
- `persona-sources`: 1 fichier / 1 enregistrement
|
||||
- `persona-feedback`: 16 fichiers / 45 enregistrements
|
||||
- `persona-proposals`: 1 fichier / 2 enregistrements
|
||||
- Aucun fichier global V2 restant dans `data/v2-local`:
|
||||
- `personas.json`
|
||||
- `persona-sources.json`
|
||||
- `persona-feedback.json`
|
||||
- `persona-proposals.json`
|
||||
- Restes legacy V1 encore presents sous `data/`:
|
||||
- `personas.overrides.json`
|
||||
- `persona-sources/`
|
||||
- `persona-feedback/`
|
||||
- `persona-proposals/`
|
||||
|
||||
## Validation
|
||||
|
||||
- `node --check ops/v2/persona-store-audit.js` OK
|
||||
- `node ops/v2/persona-store-audit.js --help` OK
|
||||
- Execution reelle `--json` OK
|
||||
- Fixture `--archive` OK, 2 fichiers globaux archives en `.migrated-<timestamp>.bak`
|
||||
|
||||
## Decision
|
||||
|
||||
- Le runtime V2 est bien per-file.
|
||||
- Les reliquats V1 restent en mode compat/migration uniquement.
|
||||
- Les logs de travail du lot sont purges apres synthese documentaire.
|
||||
@@ -0,0 +1,40 @@
|
||||
# lot-206-feedback-convergence
|
||||
|
||||
- Date: 2026-03-25T19:34:59Z
|
||||
- Owner: Backend API
|
||||
- Status: done
|
||||
|
||||
## Objectif
|
||||
|
||||
Converger les feedbacks personas vers une seule source de verite repo-backed et supprimer le doublon JSONL legacy qui polluait l export DPO.
|
||||
|
||||
## Correctifs
|
||||
|
||||
- `packages/persona-domain/src/editorial.ts`
|
||||
- votes structures `JSON`
|
||||
- parser compatible legacy
|
||||
- `extractDPOPairs()` base sur `prompt`, `chosen`, `rejected`
|
||||
- `apps/api/src/routes/personas.ts`
|
||||
- nouveau `POST /api/v2/feedback` repo-backed avec validation Zod
|
||||
- `apps/api/src/routes/session.ts`
|
||||
- suppression de l ancien writer `data/feedback/*.jsonl`
|
||||
- suppression du vieux `GET /api/v2/export/dpo`
|
||||
- `apps/api/src/routes/chat-history.ts`
|
||||
- export DPO unique, compat `persona_id` et alias `persona`
|
||||
- `apps/web/src/components/Chat.tsx`
|
||||
- recuperation du prompt utilisateur precedent au vote
|
||||
- `apps/web/src/components/ChatMessage.tsx`
|
||||
- `signal: "react" | "pin"` au lieu de surcharger `vote`
|
||||
|
||||
## Validation
|
||||
|
||||
- test domaine `packages/persona-domain/src/index.test.ts` OK
|
||||
- test API cible `apps/api/src/app.test.ts` OK
|
||||
- `npm run check` OK
|
||||
- `npm run test:v2` OK
|
||||
|
||||
## Impact
|
||||
|
||||
- plus de split-brain entre feedback repo et JSONL legacy
|
||||
- export DPO alimente par les vraies paires `prompt/chosen/rejected`
|
||||
- les reactions et pins n injectent plus de faux votes dans le corpus DPO
|
||||
@@ -66,6 +66,39 @@
|
||||
{ "id": "mem0-benchmark-plan", "query": "Preparer un benchmark Mem0 contre le runtime local sur latence et recall", "status": "done", "owner": "Veille OSS", "severity": "P2" },
|
||||
{ "id": "opencharacter-pcl-spike", "query": "Definir un spike training offline sur 5 personas avec OpenCharacter et PCL", "status": "pending", "owner": "Training", "severity": "P3" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lot-205-store-audit",
|
||||
"description": "Ajouter un audit TUI du store personas per-file, verifier les reliquats legacy et encadrer l archivage des fichiers globaux",
|
||||
"summary": "Done: audit TUI/JSON ajoute, verification du store reel, fixture d archivage validee, logs de travail purges apres synthese.",
|
||||
"status": "done",
|
||||
"owner": "Ops/TUI",
|
||||
"execution": "manual",
|
||||
"checks": ["node --check ops/v2/persona-store-audit.js", "node ops/v2/persona-store-audit.js --json"],
|
||||
"depends_on": ["lot-201-runtime-hardening"],
|
||||
"tasks": [
|
||||
{ "id": "audit-script", "query": "Ajouter un script TUI pour auditer data/v2-local et les reliquats legacy personas", "status": "done", "owner": "Ops/TUI", "severity": "P1" },
|
||||
{ "id": "real-store-audit", "query": "Executer l audit sur le store reel et produire une synthese exploitable", "status": "done", "owner": "Ops/TUI", "severity": "P1" },
|
||||
{ "id": "fixture-archive-check", "query": "Valider l archivage des anciens fichiers globaux sur fixture isolee", "status": "done", "owner": "Ops/TUI", "severity": "P2" },
|
||||
{ "id": "log-purge", "query": "Lire puis purger les logs de travail du lot pour ne garder que la trace documentaire", "status": "done", "owner": "Ops/TUI", "severity": "P2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lot-206-feedback-convergence",
|
||||
"description": "Converger le runtime feedback personas vers le repo per-file et supprimer le doublon DPO legacy",
|
||||
"summary": "Done: votes structures, route feedback repo-backed, export DPO unique, compat query alias, frontend normalise et tests passes.",
|
||||
"status": "done",
|
||||
"owner": "Backend API",
|
||||
"execution": "manual",
|
||||
"checks": ["npm run check", "npm run test:v2"],
|
||||
"depends_on": ["lot-201-runtime-hardening"],
|
||||
"tasks": [
|
||||
{ "id": "structured-vote-payload", "query": "Normaliser les votes personas en payload JSON parseable et compatible legacy", "status": "done", "owner": "Personas", "severity": "P1" },
|
||||
{ "id": "repo-feedback-route", "query": "Basculer POST /api/v2/feedback vers le repo personas avec validation Zod", "status": "done", "owner": "Backend API", "severity": "P1" },
|
||||
{ "id": "drop-legacy-dpo-duplication", "query": "Supprimer le vieux writer JSONL et le doublon GET /api/v2/export/dpo", "status": "done", "owner": "Backend API", "severity": "P1" },
|
||||
{ "id": "frontend-signal-normalization", "query": "Retrouver le prompt cote frontend et envoyer signal react pin hors votes DPO", "status": "done", "owner": "Frontend", "severity": "P1" },
|
||||
{ "id": "regression-tests", "query": "Verifier par tests domaine et API que l export DPO est alimente par le repo", "status": "done", "owner": "Backend API", "severity": "P1" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"project": "kxkm-personas-runtime-20260325",
|
||||
"updated_at": "2026-03-25T06:02:30Z",
|
||||
"updated_at": "2026-03-25T19:34:59Z",
|
||||
"batches": {
|
||||
"lot-201-runtime-hardening": {
|
||||
"status": "done",
|
||||
@@ -131,6 +131,78 @@
|
||||
"last_error": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"lot-205-store-audit": {
|
||||
"status": "done",
|
||||
"depends_on": [
|
||||
"lot-201-runtime-hardening"
|
||||
],
|
||||
"owner": "Ops/TUI",
|
||||
"tasks": {
|
||||
"audit-script": {
|
||||
"status": "done",
|
||||
"attempts": 1,
|
||||
"output": "ops/v2/persona-store-audit.js",
|
||||
"last_error": ""
|
||||
},
|
||||
"real-store-audit": {
|
||||
"status": "done",
|
||||
"attempts": 1,
|
||||
"output": "ops/v2/personas-runtime-20260325/outputs/lot-205-store-audit/summary.md",
|
||||
"last_error": ""
|
||||
},
|
||||
"fixture-archive-check": {
|
||||
"status": "done",
|
||||
"attempts": 1,
|
||||
"output": "ops/v2/personas-runtime-20260325/outputs/lot-205-store-audit/summary.md",
|
||||
"last_error": ""
|
||||
},
|
||||
"log-purge": {
|
||||
"status": "done",
|
||||
"attempts": 1,
|
||||
"output": "ops/v2/personas-runtime-20260325/outputs/lot-205-store-audit/summary.md",
|
||||
"last_error": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"lot-206-feedback-convergence": {
|
||||
"status": "done",
|
||||
"depends_on": [
|
||||
"lot-201-runtime-hardening"
|
||||
],
|
||||
"owner": "Backend API",
|
||||
"tasks": {
|
||||
"structured-vote-payload": {
|
||||
"status": "done",
|
||||
"attempts": 1,
|
||||
"output": "packages/persona-domain/src/editorial.ts",
|
||||
"last_error": ""
|
||||
},
|
||||
"repo-feedback-route": {
|
||||
"status": "done",
|
||||
"attempts": 1,
|
||||
"output": "apps/api/src/routes/personas.ts",
|
||||
"last_error": ""
|
||||
},
|
||||
"drop-legacy-dpo-duplication": {
|
||||
"status": "done",
|
||||
"attempts": 1,
|
||||
"output": "apps/api/src/routes/session.ts",
|
||||
"last_error": ""
|
||||
},
|
||||
"frontend-signal-normalization": {
|
||||
"status": "done",
|
||||
"attempts": 1,
|
||||
"output": "apps/web/src/components/Chat.tsx",
|
||||
"last_error": ""
|
||||
},
|
||||
"regression-tests": {
|
||||
"status": "done",
|
||||
"attempts": 1,
|
||||
"output": "apps/api/src/app.test.ts",
|
||||
"last_error": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user