@hir4ta/mneme 0.20.2 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -5
- package/README.ja.md +45 -283
- package/README.md +48 -280
- package/dist/lib/db.js +7 -5
- package/dist/lib/incremental-save.js +122 -28
- package/dist/lib/prompt-search.js +570 -0
- package/dist/lib/search-core.js +516 -0
- package/dist/lib/session-finalize.js +983 -0
- package/dist/lib/session-init.js +397 -0
- package/dist/lib/suppress-sqlite-warning.js +8 -0
- package/dist/public/assets/index-Bvl_IrPy.css +1 -0
- package/dist/public/assets/index-k5JYSPV6.js +351 -0
- package/dist/public/assets/{react-force-graph-2d-CGnpkwRw.js → react-force-graph-2d-Dlcfvz01.js} +1 -1
- package/dist/public/index.html +2 -2
- package/dist/server.js +565 -37
- package/dist/servers/db-server.js +1301 -98
- package/dist/servers/search-server.js +613 -333
- package/hooks/hooks.json +1 -0
- package/hooks/lib/common.sh +55 -0
- package/hooks/post-tool-use.sh +52 -58
- package/hooks/pre-compact.sh +30 -42
- package/hooks/session-end.sh +30 -142
- package/hooks/session-start.sh +32 -337
- package/hooks/stop.sh +31 -42
- package/hooks/user-prompt-submit.sh +58 -212
- package/package.json +10 -3
- package/scripts/export-weekly-knowledge-html.ts +906 -0
- package/scripts/search-benchmark.queries.json +78 -0
- package/scripts/search-benchmark.ts +120 -0
- package/scripts/validate-source-artifacts.mjs +378 -0
- package/servers/db-server.ts +995 -65
- package/servers/search-server.ts +117 -528
- package/skills/harvest/SKILL.md +78 -0
- package/skills/init-mneme/{skill.md → SKILL.md} +7 -1
- package/skills/resume/{skill.md → SKILL.md} +24 -9
- package/skills/save/SKILL.md +131 -0
- package/skills/search/SKILL.md +76 -0
- package/skills/using-mneme/SKILL.md +38 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/public/assets/index-CeHiZXwl.js +0 -345
- package/dist/public/assets/index-t_srr1OD.css +0 -1
- package/learn_claude_code/figma_exports/claude_code_map.svg +0 -107
- package/learn_claude_code/figma_exports/claude_code_whiteboard.excalidraw +0 -2578
- package/skills/AGENTS.override.md +0 -5
- package/skills/harvest/skill.md +0 -295
- package/skills/plan/skill.md +0 -422
- package/skills/report/skill.md +0 -74
- package/skills/review/skill.md +0 -419
- package/skills/save/skill.md +0 -496
- package/skills/search/skill.md +0 -175
- package/skills/using-mneme/skill.md +0 -185
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
interface DecisionItem {
|
|
5
|
+
id: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
decision?: string;
|
|
8
|
+
reasoning?: string;
|
|
9
|
+
tags?: string[];
|
|
10
|
+
createdAt?: string;
|
|
11
|
+
updatedAt?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PatternItem {
|
|
15
|
+
id: string;
|
|
16
|
+
type?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
errorPattern?: string;
|
|
20
|
+
solution?: string;
|
|
21
|
+
tags?: string[];
|
|
22
|
+
createdAt?: string;
|
|
23
|
+
updatedAt?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RuleItem {
|
|
27
|
+
id: string;
|
|
28
|
+
key?: string;
|
|
29
|
+
text?: string;
|
|
30
|
+
rule?: string;
|
|
31
|
+
category?: string;
|
|
32
|
+
priority?: string;
|
|
33
|
+
rationale?: string;
|
|
34
|
+
tags?: string[];
|
|
35
|
+
createdAt?: string;
|
|
36
|
+
updatedAt?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface UnitItem {
|
|
40
|
+
id: string;
|
|
41
|
+
type: "decision" | "pattern" | "rule";
|
|
42
|
+
kind: "policy" | "pitfall" | "playbook";
|
|
43
|
+
title: string;
|
|
44
|
+
summary: string;
|
|
45
|
+
tags: string[];
|
|
46
|
+
sourceType: "decision" | "pattern" | "rule";
|
|
47
|
+
sourceId: string;
|
|
48
|
+
status: "pending" | "approved" | "rejected";
|
|
49
|
+
createdAt: string;
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
reviewedAt?: string;
|
|
52
|
+
reviewedBy?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface AuditEntry {
|
|
56
|
+
timestamp: string;
|
|
57
|
+
actor?: string;
|
|
58
|
+
entity?: string;
|
|
59
|
+
action?: string;
|
|
60
|
+
targetId?: string;
|
|
61
|
+
detail?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface HighlightCard {
|
|
65
|
+
title: string;
|
|
66
|
+
subtitle: string;
|
|
67
|
+
subtitleJa: string;
|
|
68
|
+
body: string;
|
|
69
|
+
tags: string[];
|
|
70
|
+
score: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toDateOrNull(value: unknown): Date | null {
|
|
74
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
75
|
+
const date = new Date(value);
|
|
76
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isWithinRange(date: Date | null, from: Date, to: Date): boolean {
|
|
80
|
+
if (!date) return false;
|
|
81
|
+
const time = date.getTime();
|
|
82
|
+
return time >= from.getTime() && time <= to.getTime();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readJsonFile<T>(filePath: string): T | null {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8")) as T;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function listJsonFiles(dir: string): string[] {
|
|
94
|
+
if (!fs.existsSync(dir)) return [];
|
|
95
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
96
|
+
return entries.flatMap((entry) => {
|
|
97
|
+
const fullPath = path.join(dir, entry.name);
|
|
98
|
+
if (entry.isDirectory()) return listJsonFiles(fullPath);
|
|
99
|
+
return entry.isFile() && entry.name.endsWith(".json") ? [fullPath] : [];
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readJsonl(filePath: string): AuditEntry[] {
|
|
104
|
+
if (!fs.existsSync(filePath)) return [];
|
|
105
|
+
const lines = fs.readFileSync(filePath, "utf-8").split("\n");
|
|
106
|
+
const entries: AuditEntry[] = [];
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
if (!line.trim()) continue;
|
|
109
|
+
try {
|
|
110
|
+
entries.push(JSON.parse(line) as AuditEntry);
|
|
111
|
+
} catch {
|
|
112
|
+
// skip invalid line
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return entries;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function escapeHtml(value: string): string {
|
|
119
|
+
return value
|
|
120
|
+
.replace(/&/g, "&")
|
|
121
|
+
.replace(/</g, "<")
|
|
122
|
+
.replace(/>/g, ">")
|
|
123
|
+
.replace(/"/g, """)
|
|
124
|
+
.replace(/'/g, "'");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function formatDate(value: Date): string {
|
|
128
|
+
return value.toISOString().slice(0, 10);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function startOfDay(value: Date): Date {
|
|
132
|
+
const date = new Date(value);
|
|
133
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
134
|
+
return date;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function endOfDay(value: Date): Date {
|
|
138
|
+
const date = new Date(value);
|
|
139
|
+
date.setUTCHours(23, 59, 59, 999);
|
|
140
|
+
return date;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildApprovedKnowledgeCards(units: UnitItem[]): HighlightCard[] {
|
|
144
|
+
const cards: HighlightCard[] = [];
|
|
145
|
+
for (const item of units) {
|
|
146
|
+
const sourceLabelJa =
|
|
147
|
+
item.sourceType === "decision"
|
|
148
|
+
? "意思決定"
|
|
149
|
+
: item.sourceType === "pattern"
|
|
150
|
+
? "パターン"
|
|
151
|
+
: "ルール";
|
|
152
|
+
const latestTimestamp = toDateOrNull(
|
|
153
|
+
item.reviewedAt || item.updatedAt || item.createdAt,
|
|
154
|
+
);
|
|
155
|
+
const freshnessScore = latestTimestamp
|
|
156
|
+
? Math.max(
|
|
157
|
+
0,
|
|
158
|
+
100 -
|
|
159
|
+
Math.floor(
|
|
160
|
+
(Date.now() - latestTimestamp.getTime()) / (1000 * 60 * 60 * 24),
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
: 0;
|
|
164
|
+
cards.push({
|
|
165
|
+
title: item.title,
|
|
166
|
+
subtitle: `Approved ${item.sourceType}`,
|
|
167
|
+
subtitleJa: `承認済み ${sourceLabelJa}`,
|
|
168
|
+
body: item.summary,
|
|
169
|
+
tags: Array.isArray(item.tags) ? item.tags : [],
|
|
170
|
+
score: 60 + freshnessScore,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return cards.sort((a, b) => b.score - a.score).slice(0, 6);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderHtml(params: {
|
|
178
|
+
from: Date;
|
|
179
|
+
to: Date;
|
|
180
|
+
generatedAt: Date;
|
|
181
|
+
newDecisions: DecisionItem[];
|
|
182
|
+
newPatterns: PatternItem[];
|
|
183
|
+
changedRules: RuleItem[];
|
|
184
|
+
touchedUnits: UnitItem[];
|
|
185
|
+
approvedUnits: UnitItem[];
|
|
186
|
+
approvedKnowledgeUnits: UnitItem[];
|
|
187
|
+
pendingUnits: UnitItem[];
|
|
188
|
+
auditEntries: AuditEntry[];
|
|
189
|
+
}): string {
|
|
190
|
+
const {
|
|
191
|
+
from,
|
|
192
|
+
to,
|
|
193
|
+
generatedAt,
|
|
194
|
+
newDecisions,
|
|
195
|
+
newPatterns,
|
|
196
|
+
changedRules,
|
|
197
|
+
touchedUnits,
|
|
198
|
+
approvedUnits,
|
|
199
|
+
approvedKnowledgeUnits,
|
|
200
|
+
pendingUnits,
|
|
201
|
+
auditEntries,
|
|
202
|
+
} = params;
|
|
203
|
+
|
|
204
|
+
const highlights = buildApprovedKnowledgeCards(approvedKnowledgeUnits);
|
|
205
|
+
|
|
206
|
+
const topTags = new Map<string, number>();
|
|
207
|
+
for (const item of highlights) {
|
|
208
|
+
for (const tag of item.tags) {
|
|
209
|
+
topTags.set(tag, (topTags.get(tag) || 0) + 1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const topTagList = Array.from(topTags.entries())
|
|
213
|
+
.sort((a, b) => b[1] - a[1])
|
|
214
|
+
.slice(0, 12);
|
|
215
|
+
|
|
216
|
+
const actorCounts = new Map<string, number>();
|
|
217
|
+
for (const entry of auditEntries) {
|
|
218
|
+
const actor = entry.actor || "unknown";
|
|
219
|
+
actorCounts.set(actor, (actorCounts.get(actor) || 0) + 1);
|
|
220
|
+
}
|
|
221
|
+
const topActors = Array.from(actorCounts.entries())
|
|
222
|
+
.sort((a, b) => b[1] - a[1])
|
|
223
|
+
.slice(0, 6);
|
|
224
|
+
|
|
225
|
+
const highlightHtml = highlights
|
|
226
|
+
.map((item) => {
|
|
227
|
+
const tags = item.tags
|
|
228
|
+
.slice(0, 5)
|
|
229
|
+
.map((tag) => `<span class="chip">${escapeHtml(tag)}</span>`)
|
|
230
|
+
.join("");
|
|
231
|
+
return `
|
|
232
|
+
<article class="knowledge-card">
|
|
233
|
+
<p class="sub"><span data-i18n-item="en">${escapeHtml(item.subtitle)}</span><span data-i18n-item="ja">${escapeHtml(item.subtitleJa)}</span></p>
|
|
234
|
+
<h3>${escapeHtml(item.title)}</h3>
|
|
235
|
+
<p>${escapeHtml(item.body)}</p>
|
|
236
|
+
<div class="chips">${tags}</div>
|
|
237
|
+
</article>
|
|
238
|
+
`;
|
|
239
|
+
})
|
|
240
|
+
.join("\n");
|
|
241
|
+
|
|
242
|
+
const tagsHtml = topTagList
|
|
243
|
+
.map(
|
|
244
|
+
([tag, count]) =>
|
|
245
|
+
`<span class="tag-pill">${escapeHtml(tag)} <b>${count}</b></span>`,
|
|
246
|
+
)
|
|
247
|
+
.join("\n");
|
|
248
|
+
|
|
249
|
+
const actorsHtml = topActors
|
|
250
|
+
.map(
|
|
251
|
+
([actor, count]) => `<li>${escapeHtml(actor)} <span>${count}</span></li>`,
|
|
252
|
+
)
|
|
253
|
+
.join("\n");
|
|
254
|
+
|
|
255
|
+
const pendingByType = pendingUnits.reduce(
|
|
256
|
+
(acc, unit) => {
|
|
257
|
+
acc[unit.type] = (acc[unit.type] || 0) + 1;
|
|
258
|
+
return acc;
|
|
259
|
+
},
|
|
260
|
+
{} as Record<string, number>,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const approvalRate =
|
|
264
|
+
touchedUnits.length > 0
|
|
265
|
+
? Math.round((approvedUnits.length / touchedUnits.length) * 100)
|
|
266
|
+
: 0;
|
|
267
|
+
const newKnowledgeCount =
|
|
268
|
+
newDecisions.length + newPatterns.length + changedRules.length;
|
|
269
|
+
|
|
270
|
+
const actionHints = [
|
|
271
|
+
{
|
|
272
|
+
en:
|
|
273
|
+
pendingUnits.length > 0
|
|
274
|
+
? `Pending approvals remain (${pendingUnits.length}). Prioritize high-impact units first.`
|
|
275
|
+
: "No pending units. Approval queue is healthy.",
|
|
276
|
+
ja:
|
|
277
|
+
pendingUnits.length > 0
|
|
278
|
+
? `承認待ちユニットが ${pendingUnits.length} 件あります。影響の大きいものから優先承認してください。`
|
|
279
|
+
: "承認待ちはありません。承認キューは健全です。",
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
en:
|
|
283
|
+
changedRules.length > 0
|
|
284
|
+
? `Rules changed this week (${changedRules.length}). Share rationale with the team.`
|
|
285
|
+
: "No rule changes this week. Consider capturing reusable standards.",
|
|
286
|
+
ja:
|
|
287
|
+
changedRules.length > 0
|
|
288
|
+
? `今週はルール変更が ${changedRules.length} 件ありました。背景理由をチームへ共有してください。`
|
|
289
|
+
: "今週のルール変更はありません。再利用できる標準を追加できないか確認してください。",
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
en:
|
|
293
|
+
newPatterns.length > 0
|
|
294
|
+
? `New patterns detected (${newPatterns.length}). Promote stable ones to approved units.`
|
|
295
|
+
: "Low pattern capture. Run /mneme:save more frequently during debugging sessions.",
|
|
296
|
+
ja:
|
|
297
|
+
newPatterns.length > 0
|
|
298
|
+
? `今週の新規パターンは ${newPatterns.length} 件です。安定したものは承認済みユニットへ昇格してください。`
|
|
299
|
+
: "パターンの蓄積が少なめです。デバッグ中は /mneme:save をこまめに実行してください。",
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
return `<!doctype html>
|
|
304
|
+
<html lang="ja" data-lang="ja">
|
|
305
|
+
<head>
|
|
306
|
+
<meta charset="utf-8" />
|
|
307
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
308
|
+
<title>Weekly Knowledge Report</title>
|
|
309
|
+
<style>
|
|
310
|
+
:root {
|
|
311
|
+
--bg: #efede8;
|
|
312
|
+
--bg-soft: #e7e4de;
|
|
313
|
+
--ink: #1f1e1b;
|
|
314
|
+
--ink-subtle: #4f4b44;
|
|
315
|
+
--card: #f7f4ef;
|
|
316
|
+
--card-deep: #ece8e1;
|
|
317
|
+
--line: #d4d0c8;
|
|
318
|
+
--line-strong: #b5b0a6;
|
|
319
|
+
--chip: #ebe7e0;
|
|
320
|
+
--chip-border: #ccc6bb;
|
|
321
|
+
--shadow: rgba(20, 19, 17, 0.08);
|
|
322
|
+
--hero-dark: #262521;
|
|
323
|
+
--hero-mid: #34322d;
|
|
324
|
+
--hero-soft: #43413a;
|
|
325
|
+
}
|
|
326
|
+
* {
|
|
327
|
+
box-sizing: border-box;
|
|
328
|
+
}
|
|
329
|
+
body {
|
|
330
|
+
margin: 0;
|
|
331
|
+
font-family: "Nunito", "Avenir Next", "Segoe UI", sans-serif;
|
|
332
|
+
color: var(--ink);
|
|
333
|
+
background:
|
|
334
|
+
radial-gradient(980px 460px at 3% -10%, #f8f6f2 0%, transparent 72%),
|
|
335
|
+
radial-gradient(900px 420px at 100% 0%, #e2ded6 0%, transparent 68%),
|
|
336
|
+
repeating-linear-gradient(
|
|
337
|
+
135deg,
|
|
338
|
+
rgba(255, 255, 255, 0.18) 0 2px,
|
|
339
|
+
rgba(255, 255, 255, 0) 2px 8px
|
|
340
|
+
),
|
|
341
|
+
linear-gradient(180deg, var(--bg), var(--bg-soft));
|
|
342
|
+
line-height: 1.5;
|
|
343
|
+
}
|
|
344
|
+
.wrap {
|
|
345
|
+
max-width: 1160px;
|
|
346
|
+
margin: 0 auto;
|
|
347
|
+
padding: 32px 24px 64px;
|
|
348
|
+
}
|
|
349
|
+
.hero {
|
|
350
|
+
position: relative;
|
|
351
|
+
overflow: hidden;
|
|
352
|
+
background: linear-gradient(
|
|
353
|
+
145deg,
|
|
354
|
+
var(--hero-dark) 0%,
|
|
355
|
+
var(--hero-mid) 56%,
|
|
356
|
+
var(--hero-soft) 100%
|
|
357
|
+
);
|
|
358
|
+
color: #f5f4f1;
|
|
359
|
+
border: 1px solid #4a4740;
|
|
360
|
+
border-radius: 18px;
|
|
361
|
+
padding: 24px;
|
|
362
|
+
box-shadow: 0 12px 30px rgba(10, 10, 10, 0.24);
|
|
363
|
+
}
|
|
364
|
+
.hero::after {
|
|
365
|
+
content: "";
|
|
366
|
+
position: absolute;
|
|
367
|
+
width: 220px;
|
|
368
|
+
height: 220px;
|
|
369
|
+
right: -50px;
|
|
370
|
+
top: -60px;
|
|
371
|
+
border-radius: 999px;
|
|
372
|
+
background: radial-gradient(circle, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0));
|
|
373
|
+
pointer-events: none;
|
|
374
|
+
}
|
|
375
|
+
.hero-row {
|
|
376
|
+
display: flex;
|
|
377
|
+
justify-content: space-between;
|
|
378
|
+
gap: 12px;
|
|
379
|
+
align-items: flex-start;
|
|
380
|
+
position: relative;
|
|
381
|
+
z-index: 1;
|
|
382
|
+
}
|
|
383
|
+
.hero h1 {
|
|
384
|
+
margin: 0 0 10px;
|
|
385
|
+
font-size: 32px;
|
|
386
|
+
letter-spacing: 0.1px;
|
|
387
|
+
}
|
|
388
|
+
.hero p {
|
|
389
|
+
margin: 0;
|
|
390
|
+
opacity: 0.94;
|
|
391
|
+
max-width: 760px;
|
|
392
|
+
}
|
|
393
|
+
.hero-sticker {
|
|
394
|
+
display: inline-flex;
|
|
395
|
+
margin-bottom: 12px;
|
|
396
|
+
background: #f0ece5;
|
|
397
|
+
color: #201f1c;
|
|
398
|
+
border: 1px solid #d7d2c7;
|
|
399
|
+
border-radius: 999px;
|
|
400
|
+
padding: 4px 10px;
|
|
401
|
+
font-size: 11px;
|
|
402
|
+
font-weight: 700;
|
|
403
|
+
letter-spacing: 0.08em;
|
|
404
|
+
text-transform: uppercase;
|
|
405
|
+
transform: rotate(-2deg);
|
|
406
|
+
}
|
|
407
|
+
.meta {
|
|
408
|
+
margin-top: 12px;
|
|
409
|
+
font-size: 14px;
|
|
410
|
+
opacity: 0.88;
|
|
411
|
+
}
|
|
412
|
+
.lang-switch {
|
|
413
|
+
display: inline-flex;
|
|
414
|
+
border: 1px solid #6a665f;
|
|
415
|
+
border-radius: 999px;
|
|
416
|
+
overflow: hidden;
|
|
417
|
+
background: rgba(255, 255, 255, 0.04);
|
|
418
|
+
}
|
|
419
|
+
.lang-switch button {
|
|
420
|
+
border: 0;
|
|
421
|
+
background: transparent;
|
|
422
|
+
color: #dad8d2;
|
|
423
|
+
padding: 7px 12px;
|
|
424
|
+
font-size: 12px;
|
|
425
|
+
cursor: pointer;
|
|
426
|
+
transition: background-color 120ms ease, color 120ms ease;
|
|
427
|
+
}
|
|
428
|
+
.lang-switch button.active {
|
|
429
|
+
background: #ece8e1;
|
|
430
|
+
color: #1f1f1c;
|
|
431
|
+
}
|
|
432
|
+
.grid {
|
|
433
|
+
display: grid;
|
|
434
|
+
gap: 14px;
|
|
435
|
+
margin-top: 18px;
|
|
436
|
+
}
|
|
437
|
+
.kpi-grid {
|
|
438
|
+
grid-template-columns: repeat(6, minmax(120px, 1fr));
|
|
439
|
+
}
|
|
440
|
+
.kpi {
|
|
441
|
+
background: var(--card);
|
|
442
|
+
border: 1px solid var(--line);
|
|
443
|
+
border-radius: 12px;
|
|
444
|
+
padding: 14px;
|
|
445
|
+
min-height: 96px;
|
|
446
|
+
box-shadow: 0 2px 10px var(--shadow);
|
|
447
|
+
position: relative;
|
|
448
|
+
overflow: hidden;
|
|
449
|
+
}
|
|
450
|
+
.kpi::before {
|
|
451
|
+
content: "";
|
|
452
|
+
position: absolute;
|
|
453
|
+
width: 4px;
|
|
454
|
+
top: 0;
|
|
455
|
+
bottom: 0;
|
|
456
|
+
left: 0;
|
|
457
|
+
background: linear-gradient(180deg, #3a3935, #8b867c);
|
|
458
|
+
}
|
|
459
|
+
.kpi .label {
|
|
460
|
+
color: var(--ink-subtle);
|
|
461
|
+
font-size: 12px;
|
|
462
|
+
text-transform: uppercase;
|
|
463
|
+
letter-spacing: 0.08em;
|
|
464
|
+
}
|
|
465
|
+
.kpi .value {
|
|
466
|
+
font-size: 30px;
|
|
467
|
+
font-weight: 700;
|
|
468
|
+
margin-top: 6px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.section {
|
|
472
|
+
margin-top: 26px;
|
|
473
|
+
}
|
|
474
|
+
.section h2 {
|
|
475
|
+
margin: 0 0 10px;
|
|
476
|
+
font-size: 22px;
|
|
477
|
+
letter-spacing: -0.01em;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.knowledge-grid {
|
|
481
|
+
display: grid;
|
|
482
|
+
gap: 12px;
|
|
483
|
+
grid-template-columns: repeat(2, minmax(260px, 1fr));
|
|
484
|
+
}
|
|
485
|
+
.knowledge-card {
|
|
486
|
+
background: var(--card);
|
|
487
|
+
border: 1px solid var(--line);
|
|
488
|
+
border-radius: 12px;
|
|
489
|
+
padding: 16px;
|
|
490
|
+
box-shadow: 0 2px 8px var(--shadow);
|
|
491
|
+
position: relative;
|
|
492
|
+
transition: transform 140ms ease, box-shadow 140ms ease;
|
|
493
|
+
}
|
|
494
|
+
.knowledge-card:nth-child(odd) {
|
|
495
|
+
transform: rotate(-0.35deg);
|
|
496
|
+
}
|
|
497
|
+
.knowledge-card:nth-child(even) {
|
|
498
|
+
transform: rotate(0.35deg);
|
|
499
|
+
}
|
|
500
|
+
.knowledge-card:hover {
|
|
501
|
+
transform: translateY(-2px) rotate(0deg);
|
|
502
|
+
box-shadow: 0 8px 18px rgba(15, 14, 12, 0.12);
|
|
503
|
+
}
|
|
504
|
+
.knowledge-card .sub {
|
|
505
|
+
margin: 0 0 6px;
|
|
506
|
+
color: var(--ink-subtle);
|
|
507
|
+
font-size: 12px;
|
|
508
|
+
text-transform: uppercase;
|
|
509
|
+
letter-spacing: 0.08em;
|
|
510
|
+
font-weight: 700;
|
|
511
|
+
}
|
|
512
|
+
.knowledge-card h3 {
|
|
513
|
+
margin: 0 0 8px;
|
|
514
|
+
font-size: 18px;
|
|
515
|
+
}
|
|
516
|
+
.knowledge-card p {
|
|
517
|
+
margin: 0;
|
|
518
|
+
color: #3a3731;
|
|
519
|
+
}
|
|
520
|
+
.chips {
|
|
521
|
+
margin-top: 10px;
|
|
522
|
+
display: flex;
|
|
523
|
+
flex-wrap: wrap;
|
|
524
|
+
gap: 6px;
|
|
525
|
+
}
|
|
526
|
+
.chip {
|
|
527
|
+
background: var(--chip);
|
|
528
|
+
border: 1px solid var(--chip-border);
|
|
529
|
+
border-radius: 999px;
|
|
530
|
+
font-size: 12px;
|
|
531
|
+
padding: 3px 9px;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.pulse-grid {
|
|
535
|
+
display: grid;
|
|
536
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
537
|
+
gap: 12px;
|
|
538
|
+
}
|
|
539
|
+
.pulse {
|
|
540
|
+
background: var(--card-deep);
|
|
541
|
+
border: 1px solid var(--line);
|
|
542
|
+
border-radius: 12px;
|
|
543
|
+
padding: 12px;
|
|
544
|
+
}
|
|
545
|
+
.pulse-label {
|
|
546
|
+
font-size: 12px;
|
|
547
|
+
color: var(--ink-subtle);
|
|
548
|
+
text-transform: uppercase;
|
|
549
|
+
letter-spacing: 0.08em;
|
|
550
|
+
}
|
|
551
|
+
.pulse-note {
|
|
552
|
+
margin-top: 8px;
|
|
553
|
+
font-size: 12px;
|
|
554
|
+
color: var(--ink-subtle);
|
|
555
|
+
line-height: 1.45;
|
|
556
|
+
}
|
|
557
|
+
.pulse-value {
|
|
558
|
+
font-size: 28px;
|
|
559
|
+
font-weight: 700;
|
|
560
|
+
margin: 8px 0 10px;
|
|
561
|
+
}
|
|
562
|
+
.meter {
|
|
563
|
+
height: 8px;
|
|
564
|
+
background: #ddd7ce;
|
|
565
|
+
border-radius: 999px;
|
|
566
|
+
overflow: hidden;
|
|
567
|
+
}
|
|
568
|
+
.meter > i {
|
|
569
|
+
display: block;
|
|
570
|
+
height: 100%;
|
|
571
|
+
background: linear-gradient(90deg, #31302b, #7b766d);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.split {
|
|
575
|
+
display: grid;
|
|
576
|
+
grid-template-columns: 1.2fr 1fr;
|
|
577
|
+
gap: 14px;
|
|
578
|
+
}
|
|
579
|
+
.panel {
|
|
580
|
+
background: var(--card);
|
|
581
|
+
border: 1px solid var(--line);
|
|
582
|
+
border-radius: 12px;
|
|
583
|
+
padding: 16px;
|
|
584
|
+
box-shadow: 0 2px 8px var(--shadow);
|
|
585
|
+
}
|
|
586
|
+
.tag-pill {
|
|
587
|
+
display: inline-flex;
|
|
588
|
+
align-items: center;
|
|
589
|
+
gap: 8px;
|
|
590
|
+
margin: 0 8px 8px 0;
|
|
591
|
+
background: var(--chip);
|
|
592
|
+
border: 1px solid var(--chip-border);
|
|
593
|
+
border-radius: 999px;
|
|
594
|
+
padding: 5px 10px;
|
|
595
|
+
font-size: 13px;
|
|
596
|
+
}
|
|
597
|
+
.actors {
|
|
598
|
+
list-style: none;
|
|
599
|
+
margin: 0;
|
|
600
|
+
padding: 0;
|
|
601
|
+
}
|
|
602
|
+
.actors li {
|
|
603
|
+
display: flex;
|
|
604
|
+
justify-content: space-between;
|
|
605
|
+
border-bottom: 1px dashed var(--line);
|
|
606
|
+
padding: 8px 0;
|
|
607
|
+
}
|
|
608
|
+
.actors li span { color: var(--ink); font-weight: 700; }
|
|
609
|
+
|
|
610
|
+
.actions ol {
|
|
611
|
+
margin: 8px 0 0 20px;
|
|
612
|
+
padding: 0;
|
|
613
|
+
}
|
|
614
|
+
.actions li {
|
|
615
|
+
margin: 8px 0;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
footer {
|
|
619
|
+
margin-top: 28px;
|
|
620
|
+
color: var(--ink-subtle);
|
|
621
|
+
font-size: 13px;
|
|
622
|
+
text-align: right;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
@media (max-width: 980px) {
|
|
626
|
+
.hero-row { flex-direction: column; }
|
|
627
|
+
.kpi-grid { grid-template-columns: repeat(3, minmax(120px, 1fr)); }
|
|
628
|
+
.knowledge-grid { grid-template-columns: 1fr; }
|
|
629
|
+
.pulse-grid { grid-template-columns: 1fr; }
|
|
630
|
+
.split { grid-template-columns: 1fr; }
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
@media print {
|
|
634
|
+
body { background: #fff; }
|
|
635
|
+
.wrap { max-width: none; padding: 0; }
|
|
636
|
+
.hero { box-shadow: none; }
|
|
637
|
+
.panel, .kpi, .knowledge-card { break-inside: avoid; }
|
|
638
|
+
}
|
|
639
|
+
</style>
|
|
640
|
+
</head>
|
|
641
|
+
<body>
|
|
642
|
+
<div class="wrap">
|
|
643
|
+
<section class="hero">
|
|
644
|
+
<div class="hero-row">
|
|
645
|
+
<div>
|
|
646
|
+
<div class="hero-sticker">
|
|
647
|
+
<span data-i18n="stickerEn">TEAM MEMORY ISSUE</span>
|
|
648
|
+
<span data-i18n="stickerJa">TEAM MEMORY ISSUE</span>
|
|
649
|
+
</div>
|
|
650
|
+
<h1>
|
|
651
|
+
<span data-i18n="heroTitleEn">Weekly Knowledge Snapshot</span>
|
|
652
|
+
<span data-i18n="heroTitleJa">週次ナレッジスナップショット</span>
|
|
653
|
+
</h1>
|
|
654
|
+
<p>
|
|
655
|
+
<span data-i18n="heroDescEn">This week, your team captured and refined project knowledge across sessions, units, and source artifacts.</span>
|
|
656
|
+
<span data-i18n="heroDescJa">この1週間で、チームはセッション・ユニット・元データを通じて知見を蓄積し、整理しました。</span>
|
|
657
|
+
</p>
|
|
658
|
+
<div class="meta">
|
|
659
|
+
<span data-i18n="metaEn">Period: ${formatDate(from)} to ${formatDate(to)} | Generated: ${generatedAt.toISOString()}</span>
|
|
660
|
+
<span data-i18n="metaJa">期間: ${formatDate(from)} 〜 ${formatDate(to)} | 生成日時: ${generatedAt.toISOString()}</span>
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
<div class="lang-switch">
|
|
664
|
+
<button type="button" data-lang-btn="ja">日本語</button>
|
|
665
|
+
<button type="button" data-lang-btn="en">EN</button>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
</section>
|
|
669
|
+
|
|
670
|
+
<section class="grid kpi-grid">
|
|
671
|
+
<article class="kpi"><div class="label"><span data-i18n="kpiDecisionEn">New Decisions</span><span data-i18n="kpiDecisionJa">新規意思決定</span></div><div class="value">${newDecisions.length}</div></article>
|
|
672
|
+
<article class="kpi"><div class="label"><span data-i18n="kpiPatternEn">New Patterns</span><span data-i18n="kpiPatternJa">新規パターン</span></div><div class="value">${newPatterns.length}</div></article>
|
|
673
|
+
<article class="kpi"><div class="label"><span data-i18n="kpiRuleEn">Changed Rules</span><span data-i18n="kpiRuleJa">変更ルール</span></div><div class="value">${changedRules.length}</div></article>
|
|
674
|
+
<article class="kpi"><div class="label"><span data-i18n="kpiTouchedEn">Touched Units</span><span data-i18n="kpiTouchedJa">更新ユニット</span></div><div class="value">${touchedUnits.length}</div></article>
|
|
675
|
+
<article class="kpi"><div class="label"><span data-i18n="kpiApprovedEn">Approved Units</span><span data-i18n="kpiApprovedJa">承認済みユニット</span></div><div class="value">${approvedUnits.length}</div></article>
|
|
676
|
+
<article class="kpi"><div class="label"><span data-i18n="kpiPendingEn">Pending Units</span><span data-i18n="kpiPendingJa">承認待ちユニット</span></div><div class="value">${pendingUnits.length}</div></article>
|
|
677
|
+
</section>
|
|
678
|
+
|
|
679
|
+
<section class="section">
|
|
680
|
+
<h2><span data-i18n="pulseTitleEn">This Week at a Glance</span><span data-i18n="pulseTitleJa">今週の状況サマリー</span></h2>
|
|
681
|
+
<div class="pulse-grid">
|
|
682
|
+
<article class="pulse">
|
|
683
|
+
<div class="pulse-label"><span data-i18n="pulseApprovalEn">Approval Rate</span><span data-i18n="pulseApprovalJa">承認率</span></div>
|
|
684
|
+
<div class="pulse-value">${approvalRate}%</div>
|
|
685
|
+
<div class="meter"><i style="width:${approvalRate}%"></i></div>
|
|
686
|
+
<div class="pulse-note">
|
|
687
|
+
<span data-i18n="pulseApprovalNoteEn">Among units touched this week, how many were approved.</span>
|
|
688
|
+
<span data-i18n="pulseApprovalNoteJa">今週更新されたユニットのうち、承認済みになった割合です。</span>
|
|
689
|
+
</div>
|
|
690
|
+
</article>
|
|
691
|
+
<article class="pulse">
|
|
692
|
+
<div class="pulse-label"><span data-i18n="pulseSignalEn">New Knowledge Captured</span><span data-i18n="pulseSignalJa">新規ナレッジ登録</span></div>
|
|
693
|
+
<div class="pulse-value">${newKnowledgeCount}</div>
|
|
694
|
+
<div class="meter"><i style="width:${Math.min(100, newKnowledgeCount * 8)}%"></i></div>
|
|
695
|
+
<div class="pulse-note">
|
|
696
|
+
<span data-i18n="pulseSignalNoteEn">Decisions + patterns + rule updates recorded in this period.</span>
|
|
697
|
+
<span data-i18n="pulseSignalNoteJa">この期間に記録された意思決定・パターン・ルール更新の合計件数です。</span>
|
|
698
|
+
</div>
|
|
699
|
+
</article>
|
|
700
|
+
<article class="pulse">
|
|
701
|
+
<div class="pulse-label"><span data-i18n="pulseQueueEn">Pending Approvals</span><span data-i18n="pulseQueueJa">承認待ち件数</span></div>
|
|
702
|
+
<div class="pulse-value">${pendingUnits.length}</div>
|
|
703
|
+
<div class="meter"><i style="width:${Math.min(100, pendingUnits.length * 10)}%"></i></div>
|
|
704
|
+
<div class="pulse-note">
|
|
705
|
+
<span data-i18n="pulseQueueNoteEn">Units waiting for approval in the project-wide queue.</span>
|
|
706
|
+
<span data-i18n="pulseQueueNoteJa">プロジェクト全体で現在承認待ちのユニット件数です。</span>
|
|
707
|
+
</div>
|
|
708
|
+
</article>
|
|
709
|
+
</div>
|
|
710
|
+
</section>
|
|
711
|
+
|
|
712
|
+
<section class="section">
|
|
713
|
+
<h2><span data-i18n="topCardsEn">Project Approved Knowledge</span><span data-i18n="topCardsJa">プロジェクトで承認されたナレッジ</span></h2>
|
|
714
|
+
<div class="knowledge-grid">
|
|
715
|
+
${highlightHtml || '<p><span data-i18n="emptyHighlightsEn">No approved knowledge yet.</span><span data-i18n="emptyHighlightsJa">承認済みナレッジはまだありません。</span></p>'}
|
|
716
|
+
</div>
|
|
717
|
+
</section>
|
|
718
|
+
|
|
719
|
+
<section class="section split">
|
|
720
|
+
<article class="panel">
|
|
721
|
+
<h2><span data-i18n="tagHeatEn">Tag Heat</span><span data-i18n="tagHeatJa">タグ頻度</span></h2>
|
|
722
|
+
<div>${tagsHtml || '<p><span data-i18n="emptyTagsEn">No tags in this period.</span><span data-i18n="emptyTagsJa">この期間のタグはありません。</span></p>'}</div>
|
|
723
|
+
<h2 style="margin-top:18px"><span data-i18n="approvalLoadEn">Approval Load</span><span data-i18n="approvalLoadJa">承認キュー内訳</span></h2>
|
|
724
|
+
<p>
|
|
725
|
+
<span data-i18n="pendingByTypeEn">Pending by type: decision=${pendingByType.decision || 0}, pattern=${pendingByType.pattern || 0}, rule=${pendingByType.rule || 0}</span>
|
|
726
|
+
<span data-i18n="pendingByTypeJa">種別ごとの承認待ち: decision=${pendingByType.decision || 0}, pattern=${pendingByType.pattern || 0}, rule=${pendingByType.rule || 0}</span>
|
|
727
|
+
</p>
|
|
728
|
+
</article>
|
|
729
|
+
<article class="panel">
|
|
730
|
+
<h2><span data-i18n="topContribEn">Top Contributors (Audit)</span><span data-i18n="topContribJa">主要コントリビューター(監査ログ)</span></h2>
|
|
731
|
+
<ul class="actors">${actorsHtml || '<li><span data-i18n="emptyActivityEn">no activity</span><span data-i18n="emptyActivityJa">活動なし</span> <span>0</span></li>'}</ul>
|
|
732
|
+
</article>
|
|
733
|
+
</section>
|
|
734
|
+
|
|
735
|
+
<section class="section panel actions">
|
|
736
|
+
<h2><span data-i18n="nextActionEn">Suggested Next Actions</span><span data-i18n="nextActionJa">次のアクション提案</span></h2>
|
|
737
|
+
<ol>
|
|
738
|
+
${actionHints
|
|
739
|
+
.map(
|
|
740
|
+
(item) =>
|
|
741
|
+
`<li><span data-i18n-item="en">${escapeHtml(item.en)}</span><span data-i18n-item="ja">${escapeHtml(item.ja)}</span></li>`,
|
|
742
|
+
)
|
|
743
|
+
.join("\n")}
|
|
744
|
+
</ol>
|
|
745
|
+
</section>
|
|
746
|
+
|
|
747
|
+
<footer>
|
|
748
|
+
<span data-i18n="footerEn">Generated by mneme weekly HTML export</span>
|
|
749
|
+
<span data-i18n="footerJa">mneme 週次HTMLエクスポートで生成</span>
|
|
750
|
+
</footer>
|
|
751
|
+
</div>
|
|
752
|
+
<script>
|
|
753
|
+
(function () {
|
|
754
|
+
const root = document.documentElement;
|
|
755
|
+
const key = "mneme-weekly-lang";
|
|
756
|
+
const buttons = Array.from(document.querySelectorAll("[data-lang-btn]"));
|
|
757
|
+
function applyLang(lang) {
|
|
758
|
+
root.setAttribute("data-lang", lang);
|
|
759
|
+
root.setAttribute("lang", lang === "ja" ? "ja" : "en");
|
|
760
|
+
for (const button of buttons) {
|
|
761
|
+
const isActive = button.getAttribute("data-lang-btn") === lang;
|
|
762
|
+
button.classList.toggle("active", isActive);
|
|
763
|
+
}
|
|
764
|
+
try { localStorage.setItem(key, lang); } catch {}
|
|
765
|
+
}
|
|
766
|
+
const preferred = (function () {
|
|
767
|
+
try {
|
|
768
|
+
const saved = localStorage.getItem(key);
|
|
769
|
+
if (saved === "ja" || saved === "en") return saved;
|
|
770
|
+
} catch {}
|
|
771
|
+
return (navigator.language || "").toLowerCase().startsWith("ja") ? "ja" : "en";
|
|
772
|
+
})();
|
|
773
|
+
for (const button of buttons) {
|
|
774
|
+
button.addEventListener("click", function () {
|
|
775
|
+
applyLang(button.getAttribute("data-lang-btn") || "ja");
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
applyLang(preferred);
|
|
779
|
+
})();
|
|
780
|
+
</script>
|
|
781
|
+
<style>
|
|
782
|
+
[data-i18n$="En"], [data-i18n-item="en"] { display: none; }
|
|
783
|
+
html[data-lang="en"] [data-i18n$="En"],
|
|
784
|
+
html[data-lang="en"] [data-i18n-item="en"] { display: inline; }
|
|
785
|
+
html[data-lang="en"] [data-i18n$="Ja"],
|
|
786
|
+
html[data-lang="en"] [data-i18n-item="ja"] { display: none; }
|
|
787
|
+
html[data-lang="ja"] [data-i18n$="Ja"],
|
|
788
|
+
html[data-lang="ja"] [data-i18n-item="ja"] { display: inline; }
|
|
789
|
+
html[data-lang="ja"] [data-i18n$="En"],
|
|
790
|
+
html[data-lang="ja"] [data-i18n-item="en"] { display: none; }
|
|
791
|
+
</style>
|
|
792
|
+
</body>
|
|
793
|
+
</html>`;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function main() {
|
|
797
|
+
const projectRoot = process.cwd();
|
|
798
|
+
const mnemeDir = path.join(projectRoot, ".mneme");
|
|
799
|
+
if (!fs.existsSync(mnemeDir)) {
|
|
800
|
+
console.error(".mneme directory not found. Run /init-mneme first.");
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const now = new Date();
|
|
805
|
+
const to = endOfDay(now);
|
|
806
|
+
const from = startOfDay(new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000));
|
|
807
|
+
|
|
808
|
+
const decisions = listJsonFiles(path.join(mnemeDir, "decisions"))
|
|
809
|
+
.map((filePath) => readJsonFile<DecisionItem>(filePath))
|
|
810
|
+
.filter((item): item is DecisionItem => !!item)
|
|
811
|
+
.filter((item) =>
|
|
812
|
+
isWithinRange(toDateOrNull(item.createdAt || item.updatedAt), from, to),
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
const patterns = listJsonFiles(path.join(mnemeDir, "patterns"))
|
|
816
|
+
.flatMap((filePath) => {
|
|
817
|
+
const doc = readJsonFile<{
|
|
818
|
+
items?: PatternItem[];
|
|
819
|
+
patterns?: PatternItem[];
|
|
820
|
+
}>(filePath);
|
|
821
|
+
const items = doc?.items || doc?.patterns || [];
|
|
822
|
+
return Array.isArray(items) ? items : [];
|
|
823
|
+
})
|
|
824
|
+
.filter((item) =>
|
|
825
|
+
isWithinRange(toDateOrNull(item.createdAt || item.updatedAt), from, to),
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
const ruleFiles = [
|
|
829
|
+
path.join(mnemeDir, "rules", "dev-rules.json"),
|
|
830
|
+
path.join(mnemeDir, "rules", "review-guidelines.json"),
|
|
831
|
+
];
|
|
832
|
+
const changedRules = ruleFiles
|
|
833
|
+
.flatMap((filePath) => {
|
|
834
|
+
const doc = readJsonFile<{ items?: RuleItem[]; rules?: RuleItem[] }>(
|
|
835
|
+
filePath,
|
|
836
|
+
);
|
|
837
|
+
const items = doc?.items || doc?.rules || [];
|
|
838
|
+
return Array.isArray(items) ? items : [];
|
|
839
|
+
})
|
|
840
|
+
.filter((item) =>
|
|
841
|
+
isWithinRange(toDateOrNull(item.updatedAt || item.createdAt), from, to),
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
const unitsDoc = readJsonFile<{ items?: UnitItem[] }>(
|
|
845
|
+
path.join(mnemeDir, "units", "units.json"),
|
|
846
|
+
);
|
|
847
|
+
const allUnits = Array.isArray(unitsDoc?.items) ? unitsDoc.items : [];
|
|
848
|
+
const touchedUnits = allUnits.filter((item) => {
|
|
849
|
+
return (
|
|
850
|
+
isWithinRange(toDateOrNull(item.createdAt), from, to) ||
|
|
851
|
+
isWithinRange(toDateOrNull(item.updatedAt), from, to) ||
|
|
852
|
+
isWithinRange(toDateOrNull(item.reviewedAt), from, to)
|
|
853
|
+
);
|
|
854
|
+
});
|
|
855
|
+
const approvedUnits = touchedUnits.filter(
|
|
856
|
+
(item) => item.status === "approved",
|
|
857
|
+
);
|
|
858
|
+
const approvedKnowledgeUnits = allUnits
|
|
859
|
+
.filter((item) => item.status === "approved")
|
|
860
|
+
.sort((a, b) => {
|
|
861
|
+
const at = toDateOrNull(a.reviewedAt || a.updatedAt || a.createdAt);
|
|
862
|
+
const bt = toDateOrNull(b.reviewedAt || b.updatedAt || b.createdAt);
|
|
863
|
+
return (bt?.getTime() || 0) - (at?.getTime() || 0);
|
|
864
|
+
});
|
|
865
|
+
const pendingUnits = allUnits.filter((item) => item.status === "pending");
|
|
866
|
+
|
|
867
|
+
const auditEntries = fs.existsSync(path.join(mnemeDir, "audit"))
|
|
868
|
+
? fs
|
|
869
|
+
.readdirSync(path.join(mnemeDir, "audit"))
|
|
870
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
871
|
+
.flatMap((name) => readJsonl(path.join(mnemeDir, "audit", name)))
|
|
872
|
+
.filter((entry) =>
|
|
873
|
+
isWithinRange(toDateOrNull(entry.timestamp), from, to),
|
|
874
|
+
)
|
|
875
|
+
.sort(
|
|
876
|
+
(a, b) =>
|
|
877
|
+
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
|
|
878
|
+
)
|
|
879
|
+
: [];
|
|
880
|
+
|
|
881
|
+
const html = renderHtml({
|
|
882
|
+
from,
|
|
883
|
+
to,
|
|
884
|
+
generatedAt: now,
|
|
885
|
+
newDecisions: decisions,
|
|
886
|
+
newPatterns: patterns,
|
|
887
|
+
changedRules,
|
|
888
|
+
touchedUnits,
|
|
889
|
+
approvedUnits,
|
|
890
|
+
approvedKnowledgeUnits,
|
|
891
|
+
pendingUnits,
|
|
892
|
+
auditEntries,
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
const outputDir = path.join(mnemeDir, "exports");
|
|
896
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
897
|
+
const outputPath = path.join(
|
|
898
|
+
outputDir,
|
|
899
|
+
`weekly-knowledge-${formatDate(now)}.html`,
|
|
900
|
+
);
|
|
901
|
+
fs.writeFileSync(outputPath, html, "utf-8");
|
|
902
|
+
|
|
903
|
+
console.log(`Generated weekly knowledge HTML: ${outputPath}`);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
main();
|