mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(triage): add incremental resyncIdeaFile (append-only)
This commit is contained in:
84
scripts/features/lib/resync.mjs
Normal file
84
scripts/features/lib/resync.mjs
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Incremental re-sync of idea files: append new comments only,
|
||||
* update frontmatter snapshot.
|
||||
*/
|
||||
import { parseFrontmatter, serializeFrontmatter, stripFrontmatter } from "./frontmatter.mjs";
|
||||
import { isBot } from "./classify.mjs";
|
||||
|
||||
function countThumbs(reactionGroups) {
|
||||
if (!Array.isArray(reactionGroups)) return 0;
|
||||
const g = reactionGroups.find((rg) => rg.content === "THUMBS_UP");
|
||||
return g?.users?.totalCount ?? 0;
|
||||
}
|
||||
|
||||
function uniqueCommenters(comments, authorLogin) {
|
||||
const seen = new Set();
|
||||
for (const c of comments ?? []) {
|
||||
const u = c.author;
|
||||
if (!u || !u.login) continue;
|
||||
if (u.login === authorLogin) continue;
|
||||
if (isBot(u)) continue;
|
||||
seen.add(u.login);
|
||||
}
|
||||
return seen.size;
|
||||
}
|
||||
|
||||
function renderComments(comments) {
|
||||
return comments
|
||||
.map(
|
||||
(c) =>
|
||||
`- **@${c.author?.login ?? "unknown"}** (${c.createdAt}):\n > ${(c.body ?? "").split("\n").join("\n > ")}`
|
||||
)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function appendComments(body, newComments) {
|
||||
const heading = "## 💬 Community Discussion";
|
||||
let idx = body.indexOf(heading);
|
||||
if (idx < 0) idx = body.indexOf("## Community Discussion");
|
||||
if (idx < 0) {
|
||||
return body + `\n\n${heading}\n\n` + renderComments(newComments);
|
||||
}
|
||||
const after = body.slice(idx);
|
||||
const nextHeaderMatch = after.slice(heading.length).match(/^##\s+/m);
|
||||
const insertAt = nextHeaderMatch ? idx + heading.length + nextHeaderMatch.index : body.length;
|
||||
const insertBlock = "\n\n" + renderComments(newComments) + "\n";
|
||||
return body.slice(0, insertAt) + insertBlock + body.slice(insertAt);
|
||||
}
|
||||
|
||||
export function resyncIdeaFile(text, issue, now = new Date(), opts = {}) {
|
||||
const meta = parseFrontmatter(text);
|
||||
if (!meta) return { changed: false, text };
|
||||
|
||||
const lastSyncedId = Number(meta.last_synced_comment_id ?? 0);
|
||||
const allComments = issue.comments ?? [];
|
||||
const newComments = allComments.filter((c) => Number(c.databaseId) > lastSyncedId);
|
||||
|
||||
if (newComments.length === 0) {
|
||||
return { changed: false, text };
|
||||
}
|
||||
|
||||
const body = stripFrontmatter(text);
|
||||
const newBody = appendComments(body, newComments);
|
||||
|
||||
const newMeta = {
|
||||
...meta,
|
||||
last_synced_at: now.toISOString(),
|
||||
last_synced_comment_id: Math.max(...newComments.map((c) => Number(c.databaseId))),
|
||||
snapshot: {
|
||||
...(meta.snapshot ?? {}),
|
||||
thumbs: countThumbs(issue.reactionGroups),
|
||||
commenters: uniqueCommenters(allComments, issue.author?.login),
|
||||
labels: (issue.labels ?? []).map((l) => l.name).filter(Boolean),
|
||||
state: String(issue.state ?? "open").toLowerCase(),
|
||||
},
|
||||
};
|
||||
|
||||
const out = serializeFrontmatter(newMeta, newBody);
|
||||
|
||||
const needsReclassification = !!(
|
||||
opts.inNeedDetails && newComments.some((c) => c.author?.login === issue.author?.login)
|
||||
);
|
||||
|
||||
return { changed: true, text: out, needsReclassification };
|
||||
}
|
||||
91
tests/unit/feature-triage/resync.test.mjs
Normal file
91
tests/unit/feature-triage/resync.test.mjs
Normal file
@@ -0,0 +1,91 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resyncIdeaFile } from "../../../scripts/features/lib/resync.mjs";
|
||||
import { serializeFrontmatter } from "../../../scripts/features/lib/frontmatter.mjs";
|
||||
|
||||
function makeIdeaFile(
|
||||
meta,
|
||||
body = "# Feature: X\n\n## Community Discussion\n\nInitial summary.\n"
|
||||
) {
|
||||
return serializeFrontmatter(meta, body);
|
||||
}
|
||||
|
||||
const META_V1 = {
|
||||
issue: 1046,
|
||||
last_synced_at: "2026-05-01T00:00:00Z",
|
||||
last_synced_comment_id: 100,
|
||||
snapshot: { thumbs: 2, commenters: 1, age_days: 10, state: "open" },
|
||||
};
|
||||
|
||||
describe("resyncIdeaFile", () => {
|
||||
it("returns unchanged when file has no frontmatter", () => {
|
||||
const text = "# Plain idea file\nNo frontmatter here.";
|
||||
const r = resyncIdeaFile(text, { comments: [] });
|
||||
assert.equal(r.changed, false);
|
||||
assert.equal(r.text, text);
|
||||
});
|
||||
|
||||
it("appends only new comments (id > last_synced_comment_id)", () => {
|
||||
const text = makeIdeaFile(META_V1);
|
||||
const issue = {
|
||||
comments: [
|
||||
{
|
||||
databaseId: 100,
|
||||
author: { login: "alice" },
|
||||
createdAt: "2026-05-01T00:00:00Z",
|
||||
body: "old",
|
||||
},
|
||||
{
|
||||
databaseId: 200,
|
||||
author: { login: "bob" },
|
||||
createdAt: "2026-05-10T00:00:00Z",
|
||||
body: "NEW comment",
|
||||
},
|
||||
{
|
||||
databaseId: 300,
|
||||
author: { login: "carol" },
|
||||
createdAt: "2026-05-15T00:00:00Z",
|
||||
body: "ANOTHER NEW",
|
||||
},
|
||||
],
|
||||
reactionGroups: [{ content: "THUMBS_UP", users: { totalCount: 5 } }],
|
||||
labels: [{ name: "enhancement" }],
|
||||
state: "OPEN",
|
||||
};
|
||||
const r = resyncIdeaFile(text, issue, new Date("2026-05-19T00:00:00Z"));
|
||||
assert.equal(r.changed, true);
|
||||
assert.match(r.text, /NEW comment/);
|
||||
assert.match(r.text, /ANOTHER NEW/);
|
||||
assert.doesNotMatch(r.text, /^.*\bold\b.*$/m);
|
||||
|
||||
assert.match(r.text, /last_synced_comment_id:\s*300/);
|
||||
assert.match(r.text, /thumbs:\s*5/);
|
||||
});
|
||||
|
||||
it("returns unchanged when no new comments", () => {
|
||||
const text = makeIdeaFile(META_V1);
|
||||
const r = resyncIdeaFile(text, { comments: [{ databaseId: 50 }] }, new Date());
|
||||
assert.equal(r.changed, false);
|
||||
});
|
||||
|
||||
it("flags needsReclassification when in need_details/ and author replied", () => {
|
||||
const text = makeIdeaFile(META_V1);
|
||||
const issue = {
|
||||
author: { login: "alice" },
|
||||
comments: [
|
||||
{
|
||||
databaseId: 200,
|
||||
author: { login: "alice" },
|
||||
createdAt: "2026-05-10T00:00:00Z",
|
||||
body: "Here are the details!",
|
||||
},
|
||||
],
|
||||
reactionGroups: [],
|
||||
labels: [],
|
||||
state: "OPEN",
|
||||
};
|
||||
const r = resyncIdeaFile(text, issue, new Date(), { inNeedDetails: true });
|
||||
assert.equal(r.changed, true);
|
||||
assert.equal(r.needsReclassification, true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user