minimal-vibe-coding-kit 0.5.0 → 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/tutien/SKILL.md +36 -13
- package/.agents/skills/tutien/references/adaptive-response.md +68 -0
- package/.agents/skills/tutien/references/lore-sources.md +28 -0
- package/.agents/skills/tutien/references/privacy.md +13 -1
- package/.agents/skills/tutien/references/schema.md +32 -1
- 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 +23 -8
- package/.agents/skills/tutien/scripts/catalog.mjs +17 -17
- package/.agents/skills/tutien/scripts/classify.mjs +64 -37
- package/.agents/skills/tutien/scripts/command.mjs +40 -21
- package/.agents/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.agents/skills/tutien/scripts/render-report.mjs +128 -72
- package/.agents/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.agents/skills/tutien/scripts/run-tutien.mjs +141 -63
- package/.agents/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.agents/skills/tutien/scripts/villains.mjs +14 -14
- package/.claude/skills/tutien/SKILL.md +36 -13
- package/.claude/skills/tutien/references/adaptive-response.md +68 -0
- package/.claude/skills/tutien/references/lore-sources.md +28 -0
- package/.claude/skills/tutien/references/privacy.md +13 -1
- package/.claude/skills/tutien/references/schema.md +32 -1
- 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 +23 -8
- package/.claude/skills/tutien/scripts/catalog.mjs +17 -17
- package/.claude/skills/tutien/scripts/classify.mjs +64 -37
- package/.claude/skills/tutien/scripts/command.mjs +40 -21
- package/.claude/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.claude/skills/tutien/scripts/render-report.mjs +128 -72
- package/.claude/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.claude/skills/tutien/scripts/run-tutien.mjs +141 -63
- package/.claude/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.claude/skills/tutien/scripts/villains.mjs +14 -14
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/skills/tutien/SKILL.md +36 -13
- package/.cursor/skills/tutien/references/adaptive-response.md +68 -0
- package/.cursor/skills/tutien/references/lore-sources.md +28 -0
- package/.cursor/skills/tutien/references/privacy.md +13 -1
- package/.cursor/skills/tutien/references/schema.md +32 -1
- 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 +23 -8
- package/.cursor/skills/tutien/scripts/catalog.mjs +17 -17
- package/.cursor/skills/tutien/scripts/classify.mjs +64 -37
- package/.cursor/skills/tutien/scripts/command.mjs +40 -21
- package/.cursor/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.cursor/skills/tutien/scripts/render-report.mjs +128 -72
- package/.cursor/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.cursor/skills/tutien/scripts/run-tutien.mjs +141 -63
- package/.cursor/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.cursor/skills/tutien/scripts/villains.mjs +14 -14
- package/.grok/skills/tutien/SKILL.md +36 -13
- package/.grok/skills/tutien/references/adaptive-response.md +68 -0
- package/.grok/skills/tutien/references/lore-sources.md +28 -0
- package/.grok/skills/tutien/references/privacy.md +13 -1
- package/.grok/skills/tutien/references/schema.md +32 -1
- 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 +23 -8
- package/.grok/skills/tutien/scripts/catalog.mjs +17 -17
- package/.grok/skills/tutien/scripts/classify.mjs +64 -37
- package/.grok/skills/tutien/scripts/command.mjs +40 -21
- package/.grok/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.grok/skills/tutien/scripts/render-report.mjs +128 -72
- package/.grok/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.grok/skills/tutien/scripts/run-tutien.mjs +141 -63
- package/.grok/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.grok/skills/tutien/scripts/villains.mjs +14 -14
- package/.vibekit/skills/tutien/SKILL.md +36 -13
- package/.vibekit/skills/tutien/references/adaptive-response.md +68 -0
- package/.vibekit/skills/tutien/references/lore-sources.md +28 -0
- package/.vibekit/skills/tutien/references/privacy.md +13 -1
- package/.vibekit/skills/tutien/references/schema.md +32 -1
- 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 +23 -8
- package/.vibekit/skills/tutien/scripts/catalog.mjs +17 -17
- package/.vibekit/skills/tutien/scripts/classify.mjs +64 -37
- package/.vibekit/skills/tutien/scripts/command.mjs +40 -21
- package/.vibekit/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.vibekit/skills/tutien/scripts/render-report.mjs +128 -72
- package/.vibekit/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.vibekit/skills/tutien/scripts/run-tutien.mjs +141 -63
- package/.vibekit/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.vibekit/skills/tutien/scripts/villains.mjs +14 -14
- package/CHANGELOG.md +22 -1
- package/README.md +5 -3
- package/docs/README.vi.md +5 -3
- package/docs/README.zh-CN.md +5 -3
- package/package.json +2 -2
|
@@ -5,12 +5,21 @@ import process from 'node:process';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
normalizeStoryFocus,
|
|
10
|
+
normalizeStoryLanguage,
|
|
11
|
+
normalizeStoryStyle,
|
|
12
|
+
parseInvocation,
|
|
13
|
+
TUTIEN_EXPERIENCE
|
|
14
|
+
} from './command.mjs';
|
|
9
15
|
import { analyze } from './analyze-history.mjs';
|
|
10
16
|
import { renderReport, resolveLanguage, renderClassificationMarkdown } from './render-report.mjs';
|
|
11
17
|
import { buildSnapshot, snapshotsToPrune } from './snapshot.mjs';
|
|
12
18
|
import { compareSnapshots } from './compare.mjs';
|
|
13
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';
|
|
14
23
|
|
|
15
24
|
// End-to-end executor for the advertised /tutien actions. All state lives
|
|
16
25
|
// under the git-ignored .vibekit/reports/tutien/ of the current repo; the
|
|
@@ -41,6 +50,20 @@ function saveState(state) {
|
|
|
41
50
|
fs.writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`);
|
|
42
51
|
}
|
|
43
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
|
+
|
|
44
67
|
function classifySources(sources) {
|
|
45
68
|
const spec = { jsonlFiles: [], transcriptFiles: [], gitRoot: null };
|
|
46
69
|
for (const s of sources) {
|
|
@@ -51,61 +74,60 @@ function classifySources(sources) {
|
|
|
51
74
|
return spec;
|
|
52
75
|
}
|
|
53
76
|
|
|
54
|
-
const scopeToken = (sources, range) => sha16(JSON.stringify({
|
|
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
|
+
}));
|
|
55
82
|
|
|
56
|
-
// Project profile for classification:
|
|
57
|
-
//
|
|
58
|
-
//
|
|
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.
|
|
59
86
|
function repoProfile(options) {
|
|
60
|
-
|
|
61
|
-
let primaryLanguage = null;
|
|
62
|
-
let projectType = null;
|
|
63
|
-
try {
|
|
64
|
-
const text = fs.readFileSync(path.join(root, 'backbone.yml'), 'utf8');
|
|
65
|
-
const grab = (re) => {
|
|
66
|
-
const m = text.match(re);
|
|
67
|
-
return m ? m[1].trim().replace(/^["']|["']$/g, '') : null;
|
|
68
|
-
};
|
|
69
|
-
description = grab(/^\s*description:\s*(.+)$/m);
|
|
70
|
-
primaryLanguage = grab(/^\s*primary_language:\s*(.+)$/m);
|
|
71
|
-
projectType = grab(/^\s*type:\s*(.+)$/m);
|
|
72
|
-
} catch {}
|
|
87
|
+
const profile = buildProjectProfile(root, options);
|
|
73
88
|
// Author identifiers are read transiently (mailmap-collapsed) and reduced to
|
|
74
89
|
// a distinct COUNT immediately; no email or name is retained or returned.
|
|
75
90
|
let authorsCount = null;
|
|
76
91
|
const res = spawnSync('git', ['log', '--use-mailmap', '--format=%aE'], { cwd: root, encoding: 'utf8' });
|
|
77
92
|
if (res.status === 0) authorsCount = new Set(res.stdout.split('\n').filter(Boolean)).size;
|
|
78
|
-
return {
|
|
79
|
-
description,
|
|
80
|
-
primaryLanguage,
|
|
81
|
-
projectType,
|
|
82
|
-
domains: options.domains ?? [],
|
|
83
|
-
authorization: options.authorization ?? null,
|
|
84
|
-
authorsCount,
|
|
85
|
-
declared: { faction: options.faction, affiliation: options.affiliation, paths: options.paths }
|
|
86
|
-
};
|
|
93
|
+
return { ...profile, authorsCount };
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
// Preview inspects no content: file existence and size only, plus the git
|
|
90
97
|
// toplevel path. Nothing is parsed and nothing is written except the pending
|
|
91
98
|
// approval in the state file.
|
|
92
|
-
function previewLines(options) {
|
|
93
|
-
const
|
|
94
|
-
|
|
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)');
|
|
95
106
|
for (const s of options.sources) {
|
|
96
107
|
if (s === 'git') {
|
|
97
|
-
lines.push(`- git: commit metadata of ${root} (read-only git log, current repository only)`);
|
|
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)`);
|
|
98
109
|
} else {
|
|
99
110
|
let size = null;
|
|
100
111
|
try {
|
|
101
112
|
size = fs.statSync(s).size;
|
|
102
113
|
} catch {}
|
|
103
|
-
lines.push(size == null
|
|
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)`));
|
|
104
117
|
}
|
|
105
118
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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(',')}`);
|
|
109
131
|
return { lines, token };
|
|
110
132
|
}
|
|
111
133
|
|
|
@@ -118,33 +140,50 @@ const EXPLAIN = {
|
|
|
118
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ơ".'
|
|
119
141
|
};
|
|
120
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
|
+
|
|
121
152
|
export function run(argsString = '') {
|
|
122
|
-
const { action, explicitAction, options } = parseInvocation(argsString);
|
|
153
|
+
const { action, explicitAction, options, providedOptions } = parseInvocation(argsString);
|
|
123
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;
|
|
124
158
|
const refuse = (msg) => ({ code: 2, out: [msg] });
|
|
125
159
|
|
|
126
160
|
if (action === 'off') {
|
|
127
161
|
state.mode = 'off';
|
|
128
162
|
delete state.pendingApproval;
|
|
129
163
|
saveState(state);
|
|
130
|
-
return { code: 0, out: ['Tutien mode is off. Normal kit writing style restored.'] };
|
|
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.')] };
|
|
131
165
|
}
|
|
132
166
|
if (action === 'on') {
|
|
133
167
|
state.mode = 'on';
|
|
134
168
|
saveState(state);
|
|
135
|
-
return { code: 0, out: ['Tutien mode is on — the mountain gate is open. Run preview to see the exact scope before any analysis.'] };
|
|
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.')] };
|
|
136
170
|
}
|
|
137
171
|
if (action === 'status') {
|
|
138
172
|
const snaps = fs.existsSync(snapDir) ? fs.readdirSync(snapDir).length : 0;
|
|
173
|
+
const storyStatus = validateStoryTree(root);
|
|
139
174
|
return {
|
|
140
175
|
code: 0,
|
|
141
176
|
out: [
|
|
142
|
-
`mode: ${state.mode ?? 'off'}
|
|
143
|
-
`experience: ${TUTIEN_EXPERIENCE.kind}
|
|
144
|
-
`voice: ${TUTIEN_EXPERIENCE.narrativeStyle}
|
|
145
|
-
`semantic namespace: ${TUTIEN_EXPERIENCE.semanticNamespace}
|
|
146
|
-
`pending approval: ${state.pendingApproval ?? 'none'}
|
|
147
|
-
`snapshots: ${snaps}`
|
|
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})`}`)
|
|
148
187
|
]
|
|
149
188
|
};
|
|
150
189
|
}
|
|
@@ -152,21 +191,24 @@ export function run(argsString = '') {
|
|
|
152
191
|
// Report actions. A bare `/tutien` re-activates the mode; an explicit
|
|
153
192
|
// report action while off is refused (off-mode suppression).
|
|
154
193
|
if ((state.mode ?? 'off') !== 'on') {
|
|
155
|
-
if (explicitAction) return refuse("tutien mode is off; run 'on' (or bare /tutien) first.");
|
|
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."));
|
|
156
195
|
state.mode = 'on';
|
|
157
196
|
}
|
|
158
197
|
|
|
159
198
|
if (action === 'preview') {
|
|
160
|
-
const { lines, token } = previewLines(options);
|
|
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
|
+
));
|
|
161
204
|
state.pendingApproval = token;
|
|
162
205
|
saveState(state);
|
|
163
206
|
return { code: 0, out: lines };
|
|
164
207
|
}
|
|
165
208
|
|
|
166
209
|
if (action === 'classify') {
|
|
167
|
-
// classify is a metadata-only action (no history content): it reads
|
|
168
|
-
//
|
|
169
|
-
// repo. It states that scope before producing the classification.
|
|
210
|
+
// classify is a metadata-only action (no history content): it reads only
|
|
211
|
+
// the known manifests advertised below and a transient author count.
|
|
170
212
|
let classification;
|
|
171
213
|
try {
|
|
172
214
|
classification = classifyProject(repoProfile(options));
|
|
@@ -175,37 +217,38 @@ export function run(argsString = '') {
|
|
|
175
217
|
}
|
|
176
218
|
saveState(state);
|
|
177
219
|
const language = resolveLanguage(options.language, { invocationText: argsString });
|
|
220
|
+
const sources = inventoryProjectMetadata(root).map((item) => item.path).join(', ') || 'none';
|
|
178
221
|
const scope = language === 'vi'
|
|
179
|
-
?
|
|
180
|
-
:
|
|
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\`).`;
|
|
181
224
|
return { code: 0, out: [scope, '', renderClassificationMarkdown(classification, language)] };
|
|
182
225
|
}
|
|
183
226
|
|
|
184
227
|
if (action === 'explain') {
|
|
185
228
|
if (options.metric && EXPLAIN[options.metric]) {
|
|
186
|
-
return { code: 0, out: [`A note from the Dao ledger — ${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]}`)] };
|
|
187
230
|
}
|
|
188
|
-
return { code: 0, out: [`The Dao ledger can explain: ${Object.keys(EXPLAIN).join(', ')} (pass metric=<name>).`] };
|
|
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>).`)] };
|
|
189
232
|
}
|
|
190
233
|
|
|
191
234
|
if (action === 'compare') {
|
|
192
235
|
const files = fs.existsSync(snapDir) ? fs.readdirSync(snapDir).filter((f) => f.endsWith('.json')).sort() : [];
|
|
193
|
-
if (files.length < 2) return refuse('need at least two snapshots to compare; run analyze snapshot=true first.');
|
|
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.'));
|
|
194
237
|
const load = (f) => JSON.parse(fs.readFileSync(path.join(snapDir, f), 'utf8'));
|
|
195
238
|
const result = compareSnapshots(load(files[files.length - 2]), load(files[files.length - 1]));
|
|
196
239
|
return {
|
|
197
240
|
code: 0,
|
|
198
|
-
out: ['Two cultivation records rest beneath the same lamp; the figures below show only what changed.', JSON.stringify(result, null, 2)]
|
|
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)]
|
|
199
242
|
};
|
|
200
243
|
}
|
|
201
244
|
|
|
202
245
|
if (action === 'analyze') {
|
|
203
|
-
const token = scopeToken(options.sources, options.range);
|
|
246
|
+
const token = scopeToken(options.sources, options.range, inventoryProjectMetadata(root));
|
|
204
247
|
if (!options.approve) {
|
|
205
|
-
return refuse('analysis requires explicit approval: run preview, then pass the approve=<token> it printed.');
|
|
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.'));
|
|
206
249
|
}
|
|
207
250
|
if (options.approve !== token || state.pendingApproval !== token) {
|
|
208
|
-
return refuse('approval token does not match the previewed scope; run preview again and approve that exact scope.');
|
|
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.'));
|
|
209
252
|
}
|
|
210
253
|
delete state.pendingApproval;
|
|
211
254
|
|
|
@@ -249,19 +292,54 @@ export function run(argsString = '') {
|
|
|
249
292
|
});
|
|
250
293
|
const file = path.join(snapDir, `${createdAt.replace(/[:.]/g, '-')}.json`);
|
|
251
294
|
fs.writeFileSync(file, `${JSON.stringify(snap, null, 2)}\n`);
|
|
252
|
-
out.push(`snapshot written: ${path.relative(root, file)}`);
|
|
295
|
+
out.push(say(`Đã ghi ảnh chụp tổng hợp: ${path.relative(root, file)}`, `snapshot written: ${path.relative(root, file)}`));
|
|
253
296
|
const prune = snapshotsToPrune(fs.readdirSync(snapDir).filter((f) => f.endsWith('.json')), 20);
|
|
254
297
|
if (prune.length) {
|
|
255
|
-
out.push(`retention: remove old snapshots with: trash ${prune.map((f) => path.relative(root, path.join(snapDir, f))).join(' ')}`);
|
|
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})`));
|
|
256
317
|
}
|
|
257
318
|
}
|
|
258
319
|
fs.mkdirSync(reportDir, { recursive: true });
|
|
259
|
-
|
|
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
|
+
));
|
|
260
338
|
saveState(state);
|
|
261
|
-
return { code: 0, out: [...out, markdown] };
|
|
339
|
+
return { code: 0, out: options.output === 'ledger' ? [...out, markdown] : out };
|
|
262
340
|
}
|
|
263
341
|
|
|
264
|
-
return refuse(`unknown action "${action}"`);
|
|
342
|
+
return refuse(say(`Không nhận ra hành động "${action}".`, `unknown action "${action}"`));
|
|
265
343
|
}
|
|
266
344
|
|
|
267
345
|
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Deterministic storage helpers for the agent-authored living chronicle.
|
|
3
|
+
// This module never generates story prose. It only builds an aggregate-only
|
|
4
|
+
// evidence packet and validates the plot/chapter ledger written by the agent.
|
|
5
|
+
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
export const STORY_SCHEMA = 1;
|
|
13
|
+
export const STORY_CONTEXT_SCHEMA = 'tutien-story-context-v1';
|
|
14
|
+
export const STORY_STATE_SCHEMA = 'tutien-story-state-v1';
|
|
15
|
+
export const STORY_CHAPTER_SCHEMA = 'tutien-story-chapter-v1';
|
|
16
|
+
|
|
17
|
+
const sha16 = (value) => crypto.createHash('sha256').update(String(value)).digest('hex').slice(0, 16);
|
|
18
|
+
|
|
19
|
+
export function storyPaths(root = process.cwd()) {
|
|
20
|
+
const base = path.join(path.resolve(root), '.vibekit', 'reports', 'tutien', 'story');
|
|
21
|
+
return {
|
|
22
|
+
base,
|
|
23
|
+
plot: path.join(base, 'plot.md'),
|
|
24
|
+
state: path.join(base, 'story-state.json'),
|
|
25
|
+
context: path.join(base, 'latest-context.json'),
|
|
26
|
+
chapters: path.join(base, 'chapters')
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Preserve letters from every language, including Han characters. Latin
|
|
31
|
+
// scripts are normalized so Vietnamese titles remain readable in filenames.
|
|
32
|
+
export function slugifyChapterTitle(title) {
|
|
33
|
+
const raw = String(title ?? '').trim();
|
|
34
|
+
if (!raw) throw new Error('chapter title is required');
|
|
35
|
+
const slug = raw
|
|
36
|
+
.normalize('NFD')
|
|
37
|
+
.replace(/\p{M}+/gu, '')
|
|
38
|
+
.toLocaleLowerCase('und')
|
|
39
|
+
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
|
40
|
+
.replace(/^-+|-+$/g, '')
|
|
41
|
+
.slice(0, 80)
|
|
42
|
+
.replace(/-+$/g, '');
|
|
43
|
+
if (!slug || slug === '.' || slug === '..') throw new Error('chapter title does not produce a safe filename');
|
|
44
|
+
return slug;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function chapterFilename(number, title) {
|
|
48
|
+
if (!Number.isInteger(number) || number < 1 || number > 9999) {
|
|
49
|
+
throw new Error('chapter number must be an integer from 1 to 9999');
|
|
50
|
+
}
|
|
51
|
+
return `${String(number).padStart(4, '0')}-${slugifyChapterTitle(title)}.md`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function listChapters(root = process.cwd()) {
|
|
55
|
+
const dir = storyPaths(root).chapters;
|
|
56
|
+
if (!fs.existsSync(dir)) return [];
|
|
57
|
+
return fs.readdirSync(dir)
|
|
58
|
+
.filter((file) => /^\d{4}-[\p{L}\p{N}][\p{L}\p{N}-]*\.md$/u.test(file))
|
|
59
|
+
.map((file) => ({ file, number: Number(file.slice(0, 4)), path: path.join(dir, file) }))
|
|
60
|
+
.sort((a, b) => a.number - b.number || a.file.localeCompare(b.file));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function nextChapterNumber(root = process.cwd()) {
|
|
64
|
+
const chapters = listChapters(root);
|
|
65
|
+
return chapters.length ? chapters[chapters.length - 1].number + 1 : 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolveStoryLanguage(requested = 'auto', invocationText = '', fallback = 'en') {
|
|
69
|
+
const explicit = String(requested).toLowerCase();
|
|
70
|
+
if (['vi', 'en', 'zh'].includes(explicit)) return explicit;
|
|
71
|
+
if (/\p{Script=Han}/u.test(invocationText)) return 'zh';
|
|
72
|
+
if (/[À-ỹĐđ]/u.test(invocationText) || /\b(hãy|giúp|của|và|không|chương|tu tiên)\b/iu.test(invocationText)) return 'vi';
|
|
73
|
+
return ['vi', 'en', 'zh'].includes(fallback) ? fallback : 'en';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function safeSlug(value, fallback = null) {
|
|
77
|
+
const text = String(value ?? '');
|
|
78
|
+
return /^[a-z0-9][a-z0-9_-]{0,63}$/i.test(text) ? text : fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function safeValidationCommand(value) {
|
|
82
|
+
const text = String(value ?? '');
|
|
83
|
+
return /^(?:npm run|yarn|pnpm|bun run) [A-Za-z0-9][A-Za-z0-9:_-]{0,63}$/.test(text) ? text : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function storyFacts(model, profile = {}) {
|
|
87
|
+
const cultivation = model.cultivation ?? null;
|
|
88
|
+
const progression = cultivation?.progression ?? null;
|
|
89
|
+
return {
|
|
90
|
+
project: {
|
|
91
|
+
id: safeSlug(profile.projectId, 'repo'),
|
|
92
|
+
type: safeSlug(profile.projectType),
|
|
93
|
+
primaryLanguage: safeSlug(profile.primaryLanguage),
|
|
94
|
+
domains: (profile.domains ?? []).map((v) => safeSlug(v)).filter(Boolean).slice(0, 12),
|
|
95
|
+
stack: (profile.stack ?? []).map((v) => safeSlug(v)).filter(Boolean).slice(0, 12),
|
|
96
|
+
validationCommands: (profile.validationCommands ?? []).map((v) => safeValidationCommand(v)).filter(Boolean).slice(0, 3),
|
|
97
|
+
metadataSources: (profile.metadataSources ?? []).map((v) => safeSlug(String(v).replace(/\.[^.]+$/, ''))).filter(Boolean).slice(0, 12),
|
|
98
|
+
authorsCount: Number.isInteger(profile.authorsCount) && profile.authorsCount >= 0 ? profile.authorsCount : null
|
|
99
|
+
},
|
|
100
|
+
evidence: {
|
|
101
|
+
window: model.coverage?.window ?? { start: null, end: null },
|
|
102
|
+
sessions: model.coverage?.sessions ?? 0,
|
|
103
|
+
prompts: model.coverage?.userPrompts ?? 0,
|
|
104
|
+
commits: model.coverage?.commits ?? 0,
|
|
105
|
+
confidence: model.coverage?.confidence ?? 'low',
|
|
106
|
+
tokens: {
|
|
107
|
+
reported: model.tokens?.reportedTotal ?? 0,
|
|
108
|
+
estimated: model.tokens?.estimatedTotal ?? 0,
|
|
109
|
+
unknownTurns: model.tokens?.unknownTurns ?? 0
|
|
110
|
+
},
|
|
111
|
+
realm: model.realm ? { name: model.realm.name, score: model.score } : null,
|
|
112
|
+
problems: (model.problems ?? []).map((p) => ({
|
|
113
|
+
type: p.problemType,
|
|
114
|
+
confidence: p.confidence,
|
|
115
|
+
priority: p.priority
|
|
116
|
+
})),
|
|
117
|
+
classification: cultivation ? {
|
|
118
|
+
faction: cultivation.classification?.faction?.id ?? null,
|
|
119
|
+
affiliation: cultivation.classification?.affiliation?.id ?? null,
|
|
120
|
+
paths: (cultivation.classification?.paths ?? []).map((p) => p.id)
|
|
121
|
+
} : null,
|
|
122
|
+
progression: progression ? {
|
|
123
|
+
tuVi: progression.tuVi?.window ?? 0,
|
|
124
|
+
congDuc: progression.congDuc?.window ?? 0,
|
|
125
|
+
nghiepLuc: progression.nghiepLuc?.window ?? 0,
|
|
126
|
+
daoHanhWindows: progression.daoHanh?.windows ?? 0,
|
|
127
|
+
overlap: progression.overlap ?? 0
|
|
128
|
+
} : null
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function buildStoryContext(model, options = {}) {
|
|
134
|
+
const facts = storyFacts(model, options.profile ?? {});
|
|
135
|
+
const evidenceKey = sha16(JSON.stringify({
|
|
136
|
+
storySchema: STORY_SCHEMA,
|
|
137
|
+
project: facts.project.id,
|
|
138
|
+
range: options.range ?? 'all',
|
|
139
|
+
evidence: facts.evidence
|
|
140
|
+
}));
|
|
141
|
+
return {
|
|
142
|
+
schema: STORY_CONTEXT_SCHEMA,
|
|
143
|
+
storySchema: STORY_SCHEMA,
|
|
144
|
+
evidenceKey,
|
|
145
|
+
language: resolveStoryLanguage(options.language, options.invocationText, options.conversationLanguage),
|
|
146
|
+
style: safeSlug(options.style, 'auto'),
|
|
147
|
+
focus: safeSlug(options.focus, 'balanced'),
|
|
148
|
+
policyState: model.policyState ?? 'clear',
|
|
149
|
+
canWriteChapter: model.suppressGamification !== true,
|
|
150
|
+
...facts
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function writeStoryContext(root, context) {
|
|
155
|
+
if (context?.schema !== STORY_CONTEXT_SCHEMA) throw new Error(`story context must use ${STORY_CONTEXT_SCHEMA}`);
|
|
156
|
+
const paths = storyPaths(root);
|
|
157
|
+
fs.mkdirSync(paths.base, { recursive: true });
|
|
158
|
+
fs.writeFileSync(paths.context, `${JSON.stringify(context, null, 2)}\n`);
|
|
159
|
+
return paths.context;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function chapterFrontmatter(text) {
|
|
163
|
+
const match = String(text).match(/^---\n([\s\S]*?)\n---\n/);
|
|
164
|
+
if (!match) return null;
|
|
165
|
+
const get = (key) => match[1].match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))?.[1]?.trim().replace(/^['"]|['"]$/g, '') ?? null;
|
|
166
|
+
return {
|
|
167
|
+
schema: get('schema'),
|
|
168
|
+
chapter: Number(get('chapter')),
|
|
169
|
+
title: get('title'),
|
|
170
|
+
evidenceKey: get('evidence_key')
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function validateStoryTree(root = process.cwd()) {
|
|
175
|
+
const paths = storyPaths(root);
|
|
176
|
+
const chapters = listChapters(root);
|
|
177
|
+
const errors = [];
|
|
178
|
+
if (chapters.length && !fs.existsSync(paths.plot)) errors.push('plot.md is required when chapters exist');
|
|
179
|
+
|
|
180
|
+
let state = null;
|
|
181
|
+
if (fs.existsSync(paths.state)) {
|
|
182
|
+
try {
|
|
183
|
+
state = JSON.parse(fs.readFileSync(paths.state, 'utf8'));
|
|
184
|
+
if (state.schema !== STORY_STATE_SCHEMA) errors.push(`story-state.json must use ${STORY_STATE_SCHEMA}`);
|
|
185
|
+
} catch {
|
|
186
|
+
errors.push('story-state.json must contain valid JSON');
|
|
187
|
+
}
|
|
188
|
+
} else if (chapters.length) {
|
|
189
|
+
errors.push('story-state.json is required when chapters exist');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const evidenceKeys = new Set();
|
|
193
|
+
for (let i = 0; i < chapters.length; i += 1) {
|
|
194
|
+
const expected = i + 1;
|
|
195
|
+
const item = chapters[i];
|
|
196
|
+
if (item.number !== expected) errors.push(`chapter sequence must be contiguous: expected ${expected}, found ${item.number}`);
|
|
197
|
+
const fm = chapterFrontmatter(fs.readFileSync(item.path, 'utf8'));
|
|
198
|
+
if (!fm) {
|
|
199
|
+
errors.push(`${item.file} is missing YAML frontmatter`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (fm.schema !== STORY_CHAPTER_SCHEMA) errors.push(`${item.file} must use ${STORY_CHAPTER_SCHEMA}`);
|
|
203
|
+
if (fm.chapter !== item.number) errors.push(`${item.file} frontmatter chapter must equal ${item.number}`);
|
|
204
|
+
if (!fm.title) errors.push(`${item.file} requires a title`);
|
|
205
|
+
if (!fm.evidenceKey || !/^[0-9a-f]{16}$/.test(fm.evidenceKey)) errors.push(`${item.file} requires a 16-hex evidence_key`);
|
|
206
|
+
else if (evidenceKeys.has(fm.evidenceKey)) errors.push(`${item.file} repeats evidence_key ${fm.evidenceKey}`);
|
|
207
|
+
else evidenceKeys.add(fm.evidenceKey);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (state) {
|
|
211
|
+
if (state.lastChapter !== chapters.length) errors.push(`story-state.json lastChapter must equal ${chapters.length}`);
|
|
212
|
+
const consumed = state.consumedEvidenceKeys ?? [];
|
|
213
|
+
if (!Array.isArray(consumed) || consumed.some((key) => !/^[0-9a-f]{16}$/.test(key))) {
|
|
214
|
+
errors.push('story-state.json consumedEvidenceKeys must be an array of 16-hex keys');
|
|
215
|
+
}
|
|
216
|
+
for (const key of evidenceKeys) {
|
|
217
|
+
if (!consumed.includes(key)) errors.push(`story-state.json is missing consumed evidence key ${key}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
ok: errors.length === 0,
|
|
223
|
+
errors,
|
|
224
|
+
chapters: chapters.length,
|
|
225
|
+
nextChapter: chapters.length + 1,
|
|
226
|
+
paths: Object.fromEntries(Object.entries(paths).map(([key, value]) => [key, path.relative(path.resolve(root), value)]))
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
231
|
+
if (isMain) {
|
|
232
|
+
const command = process.argv[2] ?? 'status';
|
|
233
|
+
const root = path.resolve(process.argv[3] ?? process.cwd());
|
|
234
|
+
if (!['status', 'validate'].includes(command)) {
|
|
235
|
+
console.error('usage: story-ledger.mjs [status|validate] [repo-root]');
|
|
236
|
+
process.exit(2);
|
|
237
|
+
}
|
|
238
|
+
const result = validateStoryTree(root);
|
|
239
|
+
console.log(JSON.stringify(result, null, 2));
|
|
240
|
+
if (command === 'validate' && !result.ok) process.exit(1);
|
|
241
|
+
}
|
|
@@ -60,22 +60,22 @@ export function sanitizeContext(ctx = {}) {
|
|
|
60
60
|
const LINES = {
|
|
61
61
|
'repeated-failure': {
|
|
62
62
|
serene: [
|
|
63
|
-
{ vi: 'Giữa làn sương mỏng, Luân Hồi
|
|
64
|
-
{ vi: 'Một lần thử giống hệt khẽ lay chuông, gọi Luân Hồi
|
|
63
|
+
{ vi: 'Giữa làn sương mỏng, Luân Hồi Ma Ảnh hiện bóng khi lỗi cũ quay thêm một vòng.', en: 'In the thin mountain mist, the Error-Cycle Wraith appears as the old error completes another circle.' },
|
|
64
|
+
{ vi: 'Một lần thử giống hệt khẽ lay chuông, gọi Luân Hồi Ma Ảnh trở lại đạo lộ.', en: 'One unchanged retry stirs the bell that calls the Error-Cycle Wraith back to the path.' }
|
|
65
65
|
],
|
|
66
66
|
spirited: [
|
|
67
|
-
{ vi: 'Luân Hồi
|
|
68
|
-
{ vi: 'Vòng lặp cũ vừa khép, Luân Hồi
|
|
67
|
+
{ vi: 'Luân Hồi Ma Ảnh lại gõ chuông: “Không đổi giả thuyết, sao đổi được kết cục?”', en: 'The Error-Cycle Wraith rings its bell again: “Without a new hypothesis, what new ending could appear?”' },
|
|
68
|
+
{ vi: 'Vòng lặp cũ vừa khép, Luân Hồi Ma Ảnh đã bày sẵn bàn trà chờ vòng kế tiếp.', en: 'The old loop has barely closed, and the Error-Cycle Wraith has already set tea for the next.' }
|
|
69
69
|
]
|
|
70
70
|
},
|
|
71
71
|
'too-many-prompts': {
|
|
72
72
|
serene: [
|
|
73
|
-
{ vi: 'Cửu Hoàn Tâm Ma dệt một
|
|
74
|
-
{ vi: 'Nơi ranh giới
|
|
73
|
+
{ vi: 'Cửu Hoàn Tâm Ma dệt một nhiệm vụ thành nhiều vòng sương khi cột mốc chưa hiện rõ.', en: 'The Ninefold Loop Heart-Shadow weaves one task into rings of mist when its checkpoints remain unclear.' },
|
|
74
|
+
{ vi: 'Nơi ranh giới nhiệm vụ nhạt dần, Cửu Hoàn Tâm Ma lặng lẽ nối thêm một vòng.', en: 'Where a task boundary fades, the Ninefold Loop Heart-Shadow quietly adds another circle.' }
|
|
75
75
|
],
|
|
76
76
|
spirited: [
|
|
77
|
-
{ vi: 'Cửu Hoàn Tâm Ma đếm
|
|
78
|
-
{ vi: '
|
|
77
|
+
{ vi: 'Cửu Hoàn Tâm Ma đếm lượt yêu cầu thay cột mốc; đã đến lúc đổi chiếc vòng kế tiếp thành một tiêu chí hoàn thành.', en: 'The Ninefold Loop Heart-Shadow is counting prompts instead of checkpoints; turn the next circle into a done-criterion.' },
|
|
78
|
+
{ vi: 'Lượt yêu cầu càng tản như lá thu, Cửu Hoàn Tâm Ma càng dễ giấu mất lối ra.', en: 'The more prompts scatter like autumn leaves, the easier the Ninefold Loop Heart-Shadow hides the exit.' }
|
|
79
79
|
]
|
|
80
80
|
},
|
|
81
81
|
'conflicting-instructions': {
|
|
@@ -90,21 +90,21 @@ const LINES = {
|
|
|
90
90
|
},
|
|
91
91
|
'unrecovered-failure': {
|
|
92
92
|
serene: [
|
|
93
|
-
{ vi: 'Vô
|
|
94
|
-
{ vi: 'Một lỗi bị bỏ lại làm bóng Vô
|
|
93
|
+
{ vi: 'Vô Nghiệm Ảnh Quân ẩn sau một thất bại chưa được soi lại bằng lần kiểm chứng mới.', en: 'The Unverified Trial Wraith rests behind a failure that has not yet met a fresh validation.' },
|
|
94
|
+
{ vi: 'Một lỗi bị bỏ lại làm bóng Vô Nghiệm Ảnh Quân dài thêm trên bậc đá.', en: 'A failure left behind lengthens the Unverified Trial Wraith’s shadow across the stone steps.' }
|
|
95
95
|
],
|
|
96
96
|
spirited: [
|
|
97
|
-
{ vi: 'Vô
|
|
98
|
-
{ vi: 'Chưa có lần chạy lại, Vô
|
|
97
|
+
{ vi: 'Vô Nghiệm Ảnh Quân phủi bụi trên ghế đá: một lần kiểm chứng thành công là đủ để mời nó rời đi.', en: 'The Unverified Trial Wraith dusts its stone seat; one passing validation is enough to send it on its way.' },
|
|
98
|
+
{ vi: 'Chưa có lần chạy lại, Vô Nghiệm Ảnh Quân vẫn ung dung giữ chỗ bên cổng hoàn thành.', en: 'Without a rerun, the Unverified Trial Wraith calmly keeps its seat beside the completion gate.' }
|
|
99
99
|
]
|
|
100
100
|
},
|
|
101
101
|
'work-without-proof': {
|
|
102
102
|
serene: [
|
|
103
103
|
{ vi: 'Vô Chứng Đan Ảnh lảng vảng bên lò luyện khi lời tuyên bố hoàn thành chưa có bằng chứng đi cùng.', en: 'The Proofless Elixir Shade lingers by the furnace when a claim of completion carries no evidence.' },
|
|
104
|
-
{ vi: 'Một
|
|
104
|
+
{ vi: 'Một bản ghi Git thiếu kết quả kiểm chứng để lại làn khói mỏng cho Vô Chứng Đan Ảnh nương thân.', en: 'A commit without validation leaves a thread of smoke where the Proofless Elixir Shade can dwell.' }
|
|
105
105
|
],
|
|
106
106
|
spirited: [
|
|
107
|
-
{ vi: 'Vô Chứng Đan Ảnh nâng chén trà bên viên đan chưa thử; một dòng
|
|
107
|
+
{ vi: 'Vô Chứng Đan Ảnh nâng chén trà bên viên đan chưa thử; một dòng kiểm chứng sẽ làm màn khói tan ngay.', en: 'The Proofless Elixir Shade raises tea beside an untested pill; one validation line will clear the smoke.' },
|
|
108
108
|
{ vi: 'Không có biên nhận kiểm chứng, Vô Chứng Đan Ảnh cứ ngỡ lò luyện thuộc về mình.', en: 'Without a validation receipt, the Proofless Elixir Shade starts to think the furnace is its own.' }
|
|
109
109
|
]
|
|
110
110
|
}
|