docs(ops): add persona store audit and lot tracking

This commit is contained in:
Codex Local
2026-03-25 20:38:57 +01:00
parent d4188583ab
commit 9ac151a59d
9 changed files with 656 additions and 4 deletions
+11
View File
@@ -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/`
+410
View File
@@ -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);
}
+17 -1
View File
@@ -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.
+15 -1
View File
@@ -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
+14 -1
View File
@@ -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" }
]
}
]
}
+73 -1
View File
@@ -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": ""
}
}
}
}
}