minimal-vibe-coding-kit 0.4.2 → 0.5.1
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/.agents/skills/claim/SKILL.md +82 -0
- package/.agents/skills/tutien/SKILL.md +97 -0
- package/.agents/skills/tutien/references/adaptive-response.md +68 -0
- package/.agents/skills/tutien/references/classification.md +64 -0
- package/.agents/skills/tutien/references/lore-sources.md +43 -0
- package/.agents/skills/tutien/references/privacy.md +45 -0
- package/.agents/skills/tutien/references/schema.md +106 -0
- package/.agents/skills/tutien/references/scoring-and-realms.md +37 -0
- package/.agents/skills/tutien/references/story-system.md +176 -0
- package/.agents/skills/tutien/references/vi-style-guide.md +89 -0
- package/.agents/skills/tutien/references/voice-and-mode.md +59 -0
- package/.agents/skills/tutien/scripts/adapters/generic-jsonl.mjs +82 -0
- package/.agents/skills/tutien/scripts/adapters/git.mjs +48 -0
- package/.agents/skills/tutien/scripts/adapters/plain-transcript.mjs +37 -0
- package/.agents/skills/tutien/scripts/analyze-history.mjs +84 -0
- package/.agents/skills/tutien/scripts/catalog.mjs +130 -0
- package/.agents/skills/tutien/scripts/classify.mjs +338 -0
- package/.agents/skills/tutien/scripts/command.mjs +126 -0
- package/.agents/skills/tutien/scripts/compare.mjs +41 -0
- package/.agents/skills/tutien/scripts/metrics.mjs +306 -0
- package/.agents/skills/tutien/scripts/normalize-events.mjs +68 -0
- package/.agents/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.agents/skills/tutien/scripts/redact.mjs +30 -0
- package/.agents/skills/tutien/scripts/render-report.mjs +479 -0
- package/.agents/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.agents/skills/tutien/scripts/run-tutien.mjs +350 -0
- package/.agents/skills/tutien/scripts/score.mjs +97 -0
- package/.agents/skills/tutien/scripts/snapshot.mjs +76 -0
- package/.agents/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.agents/skills/tutien/scripts/villains.mjs +196 -0
- package/.claude/skills/claim/SKILL.md +82 -0
- package/.claude/skills/tutien/SKILL.md +97 -0
- package/.claude/skills/tutien/references/adaptive-response.md +68 -0
- package/.claude/skills/tutien/references/classification.md +64 -0
- package/.claude/skills/tutien/references/lore-sources.md +43 -0
- package/.claude/skills/tutien/references/privacy.md +45 -0
- package/.claude/skills/tutien/references/schema.md +106 -0
- package/.claude/skills/tutien/references/scoring-and-realms.md +37 -0
- package/.claude/skills/tutien/references/story-system.md +176 -0
- package/.claude/skills/tutien/references/vi-style-guide.md +89 -0
- package/.claude/skills/tutien/references/voice-and-mode.md +59 -0
- package/.claude/skills/tutien/scripts/adapters/generic-jsonl.mjs +82 -0
- package/.claude/skills/tutien/scripts/adapters/git.mjs +48 -0
- package/.claude/skills/tutien/scripts/adapters/plain-transcript.mjs +37 -0
- package/.claude/skills/tutien/scripts/analyze-history.mjs +84 -0
- package/.claude/skills/tutien/scripts/catalog.mjs +130 -0
- package/.claude/skills/tutien/scripts/classify.mjs +338 -0
- package/.claude/skills/tutien/scripts/command.mjs +126 -0
- package/.claude/skills/tutien/scripts/compare.mjs +41 -0
- package/.claude/skills/tutien/scripts/metrics.mjs +306 -0
- package/.claude/skills/tutien/scripts/normalize-events.mjs +68 -0
- package/.claude/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.claude/skills/tutien/scripts/redact.mjs +30 -0
- package/.claude/skills/tutien/scripts/render-report.mjs +479 -0
- package/.claude/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.claude/skills/tutien/scripts/run-tutien.mjs +350 -0
- package/.claude/skills/tutien/scripts/score.mjs +97 -0
- package/.claude/skills/tutien/scripts/snapshot.mjs +76 -0
- package/.claude/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.claude/skills/tutien/scripts/villains.mjs +196 -0
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/skills/claim/SKILL.md +82 -0
- package/.cursor/skills/tutien/SKILL.md +97 -0
- package/.cursor/skills/tutien/references/adaptive-response.md +68 -0
- package/.cursor/skills/tutien/references/classification.md +64 -0
- package/.cursor/skills/tutien/references/lore-sources.md +43 -0
- package/.cursor/skills/tutien/references/privacy.md +45 -0
- package/.cursor/skills/tutien/references/schema.md +106 -0
- package/.cursor/skills/tutien/references/scoring-and-realms.md +37 -0
- package/.cursor/skills/tutien/references/story-system.md +176 -0
- package/.cursor/skills/tutien/references/vi-style-guide.md +89 -0
- package/.cursor/skills/tutien/references/voice-and-mode.md +59 -0
- package/.cursor/skills/tutien/scripts/adapters/generic-jsonl.mjs +82 -0
- package/.cursor/skills/tutien/scripts/adapters/git.mjs +48 -0
- package/.cursor/skills/tutien/scripts/adapters/plain-transcript.mjs +37 -0
- package/.cursor/skills/tutien/scripts/analyze-history.mjs +84 -0
- package/.cursor/skills/tutien/scripts/catalog.mjs +130 -0
- package/.cursor/skills/tutien/scripts/classify.mjs +338 -0
- package/.cursor/skills/tutien/scripts/command.mjs +126 -0
- package/.cursor/skills/tutien/scripts/compare.mjs +41 -0
- package/.cursor/skills/tutien/scripts/metrics.mjs +306 -0
- package/.cursor/skills/tutien/scripts/normalize-events.mjs +68 -0
- package/.cursor/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.cursor/skills/tutien/scripts/redact.mjs +30 -0
- package/.cursor/skills/tutien/scripts/render-report.mjs +479 -0
- package/.cursor/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.cursor/skills/tutien/scripts/run-tutien.mjs +350 -0
- package/.cursor/skills/tutien/scripts/score.mjs +97 -0
- package/.cursor/skills/tutien/scripts/snapshot.mjs +76 -0
- package/.cursor/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.cursor/skills/tutien/scripts/villains.mjs +196 -0
- package/.grok/skills/claim/SKILL.md +82 -0
- package/.grok/skills/tutien/SKILL.md +97 -0
- package/.grok/skills/tutien/references/adaptive-response.md +68 -0
- package/.grok/skills/tutien/references/classification.md +64 -0
- package/.grok/skills/tutien/references/lore-sources.md +43 -0
- package/.grok/skills/tutien/references/privacy.md +45 -0
- package/.grok/skills/tutien/references/schema.md +106 -0
- package/.grok/skills/tutien/references/scoring-and-realms.md +37 -0
- package/.grok/skills/tutien/references/story-system.md +176 -0
- package/.grok/skills/tutien/references/vi-style-guide.md +89 -0
- package/.grok/skills/tutien/references/voice-and-mode.md +59 -0
- package/.grok/skills/tutien/scripts/adapters/generic-jsonl.mjs +82 -0
- package/.grok/skills/tutien/scripts/adapters/git.mjs +48 -0
- package/.grok/skills/tutien/scripts/adapters/plain-transcript.mjs +37 -0
- package/.grok/skills/tutien/scripts/analyze-history.mjs +84 -0
- package/.grok/skills/tutien/scripts/catalog.mjs +130 -0
- package/.grok/skills/tutien/scripts/classify.mjs +338 -0
- package/.grok/skills/tutien/scripts/command.mjs +126 -0
- package/.grok/skills/tutien/scripts/compare.mjs +41 -0
- package/.grok/skills/tutien/scripts/metrics.mjs +306 -0
- package/.grok/skills/tutien/scripts/normalize-events.mjs +68 -0
- package/.grok/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.grok/skills/tutien/scripts/redact.mjs +30 -0
- package/.grok/skills/tutien/scripts/render-report.mjs +479 -0
- package/.grok/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.grok/skills/tutien/scripts/run-tutien.mjs +350 -0
- package/.grok/skills/tutien/scripts/score.mjs +97 -0
- package/.grok/skills/tutien/scripts/snapshot.mjs +76 -0
- package/.grok/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.grok/skills/tutien/scripts/villains.mjs +196 -0
- package/.vibekit/docs/BACKBONE_REFERENCE.md +2 -0
- package/.vibekit/docs/INSTALL.md +3 -1
- package/.vibekit/init/CLAUDE-template.md +2 -0
- package/.vibekit/scripts/mvck.mjs +5 -5
- package/.vibekit/scripts/pack-dry-run.mjs +38 -3
- package/.vibekit/scripts/validate-kit.mjs +34 -24
- package/.vibekit/skills/claim/SKILL.md +82 -0
- package/.vibekit/skills/skills-manifest.json +27 -0
- package/.vibekit/skills/tutien/SKILL.md +97 -0
- package/.vibekit/skills/tutien/references/adaptive-response.md +68 -0
- package/.vibekit/skills/tutien/references/classification.md +64 -0
- package/.vibekit/skills/tutien/references/lore-sources.md +43 -0
- package/.vibekit/skills/tutien/references/privacy.md +45 -0
- package/.vibekit/skills/tutien/references/schema.md +106 -0
- package/.vibekit/skills/tutien/references/scoring-and-realms.md +37 -0
- package/.vibekit/skills/tutien/references/story-system.md +176 -0
- package/.vibekit/skills/tutien/references/vi-style-guide.md +89 -0
- package/.vibekit/skills/tutien/references/voice-and-mode.md +59 -0
- package/.vibekit/skills/tutien/scripts/adapters/generic-jsonl.mjs +82 -0
- package/.vibekit/skills/tutien/scripts/adapters/git.mjs +48 -0
- package/.vibekit/skills/tutien/scripts/adapters/plain-transcript.mjs +37 -0
- package/.vibekit/skills/tutien/scripts/analyze-history.mjs +84 -0
- package/.vibekit/skills/tutien/scripts/catalog.mjs +130 -0
- package/.vibekit/skills/tutien/scripts/classify.mjs +338 -0
- package/.vibekit/skills/tutien/scripts/command.mjs +126 -0
- package/.vibekit/skills/tutien/scripts/compare.mjs +41 -0
- package/.vibekit/skills/tutien/scripts/metrics.mjs +306 -0
- package/.vibekit/skills/tutien/scripts/normalize-events.mjs +68 -0
- package/.vibekit/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.vibekit/skills/tutien/scripts/redact.mjs +30 -0
- package/.vibekit/skills/tutien/scripts/render-report.mjs +479 -0
- package/.vibekit/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.vibekit/skills/tutien/scripts/run-tutien.mjs +350 -0
- package/.vibekit/skills/tutien/scripts/score.mjs +97 -0
- package/.vibekit/skills/tutien/scripts/snapshot.mjs +76 -0
- package/.vibekit/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.vibekit/skills/tutien/scripts/villains.mjs +196 -0
- package/CHANGELOG.md +51 -0
- package/README.md +53 -47
- package/docs/README.vi.md +14 -8
- package/docs/README.zh-CN.md +279 -0
- package/package.json +9 -3
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import {
|
|
9
|
+
normalizeStoryFocus,
|
|
10
|
+
normalizeStoryLanguage,
|
|
11
|
+
normalizeStoryStyle,
|
|
12
|
+
parseInvocation,
|
|
13
|
+
TUTIEN_EXPERIENCE
|
|
14
|
+
} from './command.mjs';
|
|
15
|
+
import { analyze } from './analyze-history.mjs';
|
|
16
|
+
import { renderReport, resolveLanguage, renderClassificationMarkdown } from './render-report.mjs';
|
|
17
|
+
import { buildSnapshot, snapshotsToPrune } from './snapshot.mjs';
|
|
18
|
+
import { compareSnapshots } from './compare.mjs';
|
|
19
|
+
import { classifyProject } from './classify.mjs';
|
|
20
|
+
import { buildStoryContext, storyPaths, validateStoryTree, writeStoryContext } from './story-ledger.mjs';
|
|
21
|
+
import { buildProjectProfile, inventoryProjectMetadata } from './project-profile.mjs';
|
|
22
|
+
import { buildResponseBrief } from './response-brief.mjs';
|
|
23
|
+
|
|
24
|
+
// End-to-end executor for the advertised /tutien actions. All state lives
|
|
25
|
+
// under the git-ignored .vibekit/reports/tutien/ of the current repo; the
|
|
26
|
+
// runner never deletes anything (retention only prints a `trash` command).
|
|
27
|
+
//
|
|
28
|
+
// run-tutien.mjs [on|off|status|preview|analyze|compare|explain|classify] [k=v ...]
|
|
29
|
+
//
|
|
30
|
+
// Approval boundary: `preview` prints the exact scope and an approval token;
|
|
31
|
+
// `analyze` refuses to read anything until it receives approve=<that token>
|
|
32
|
+
// for the identical scope.
|
|
33
|
+
|
|
34
|
+
const sha16 = (s) => crypto.createHash('sha256').update(s).digest('hex').slice(0, 16);
|
|
35
|
+
|
|
36
|
+
const root = process.cwd();
|
|
37
|
+
const reportDir = path.join(root, '.vibekit', 'reports', 'tutien');
|
|
38
|
+
const stateFile = path.join(reportDir, 'state.json');
|
|
39
|
+
const snapDir = path.join(reportDir, 'snapshots');
|
|
40
|
+
|
|
41
|
+
function loadState() {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
44
|
+
} catch {
|
|
45
|
+
return { mode: 'off' };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function saveState(state) {
|
|
49
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
50
|
+
fs.writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveStoryPreferences(state, options, providedOptions = []) {
|
|
54
|
+
const provided = new Set(providedOptions);
|
|
55
|
+
const prior = state.storyPreferences ?? {};
|
|
56
|
+
const choose = (key, optionKey, fallback) => provided.has(key) ? options[optionKey] : (prior[optionKey] ?? options[optionKey] ?? fallback);
|
|
57
|
+
const preferences = {
|
|
58
|
+
story: choose('story', 'story', 'on') === 'off' ? 'off' : 'on',
|
|
59
|
+
storyLanguage: normalizeStoryLanguage(choose('story-language', 'storyLanguage', 'auto')),
|
|
60
|
+
storyStyle: normalizeStoryStyle(choose('story-style', 'storyStyle', 'auto')),
|
|
61
|
+
storyFocus: normalizeStoryFocus(choose('story-focus', 'storyFocus', 'balanced'))
|
|
62
|
+
};
|
|
63
|
+
state.storyPreferences = preferences;
|
|
64
|
+
return preferences;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function classifySources(sources) {
|
|
68
|
+
const spec = { jsonlFiles: [], transcriptFiles: [], gitRoot: null };
|
|
69
|
+
for (const s of sources) {
|
|
70
|
+
if (s === 'git') spec.gitRoot = root;
|
|
71
|
+
else if (s.endsWith('.jsonl')) spec.jsonlFiles.push(s);
|
|
72
|
+
else spec.transcriptFiles.push(s);
|
|
73
|
+
}
|
|
74
|
+
return spec;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const scopeToken = (sources, range, metadata = inventoryProjectMetadata(root)) => sha16(JSON.stringify({
|
|
78
|
+
sources: [...sources].sort(),
|
|
79
|
+
range,
|
|
80
|
+
metadata: metadata.map(({ path: file, size, mtimeMs }) => ({ path: file, size, mtimeMs }))
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
// Project profile for classification: a bounded set of known manifests plus a
|
|
84
|
+
// distinct-commit-author COUNT. Script bodies, names, emails, and file content
|
|
85
|
+
// are never returned or persisted.
|
|
86
|
+
function repoProfile(options) {
|
|
87
|
+
const profile = buildProjectProfile(root, options);
|
|
88
|
+
// Author identifiers are read transiently (mailmap-collapsed) and reduced to
|
|
89
|
+
// a distinct COUNT immediately; no email or name is retained or returned.
|
|
90
|
+
let authorsCount = null;
|
|
91
|
+
const res = spawnSync('git', ['log', '--use-mailmap', '--format=%aE'], { cwd: root, encoding: 'utf8' });
|
|
92
|
+
if (res.status === 0) authorsCount = new Set(res.stdout.split('\n').filter(Boolean)).size;
|
|
93
|
+
return { ...profile, authorsCount };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Preview inspects no content: file existence and size only, plus the git
|
|
97
|
+
// toplevel path. Nothing is parsed and nothing is written except the pending
|
|
98
|
+
// approval in the state file.
|
|
99
|
+
function previewLines(options, language = 'en') {
|
|
100
|
+
const vi = language === 'vi';
|
|
101
|
+
const metadata = inventoryProjectMetadata(root);
|
|
102
|
+
const lines = [vi
|
|
103
|
+
? 'Xem trước phạm vi tu tiên: đài quan sát đã mở nhưng chưa đọc cuộn thư nào. Chỉ phân tích sau khi phạm vi chính xác này được phê duyệt.'
|
|
104
|
+
: 'tutien preview — the observatory is open, but no scroll has been read; nothing is analyzed until this exact scope is approved.'];
|
|
105
|
+
if (options.sources.length === 0) lines.push(vi ? '- Chưa có nguồn. Hãy truyền `sources=git,/duong-dan/export.jsonl`.' : '- (no sources; pass sources=git,/path/to/export.jsonl)');
|
|
106
|
+
for (const s of options.sources) {
|
|
107
|
+
if (s === 'git') {
|
|
108
|
+
lines.push(vi ? `- Git: chỉ đọc siêu dữ liệu bản ghi của kho hiện tại tại ${root}.` : `- git: commit metadata of ${root} (read-only git log, current repository only)`);
|
|
109
|
+
} else {
|
|
110
|
+
let size = null;
|
|
111
|
+
try {
|
|
112
|
+
size = fs.statSync(s).size;
|
|
113
|
+
} catch {}
|
|
114
|
+
lines.push(size == null
|
|
115
|
+
? (vi ? `- Tệp: ${s} (không tìm thấy)` : `- file: ${s} (NOT FOUND)`)
|
|
116
|
+
: (vi ? `- Tệp: ${s} (${size} byte)` : `- file: ${s} (${size} bytes)`));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (metadata.length) {
|
|
120
|
+
lines.push(vi
|
|
121
|
+
? `- Metadata dự án: ${metadata.map((item) => `${item.path} (${item.size} byte)`).join(', ')}; chỉ đọc nội dung sau khi phê duyệt.`
|
|
122
|
+
: `- project metadata: ${metadata.map((item) => `${item.path} (${item.size} bytes)`).join(', ')}; content is read only after approval.`);
|
|
123
|
+
} else {
|
|
124
|
+
lines.push(vi ? '- Metadata dự án: không tìm thấy manifest đã biết.' : '- project metadata: no known manifest found.');
|
|
125
|
+
}
|
|
126
|
+
lines.push(vi ? `- Phạm vi thời gian: ${options.range}` : `- range: ${options.range}`);
|
|
127
|
+
const token = scopeToken(options.sources, options.range, metadata);
|
|
128
|
+
lines.push(vi
|
|
129
|
+
? `Để phê duyệt đúng phạm vi này: \`analyze approve=${token} sources=${options.sources.join(',')}\``
|
|
130
|
+
: `To approve exactly this scope: analyze approve=${token} sources=${options.sources.join(',')}`);
|
|
131
|
+
return { lines, token };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const EXPLAIN = {
|
|
135
|
+
reportedTotal: 'Sum of provider-reported usable token totals; cumulative streaming records sharing a requestId are counted once (last value), independent chunks are summed. Records without a computable positive total never contribute.',
|
|
136
|
+
estimatedTotal: 'Sum of locally estimated usable token totals; never mixed into reportedTotal.',
|
|
137
|
+
unknownTurns: 'Assistant message turns whose usage is absent or has no usable total; claimed accuracy without numbers counts as unknown.',
|
|
138
|
+
retryLoops: 'Runs of 3+ exact/near-identical user prompts within one task with intervening assistant/tool events; confidence-scored candidates.',
|
|
139
|
+
conflicts: 'A "never/do not X" instruction followed in the same task by a later prompt requesting X; paired event IDs, confidence 0.6.',
|
|
140
|
+
realm: 'Weighted dimension score (delivery 30%, validation 25%, clarity 15%, recovery 15%, safety 10%, automation 5%) mapped to the realm ladder; requires >= 60% known-token coverage, otherwise "Chưa đủ thiên cơ".'
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const EXPLAIN_VI = {
|
|
144
|
+
reportedTotal: 'Tổng số token hợp lệ do nhà cung cấp báo cáo. Bản ghi luồng tích lũy có cùng `requestId` chỉ được tính một lần theo giá trị cuối; các đoạn độc lập được cộng riêng. Bản ghi không có tổng dương hợp lệ sẽ không được tính.',
|
|
145
|
+
estimatedTotal: 'Tổng số token hợp lệ được ước lượng cục bộ; không bao giờ gộp vào `reportedTotal`.',
|
|
146
|
+
unknownTurns: 'Số lượt trả lời của trợ lý không có dữ liệu sử dụng hoặc không có tổng hợp lệ. Tuyên bố độ chính xác mà thiếu số liệu vẫn được tính là chưa rõ.',
|
|
147
|
+
retryLoops: 'Chuỗi từ ba lời yêu cầu giống hoặc gần giống nhau trong cùng một nhiệm vụ, xen giữa bởi phản hồi của trợ lý hay công cụ; đây là các dấu hiệu có chấm mức tin cậy.',
|
|
148
|
+
conflicts: 'Một chỉ thị dạng “không bao giờ” hoặc “không làm X”, sau đó trong cùng nhiệm vụ xuất hiện lời yêu cầu làm X; cặp mã sự kiện có độ tin cậy 0,6.',
|
|
149
|
+
realm: 'Điểm có trọng số từ sáu chiều được ánh xạ vào thang cảnh giới. Cần ít nhất 60% độ phủ token đã biết; nếu thiếu sẽ trả về “Chưa đủ thiên cơ”.'
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export function run(argsString = '') {
|
|
153
|
+
const { action, explicitAction, options, providedOptions } = parseInvocation(argsString);
|
|
154
|
+
const state = loadState();
|
|
155
|
+
const storyPreferences = resolveStoryPreferences(state, options, providedOptions);
|
|
156
|
+
const uiLanguage = resolveLanguage(options.language, { invocationText: argsString });
|
|
157
|
+
const say = (vi, en) => uiLanguage === 'vi' ? vi : en;
|
|
158
|
+
const refuse = (msg) => ({ code: 2, out: [msg] });
|
|
159
|
+
|
|
160
|
+
if (action === 'off') {
|
|
161
|
+
state.mode = 'off';
|
|
162
|
+
delete state.pendingApproval;
|
|
163
|
+
saveState(state);
|
|
164
|
+
return { code: 0, out: [say('Chế độ tu tiên đã tắt. Văn phong bình thường của bộ công cụ đã được khôi phục.', 'Tutien mode is off. Normal kit writing style restored.')] };
|
|
165
|
+
}
|
|
166
|
+
if (action === 'on') {
|
|
167
|
+
state.mode = 'on';
|
|
168
|
+
saveState(state);
|
|
169
|
+
return { code: 0, out: [say('Chế độ tu tiên đã bật. Sơn môn đã mở; hãy chạy `preview` để xem phạm vi chính xác trước khi phân tích.', 'Tutien mode is on — the mountain gate is open. Run preview to see the exact scope before any analysis.')] };
|
|
170
|
+
}
|
|
171
|
+
if (action === 'status') {
|
|
172
|
+
const snaps = fs.existsSync(snapDir) ? fs.readdirSync(snapDir).length : 0;
|
|
173
|
+
const storyStatus = validateStoryTree(root);
|
|
174
|
+
return {
|
|
175
|
+
code: 0,
|
|
176
|
+
out: [
|
|
177
|
+
say(`chế độ: ${(state.mode ?? 'off') === 'on' ? 'bật' : 'tắt'}`, `mode: ${state.mode ?? 'off'}`),
|
|
178
|
+
say(`trải nghiệm: \`${TUTIEN_EXPERIENCE.kind}\``, `experience: ${TUTIEN_EXPERIENCE.kind}`),
|
|
179
|
+
say(`giọng văn: \`${TUTIEN_EXPERIENCE.narrativeStyle}\``, `voice: ${TUTIEN_EXPERIENCE.narrativeStyle}`),
|
|
180
|
+
say(`không gian ngữ nghĩa: \`${TUTIEN_EXPERIENCE.semanticNamespace}\``, `semantic namespace: ${TUTIEN_EXPERIENCE.semanticNamespace}`),
|
|
181
|
+
say(`phê duyệt đang chờ: ${state.pendingApproval ?? 'không có'}`, `pending approval: ${state.pendingApproval ?? 'none'}`),
|
|
182
|
+
say(`ảnh chụp tổng hợp: ${snaps}`, `snapshots: ${snaps}`),
|
|
183
|
+
say(`trường thiên: ${storyPreferences.story === 'on' ? 'bật' : 'tắt'} (${storyPreferences.storyLanguage}, ${storyPreferences.storyStyle}, ${storyPreferences.storyFocus})`, `story: ${storyPreferences.story} (${storyPreferences.storyLanguage}, ${storyPreferences.storyStyle}, ${storyPreferences.storyFocus})`),
|
|
184
|
+
say(`số chương: ${storyStatus.chapters}`, `story chapters: ${storyStatus.chapters}`),
|
|
185
|
+
say(`tổng cương: ${path.relative(root, storyPaths(root).plot)}`, `story plot: ${path.relative(root, storyPaths(root).plot)}`),
|
|
186
|
+
say(`sổ chương: ${storyStatus.ok ? 'hợp lệ' : `cần xem lại (${storyStatus.errors.length} lỗi)`}`, `story ledger: ${storyStatus.ok ? 'valid' : `needs attention (${storyStatus.errors.length})`}`)
|
|
187
|
+
]
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Report actions. A bare `/tutien` re-activates the mode; an explicit
|
|
192
|
+
// report action while off is refused (off-mode suppression).
|
|
193
|
+
if ((state.mode ?? 'off') !== 'on') {
|
|
194
|
+
if (explicitAction) return refuse(say('Chế độ tu tiên đang tắt. Hãy chạy `on` hoặc gọi `/tutien` trước.', "tutien mode is off; run 'on' (or bare /tutien) first."));
|
|
195
|
+
state.mode = 'on';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (action === 'preview') {
|
|
199
|
+
const { lines, token } = previewLines(options, uiLanguage);
|
|
200
|
+
lines.push(say(
|
|
201
|
+
`- Trường thiên: ${storyPreferences.story === 'on' ? 'bật' : 'tắt'}; ngôn ngữ ${storyPreferences.storyLanguage}, phong cách ${storyPreferences.storyStyle}, trọng tâm ${storyPreferences.storyFocus}.`,
|
|
202
|
+
`- living chronicle: ${storyPreferences.story} (language=${storyPreferences.storyLanguage}, style=${storyPreferences.storyStyle}, focus=${storyPreferences.storyFocus})`
|
|
203
|
+
));
|
|
204
|
+
state.pendingApproval = token;
|
|
205
|
+
saveState(state);
|
|
206
|
+
return { code: 0, out: lines };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (action === 'classify') {
|
|
210
|
+
// classify is a metadata-only action (no history content): it reads only
|
|
211
|
+
// the known manifests advertised below and a transient author count.
|
|
212
|
+
let classification;
|
|
213
|
+
try {
|
|
214
|
+
classification = classifyProject(repoProfile(options));
|
|
215
|
+
} catch (err) {
|
|
216
|
+
return refuse(err.message);
|
|
217
|
+
}
|
|
218
|
+
saveState(state);
|
|
219
|
+
const language = resolveLanguage(options.language, { invocationText: argsString });
|
|
220
|
+
const sources = inventoryProjectMetadata(root).map((item) => item.path).join(', ') || 'none';
|
|
221
|
+
const scope = language === 'vi'
|
|
222
|
+
? `Phạm vi classify: chỉ đọc manifest đã biết (${sources}) và số lượng tác giả Git; không đọc nội dung lịch sử, không tính tiến cảnh (tiến cảnh cần \`analyze\`).`
|
|
223
|
+
: `classify scope: known manifests only (${sources}) plus a distinct Git-author count; no history content is read, and no progression is computed (progression needs \`analyze\`).`;
|
|
224
|
+
return { code: 0, out: [scope, '', renderClassificationMarkdown(classification, language)] };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (action === 'explain') {
|
|
228
|
+
if (options.metric && EXPLAIN[options.metric]) {
|
|
229
|
+
return { code: 0, out: [say(`Ghi chú từ đạo ký về \`${options.metric}\`: ${EXPLAIN_VI[options.metric]}`, `A note from the Dao ledger — ${options.metric}: ${EXPLAIN[options.metric]}`)] };
|
|
230
|
+
}
|
|
231
|
+
return { code: 0, out: [say(`Đạo ký có thể giải thích: ${Object.keys(EXPLAIN).map((key) => `\`${key}\``).join(', ')}. Hãy truyền \`metric=<tên>\`.`, `The Dao ledger can explain: ${Object.keys(EXPLAIN).join(', ')} (pass metric=<name>).`)] };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (action === 'compare') {
|
|
235
|
+
const files = fs.existsSync(snapDir) ? fs.readdirSync(snapDir).filter((f) => f.endsWith('.json')).sort() : [];
|
|
236
|
+
if (files.length < 2) return refuse(say('Cần ít nhất hai ảnh chụp tổng hợp để so sánh. Hãy chạy `analyze snapshot=true` trước.', 'need at least two snapshots to compare; run analyze snapshot=true first.'));
|
|
237
|
+
const load = (f) => JSON.parse(fs.readFileSync(path.join(snapDir, f), 'utf8'));
|
|
238
|
+
const result = compareSnapshots(load(files[files.length - 2]), load(files[files.length - 1]));
|
|
239
|
+
return {
|
|
240
|
+
code: 0,
|
|
241
|
+
out: [say('Hai bản tu luyện cùng đặt dưới một ngọn đèn; số liệu dưới đây chỉ cho thấy phần đã thay đổi.', 'Two cultivation records rest beneath the same lamp; the figures below show only what changed.'), JSON.stringify(result, null, 2)]
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (action === 'analyze') {
|
|
246
|
+
const token = scopeToken(options.sources, options.range, inventoryProjectMetadata(root));
|
|
247
|
+
if (!options.approve) {
|
|
248
|
+
return refuse(say('Phân tích cần được phê duyệt rõ ràng. Hãy chạy `preview`, rồi truyền lại `approve=<token>` đã nhận.', 'analysis requires explicit approval: run preview, then pass the approve=<token> it printed.'));
|
|
249
|
+
}
|
|
250
|
+
if (options.approve !== token || state.pendingApproval !== token) {
|
|
251
|
+
return refuse(say('Token phê duyệt không khớp phạm vi đã xem trước. Hãy chạy lại `preview` và phê duyệt đúng phạm vi đó.', 'approval token does not match the previewed scope; run preview again and approve that exact scope.'));
|
|
252
|
+
}
|
|
253
|
+
delete state.pendingApproval;
|
|
254
|
+
|
|
255
|
+
const analysis = analyze(classifySources(options.sources));
|
|
256
|
+
let prior = null;
|
|
257
|
+
try {
|
|
258
|
+
const files = fs.readdirSync(snapDir).filter((f) => f.endsWith('.json')).sort();
|
|
259
|
+
if (files.length) prior = JSON.parse(fs.readFileSync(path.join(snapDir, files[files.length - 1]), 'utf8'));
|
|
260
|
+
} catch {}
|
|
261
|
+
|
|
262
|
+
const language = resolveLanguage(options.language, { invocationText: argsString });
|
|
263
|
+
let profile = null;
|
|
264
|
+
try {
|
|
265
|
+
profile = repoProfile(options);
|
|
266
|
+
} catch {}
|
|
267
|
+
const { model, markdown } = renderReport(analysis, {
|
|
268
|
+
...options,
|
|
269
|
+
language,
|
|
270
|
+
profile,
|
|
271
|
+
progressionSalt: path.basename(root),
|
|
272
|
+
priorProgression: prior?.progression ?? null,
|
|
273
|
+
priorVillains: prior?.villainState ?? []
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const out = [];
|
|
277
|
+
if (options.snapshot) {
|
|
278
|
+
fs.mkdirSync(snapDir, { recursive: true });
|
|
279
|
+
const createdAt = new Date().toISOString();
|
|
280
|
+
const snap = buildSnapshot(analysis, {
|
|
281
|
+
projectId: path.basename(root),
|
|
282
|
+
window: options.range,
|
|
283
|
+
createdAt,
|
|
284
|
+
prevVillainState: prior?.villainState ?? [],
|
|
285
|
+
shownVillains: model.villainCards,
|
|
286
|
+
canGamify: !model.suppressGamification,
|
|
287
|
+
policyState: model.policyState,
|
|
288
|
+
progression: model.cultivation?.progression ?? null,
|
|
289
|
+
classification: model.cultivation
|
|
290
|
+
? { faction: model.cultivation.classification.faction.id, paths: model.cultivation.classification.paths.map((p) => p.id) }
|
|
291
|
+
: null
|
|
292
|
+
});
|
|
293
|
+
const file = path.join(snapDir, `${createdAt.replace(/[:.]/g, '-')}.json`);
|
|
294
|
+
fs.writeFileSync(file, `${JSON.stringify(snap, null, 2)}\n`);
|
|
295
|
+
out.push(say(`Đã ghi ảnh chụp tổng hợp: ${path.relative(root, file)}`, `snapshot written: ${path.relative(root, file)}`));
|
|
296
|
+
const prune = snapshotsToPrune(fs.readdirSync(snapDir).filter((f) => f.endsWith('.json')), 20);
|
|
297
|
+
if (prune.length) {
|
|
298
|
+
out.push(say(`Lưu giữ: có thể chuyển các ảnh chụp cũ vào thùng rác bằng lệnh \`trash ${prune.map((f) => path.relative(root, path.join(snapDir, f))).join(' ')}\`.`, `retention: remove old snapshots with: trash ${prune.map((f) => path.relative(root, path.join(snapDir, f))).join(' ')}`));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
let storyContext = null;
|
|
302
|
+
if (storyPreferences.story === 'on') {
|
|
303
|
+
if (model.suppressGamification) {
|
|
304
|
+
out.push(say(`Trường thiên tạm dừng: trạng thái chính sách \`${model.policyState}\` không cho phép viết cốt truyện.`, `living chronicle paused: policy state ${model.policyState} does not permit lore.`));
|
|
305
|
+
} else {
|
|
306
|
+
storyContext = buildStoryContext(model, {
|
|
307
|
+
profile,
|
|
308
|
+
range: options.range,
|
|
309
|
+
language: storyPreferences.storyLanguage,
|
|
310
|
+
invocationText: argsString,
|
|
311
|
+
conversationLanguage: language,
|
|
312
|
+
style: storyPreferences.storyStyle,
|
|
313
|
+
focus: storyPreferences.storyFocus
|
|
314
|
+
});
|
|
315
|
+
const contextFile = writeStoryContext(root, storyContext);
|
|
316
|
+
out.push(say(`Đã ghi ngữ cảnh trường thiên: ${path.relative(root, contextFile)}; bằng chứng \`${storyContext.evidenceKey}\`.`, `story context written: ${path.relative(root, contextFile)} (evidence ${storyContext.evidenceKey})`));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
320
|
+
const ledgerFile = path.join(reportDir, 'latest.md');
|
|
321
|
+
const briefFile = path.join(reportDir, 'latest-brief.json');
|
|
322
|
+
fs.writeFileSync(ledgerFile, `${markdown}\n`);
|
|
323
|
+
const brief = buildResponseBrief(model, {
|
|
324
|
+
language,
|
|
325
|
+
profile,
|
|
326
|
+
storyContext,
|
|
327
|
+
ledgerPath: path.relative(root, ledgerFile)
|
|
328
|
+
});
|
|
329
|
+
fs.writeFileSync(briefFile, `${JSON.stringify(brief, null, 2)}\n`);
|
|
330
|
+
out.push(say(
|
|
331
|
+
`Đã ghi đạo ký chứng cứ: ${path.relative(root, ledgerFile)}.`,
|
|
332
|
+
`evidence ledger written: ${path.relative(root, ledgerFile)}.`
|
|
333
|
+
));
|
|
334
|
+
out.push(say(
|
|
335
|
+
`Gói sáng tác đã sẵn sàng: ${path.relative(root, briefFile)}; hãy dùng dữ kiện này để viết phản hồi linh hoạt theo dự án, không sao chép bố cục đạo ký.`,
|
|
336
|
+
`response brief ready: ${path.relative(root, briefFile)}; compose adaptively from it instead of copying the ledger structure.`
|
|
337
|
+
));
|
|
338
|
+
saveState(state);
|
|
339
|
+
return { code: 0, out: options.output === 'ledger' ? [...out, markdown] : out };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return refuse(say(`Không nhận ra hành động "${action}".`, `unknown action "${action}"`));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
346
|
+
if (isMain) {
|
|
347
|
+
const res = run(process.argv.slice(2).join(' '));
|
|
348
|
+
for (const line of res.out) console.log(line);
|
|
349
|
+
process.exit(res.code);
|
|
350
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Deterministic scoring: analysis JSON -> dimension scores -> realm.
|
|
2
|
+
// No randomness, no wall clock. High token usage never raises the score;
|
|
3
|
+
// necessary iteration is not punished; penalties are capped so one hard task
|
|
4
|
+
// cannot erase a long history.
|
|
5
|
+
|
|
6
|
+
const clamp01 = (x) => Math.max(0, Math.min(1, x));
|
|
7
|
+
const round3 = (x) => Math.round(x * 1000) / 1000;
|
|
8
|
+
|
|
9
|
+
export const DIMENSIONS = [
|
|
10
|
+
{ key: 'delivery', weight: 0.3 },
|
|
11
|
+
{ key: 'validation', weight: 0.25 },
|
|
12
|
+
{ key: 'clarity', weight: 0.15 },
|
|
13
|
+
{ key: 'recovery', weight: 0.15 },
|
|
14
|
+
{ key: 'safety', weight: 0.1 },
|
|
15
|
+
{ key: 'automation', weight: 0.05 }
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const REALMS = [
|
|
19
|
+
{ min: 0, max: 14, name: 'Phàm Nhân Nhập Môn', gloss: 'Mortal Initiate' },
|
|
20
|
+
{ min: 15, max: 29, name: 'Luyện Khí', gloss: 'Qi Refining' },
|
|
21
|
+
{ min: 30, max: 44, name: 'Trúc Cơ', gloss: 'Foundation Establishment' },
|
|
22
|
+
{ min: 45, max: 59, name: 'Kết Đan', gloss: 'Core Formation' },
|
|
23
|
+
{ min: 60, max: 69, name: 'Nguyên Anh', gloss: 'Nascent Soul' },
|
|
24
|
+
{ min: 70, max: 79, name: 'Hóa Thần', gloss: 'Spirit Transformation' },
|
|
25
|
+
{ min: 80, max: 87, name: 'Luyện Hư', gloss: 'Void Refinement' },
|
|
26
|
+
{ min: 88, max: 93, name: 'Hợp Thể', gloss: 'Body Integration' },
|
|
27
|
+
{ min: 94, max: 97, name: 'Đại Thừa', gloss: 'Great Vehicle' },
|
|
28
|
+
{ min: 98, max: 100, name: 'Độ Kiếp / Phi Thăng', gloss: 'Tribulation / Ascension' }
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Fraction of assistant turns with known (reported or estimated) usage.
|
|
32
|
+
// Below 0.60 the run does not have enough evidence ("thiên cơ") to score.
|
|
33
|
+
export function coverageFraction(analysis) {
|
|
34
|
+
const tc = analysis.coverage?.tokenCoverage ?? {};
|
|
35
|
+
return clamp01((tc.reportedTurnsPct ?? 0) + (tc.estimatedTurnsPct ?? 0));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function scoreDimensions(analysis) {
|
|
39
|
+
const issues = analysis.issues ?? {};
|
|
40
|
+
const passes = issues.passes ?? 0;
|
|
41
|
+
const failures = (issues.failures ?? []).length;
|
|
42
|
+
const recoveries = (issues.recoveries ?? []).length;
|
|
43
|
+
const commits = analysis.coverage?.commits ?? 0;
|
|
44
|
+
const userPrompts = analysis.coverage?.userPrompts ?? 0;
|
|
45
|
+
const validationEvents = passes + failures;
|
|
46
|
+
const rep = analysis.repetition ?? {};
|
|
47
|
+
const retryPrompts = (rep.retryLoopCandidates ?? []).reduce((n, l) => n + l.count, 0);
|
|
48
|
+
const conflicts = (analysis.conflicts ?? []).length;
|
|
49
|
+
|
|
50
|
+
// delivery: verified completion vs failed attempts, plus committed output.
|
|
51
|
+
const delivery = validationEvents + commits === 0
|
|
52
|
+
? 0.3
|
|
53
|
+
: clamp01((passes + recoveries + Math.min(commits, 5) * 0.3) / (passes + failures + 1));
|
|
54
|
+
|
|
55
|
+
// validation discipline: presence and density of validation evidence.
|
|
56
|
+
const validation = validationEvents === 0
|
|
57
|
+
? 0.2
|
|
58
|
+
: clamp01(0.55 + 0.1 * recoveries + 0.05 * passes);
|
|
59
|
+
|
|
60
|
+
// clarity: 1 - avoidable-repeat rate (capped penalty).
|
|
61
|
+
const clarity = clamp01(1 - Math.min(0.6, userPrompts ? retryPrompts / userPrompts : 0));
|
|
62
|
+
|
|
63
|
+
// recovery from failure.
|
|
64
|
+
const recovery = failures === 0 ? 0.7 : clamp01(recoveries / failures);
|
|
65
|
+
|
|
66
|
+
// safety / rule alignment: high baseline, capped penalty per conflict.
|
|
67
|
+
const safety = clamp01(0.9 - Math.min(0.4, conflicts * 0.15));
|
|
68
|
+
|
|
69
|
+
// reusable automation: not inferable from history alone in V1 (neutral).
|
|
70
|
+
const automation = 0.4;
|
|
71
|
+
|
|
72
|
+
const scores = { delivery, validation, clarity, recovery, safety, automation };
|
|
73
|
+
for (const k of Object.keys(scores)) scores[k] = round3(scores[k]);
|
|
74
|
+
return scores;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function realmFor(score) {
|
|
78
|
+
return REALMS.find((r) => score >= r.min && score <= r.max) ?? REALMS[0];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function scoreReport(analysis, { coverageGate = 0.6 } = {}) {
|
|
82
|
+
const coverage = round3(coverageFraction(analysis));
|
|
83
|
+
const dimensions = scoreDimensions(analysis);
|
|
84
|
+
if (coverage < coverageGate) {
|
|
85
|
+
return {
|
|
86
|
+
enoughEvidence: false,
|
|
87
|
+
coverage,
|
|
88
|
+
dimensions,
|
|
89
|
+
score: null,
|
|
90
|
+
realm: { name: 'Chưa đủ thiên cơ', gloss: 'Not enough evidence yet' },
|
|
91
|
+
missing: ['token usage coverage below 60%; supply provider usage metadata to compute a realm']
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const weighted = DIMENSIONS.reduce((sum, d) => sum + dimensions[d.key] * d.weight, 0);
|
|
95
|
+
const score = Math.round(weighted * 100);
|
|
96
|
+
return { enoughEvidence: true, coverage, dimensions, score, realm: realmFor(score), missing: [] };
|
|
97
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Phase 4 persistence. A snapshot is an AGGREGATE only: metric counts,
|
|
2
|
+
// coverage, token totals, score/realm, thresholds, adapter versions, villain
|
|
3
|
+
// cooldown state, and SALTED event-id digests. It never stores raw prompts,
|
|
4
|
+
// URLs, secrets, or file contents. Snapshots live under the git-ignored
|
|
5
|
+
// .vibekit/reports/tutien/ tree; deletion follows the repo safe-delete policy
|
|
6
|
+
// (prefer `trash`, never silent `rm`).
|
|
7
|
+
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import { scoreReport } from './score.mjs';
|
|
10
|
+
import { detectProblems } from './catalog.mjs';
|
|
11
|
+
import { advanceVillainState } from './villains.mjs';
|
|
12
|
+
|
|
13
|
+
export const SNAPSHOT_SCHEMA = 1;
|
|
14
|
+
const PEPPER = 'tutien-snapshot-v1';
|
|
15
|
+
|
|
16
|
+
const sha16 = (s) => crypto.createHash('sha256').update(s).digest('hex').slice(0, 16);
|
|
17
|
+
// Salt is stable per project (so trends line up) but not reversible to raw ids.
|
|
18
|
+
const saltFor = (projectId) => sha16(`${PEPPER}:${projectId}`);
|
|
19
|
+
|
|
20
|
+
export function buildSnapshot(analysis, options = {}) {
|
|
21
|
+
const { projectId = 'repo', window = 'all', createdAt = null, thresholds = {}, adapterVersions = {}, prevVillainState = [] } = options;
|
|
22
|
+
const salt = saltFor(projectId);
|
|
23
|
+
const score = scoreReport(analysis);
|
|
24
|
+
const problems = detectProblems(analysis);
|
|
25
|
+
|
|
26
|
+
// Provenance without content: only salted digests of evidence event ids.
|
|
27
|
+
const provenance = [
|
|
28
|
+
...new Set(problems.flatMap((p) => p.evidence.eventIds ?? []))
|
|
29
|
+
].sort().map((id) => sha16(`${salt}:${id}`));
|
|
30
|
+
|
|
31
|
+
const rep = analysis.repetition ?? {};
|
|
32
|
+
const issues = analysis.issues ?? {};
|
|
33
|
+
|
|
34
|
+
const shown = options.shownVillains ?? [];
|
|
35
|
+
// A suppressed policy (declared-stop / needs-review / authorization-required)
|
|
36
|
+
// must not persist a score, realm, or dimensions — the same fail-closed
|
|
37
|
+
// projection the renderer uses. Only Nghiệp Lực survives in progression.
|
|
38
|
+
const suppressed = options.canGamify === false;
|
|
39
|
+
return {
|
|
40
|
+
snapshotSchema: SNAPSHOT_SCHEMA,
|
|
41
|
+
createdAt,
|
|
42
|
+
projectId,
|
|
43
|
+
window,
|
|
44
|
+
coverage: analysis.coverage ?? {},
|
|
45
|
+
tokens: analysis.tokens ?? {},
|
|
46
|
+
counts: {
|
|
47
|
+
exactRepeats: (rep.exactRepeats ?? []).length,
|
|
48
|
+
nearRepeats: (rep.nearRepeats ?? []).length,
|
|
49
|
+
retryLoops: (rep.retryLoopCandidates ?? []).length,
|
|
50
|
+
conflicts: (analysis.conflicts ?? []).length,
|
|
51
|
+
failures: (issues.failures ?? []).length,
|
|
52
|
+
recoveries: (issues.recoveries ?? []).length,
|
|
53
|
+
passes: issues.passes ?? 0
|
|
54
|
+
},
|
|
55
|
+
score: suppressed ? null : score.score,
|
|
56
|
+
realm: suppressed ? null : score.realm,
|
|
57
|
+
dimensions: suppressed ? null : score.dimensions,
|
|
58
|
+
thresholds,
|
|
59
|
+
adapterVersions,
|
|
60
|
+
villainState: advanceVillainState(prevVillainState, shown),
|
|
61
|
+
// Cultivation accumulators and classification are aggregate slugs only:
|
|
62
|
+
// metric counters, the salted seen-ledger, and faction/path ids — never
|
|
63
|
+
// rationale text, raw prompts, or authorization values.
|
|
64
|
+
progression: options.progression ?? null,
|
|
65
|
+
classification: options.classification ?? null,
|
|
66
|
+
policyState: options.policyState ?? null,
|
|
67
|
+
provenance
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Retention is a pure decision; the caller performs the actual removal with
|
|
72
|
+
// `trash` per safe-delete rules. Returns the oldest files beyond `keep`.
|
|
73
|
+
export function snapshotsToPrune(files, keep = 20) {
|
|
74
|
+
const sorted = [...files].sort();
|
|
75
|
+
return sorted.slice(0, Math.max(0, sorted.length - keep));
|
|
76
|
+
}
|