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
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Read a deliberately small, known set of project manifests and reduce them
|
|
2
|
+
// to safe metadata. Repo scripts are never executed and their bodies are never
|
|
3
|
+
// returned or persisted.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
const MAX_METADATA_BYTES = 1024 * 1024;
|
|
9
|
+
const MANIFESTS = Object.freeze([
|
|
10
|
+
'backbone.yml',
|
|
11
|
+
'package.json',
|
|
12
|
+
'yarn.lock',
|
|
13
|
+
'package-lock.json',
|
|
14
|
+
'pnpm-lock.yaml',
|
|
15
|
+
'bun.lock',
|
|
16
|
+
'bun.lockb',
|
|
17
|
+
'tsconfig.json',
|
|
18
|
+
'pyproject.toml',
|
|
19
|
+
'go.mod',
|
|
20
|
+
'Cargo.toml',
|
|
21
|
+
'pubspec.yaml',
|
|
22
|
+
'composer.json',
|
|
23
|
+
'pom.xml',
|
|
24
|
+
'build.gradle',
|
|
25
|
+
'build.gradle.kts'
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const STACK_RULES = Object.freeze([
|
|
29
|
+
['react', ['react']],
|
|
30
|
+
['typescript', ['typescript']],
|
|
31
|
+
['vite', ['vite']],
|
|
32
|
+
['material-ui', ['@mui/material', '@material-ui/core']],
|
|
33
|
+
['nextjs', ['next']],
|
|
34
|
+
['vue', ['vue']],
|
|
35
|
+
['nuxt', ['nuxt']],
|
|
36
|
+
['angular', ['@angular/core']],
|
|
37
|
+
['svelte', ['svelte']],
|
|
38
|
+
['tailwind-css', ['tailwindcss']],
|
|
39
|
+
['express', ['express']],
|
|
40
|
+
['nestjs', ['@nestjs/core']]
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const SAFE_SCRIPT = /^[A-Za-z0-9][A-Za-z0-9:_-]{0,63}$/;
|
|
44
|
+
const VALIDATION_PRIORITY = ['validate', 'check', 'test', 'test:unit', 'lint', 'typecheck', 'build'];
|
|
45
|
+
|
|
46
|
+
function regularFile(root, relative) {
|
|
47
|
+
const absolute = path.join(path.resolve(root), relative);
|
|
48
|
+
try {
|
|
49
|
+
const stat = fs.lstatSync(absolute);
|
|
50
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return null;
|
|
51
|
+
return { path: relative, absolute, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) };
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function inventoryProjectMetadata(root) {
|
|
58
|
+
return MANIFESTS.map((name) => regularFile(root, name))
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.map(({ path: relative, size, mtimeMs }) => ({ path: relative, size, mtimeMs }));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readKnownFile(root, name, warnings) {
|
|
64
|
+
const item = regularFile(root, name);
|
|
65
|
+
if (!item) return null;
|
|
66
|
+
if (item.size > MAX_METADATA_BYTES) {
|
|
67
|
+
warnings.push({ code: 'metadata-too-large', path: name });
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return fs.readFileSync(item.absolute, 'utf8');
|
|
72
|
+
} catch {
|
|
73
|
+
warnings.push({ code: 'metadata-unreadable', path: name });
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function safeSlug(value, fallback = null) {
|
|
79
|
+
const text = String(value ?? '').trim().toLowerCase();
|
|
80
|
+
return /^[a-z0-9][a-z0-9._-]{0,79}$/.test(text) ? text : fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function yamlScalar(text, key) {
|
|
84
|
+
const match = text?.match(new RegExp(`^\\s*${key}:\\s*(.+)$`, 'm'));
|
|
85
|
+
if (!match) return null;
|
|
86
|
+
const value = match[1].trim().replace(/^['"]|['"]$/g, '');
|
|
87
|
+
return value.length <= 160 && !/[\r\n]/.test(value) ? value : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function packageManager(pkg, files) {
|
|
91
|
+
const declared = String(pkg?.packageManager ?? '').match(/^(npm|yarn|pnpm|bun)@/i)?.[1]?.toLowerCase();
|
|
92
|
+
if (declared) return declared;
|
|
93
|
+
if (files.has('pnpm-lock.yaml')) return 'pnpm';
|
|
94
|
+
if (files.has('yarn.lock')) return 'yarn';
|
|
95
|
+
if (files.has('bun.lock') || files.has('bun.lockb')) return 'bun';
|
|
96
|
+
return 'npm';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function scriptCommand(manager, key) {
|
|
100
|
+
if (manager === 'yarn') return `yarn ${key}`;
|
|
101
|
+
if (manager === 'pnpm') return `pnpm ${key}`;
|
|
102
|
+
if (manager === 'bun') return `bun run ${key}`;
|
|
103
|
+
return `npm run ${key}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function packageFacts(pkg, files) {
|
|
107
|
+
const deps = new Set([
|
|
108
|
+
...Object.keys(pkg?.dependencies ?? {}),
|
|
109
|
+
...Object.keys(pkg?.devDependencies ?? {}),
|
|
110
|
+
...Object.keys(pkg?.peerDependencies ?? {})
|
|
111
|
+
]);
|
|
112
|
+
const stack = STACK_RULES.filter(([, names]) => names.some((name) => deps.has(name))).map(([name]) => name);
|
|
113
|
+
if (files.has('tsconfig.json') && !stack.includes('typescript')) stack.push('typescript');
|
|
114
|
+
|
|
115
|
+
const manager = packageManager(pkg, files);
|
|
116
|
+
const scripts = Object.keys(pkg?.scripts ?? {}).filter((key) => SAFE_SCRIPT.test(key));
|
|
117
|
+
const validationCommands = VALIDATION_PRIORITY
|
|
118
|
+
.filter((key) => scripts.includes(key))
|
|
119
|
+
.slice(0, 3)
|
|
120
|
+
.map((key) => scriptCommand(manager, key));
|
|
121
|
+
|
|
122
|
+
const hasWebFramework = stack.some((name) => ['react', 'nextjs', 'vue', 'nuxt', 'angular', 'svelte'].includes(name));
|
|
123
|
+
const hasLibraryEntry = Boolean(pkg?.main || pkg?.module || pkg?.exports);
|
|
124
|
+
return {
|
|
125
|
+
projectId: safeSlug(pkg?.name),
|
|
126
|
+
packageManager: manager,
|
|
127
|
+
stack,
|
|
128
|
+
primaryLanguage: stack.includes('typescript') ? 'typescript' : 'javascript',
|
|
129
|
+
projectType: hasLibraryEntry ? 'library' : (hasWebFramework ? 'web-app' : 'node-project'),
|
|
130
|
+
validationCommands
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function uniqueSlugs(values, limit = 16) {
|
|
135
|
+
return [...new Set(values.map((value) => safeSlug(value)).filter(Boolean))].slice(0, limit);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function buildProjectProfile(root, options = {}) {
|
|
139
|
+
const resolvedRoot = path.resolve(root);
|
|
140
|
+
const inventory = inventoryProjectMetadata(resolvedRoot);
|
|
141
|
+
const files = new Set(inventory.map((item) => item.path));
|
|
142
|
+
const warnings = [];
|
|
143
|
+
const profile = {
|
|
144
|
+
projectId: safeSlug(path.basename(resolvedRoot), 'repo'),
|
|
145
|
+
description: null,
|
|
146
|
+
primaryLanguage: null,
|
|
147
|
+
projectType: null,
|
|
148
|
+
domains: [],
|
|
149
|
+
stack: [],
|
|
150
|
+
packageManager: null,
|
|
151
|
+
validationCommands: [],
|
|
152
|
+
recommendedValidation: null,
|
|
153
|
+
metadataSources: inventory.map((item) => item.path),
|
|
154
|
+
kitInstalled: files.has('backbone.yml') && fs.existsSync(path.join(resolvedRoot, '.vibekit', 'skills')),
|
|
155
|
+
authorization: options.authorization ?? null,
|
|
156
|
+
declared: { faction: options.faction, affiliation: options.affiliation, paths: options.paths },
|
|
157
|
+
warnings
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const backbone = readKnownFile(resolvedRoot, 'backbone.yml', warnings);
|
|
161
|
+
if (backbone) {
|
|
162
|
+
profile.description = yamlScalar(backbone, 'description');
|
|
163
|
+
profile.primaryLanguage = safeSlug(yamlScalar(backbone, 'primary_language'));
|
|
164
|
+
profile.projectType = safeSlug(yamlScalar(backbone, 'type'));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const packageText = readKnownFile(resolvedRoot, 'package.json', warnings);
|
|
168
|
+
if (packageText) {
|
|
169
|
+
try {
|
|
170
|
+
const pkg = JSON.parse(packageText);
|
|
171
|
+
const facts = packageFacts(pkg, files);
|
|
172
|
+
Object.assign(profile, {
|
|
173
|
+
projectId: facts.projectId ?? profile.projectId,
|
|
174
|
+
packageManager: facts.packageManager,
|
|
175
|
+
stack: facts.stack,
|
|
176
|
+
primaryLanguage: profile.primaryLanguage ?? facts.primaryLanguage,
|
|
177
|
+
projectType: profile.projectType ?? facts.projectType,
|
|
178
|
+
validationCommands: facts.validationCommands
|
|
179
|
+
});
|
|
180
|
+
} catch {
|
|
181
|
+
warnings.push({ code: 'metadata-invalid-json', path: 'package.json' });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!profile.primaryLanguage) {
|
|
186
|
+
if (files.has('pyproject.toml')) profile.primaryLanguage = 'python';
|
|
187
|
+
else if (files.has('go.mod')) profile.primaryLanguage = 'go';
|
|
188
|
+
else if (files.has('Cargo.toml')) profile.primaryLanguage = 'rust';
|
|
189
|
+
else if (files.has('pubspec.yaml')) profile.primaryLanguage = 'dart';
|
|
190
|
+
}
|
|
191
|
+
if (!profile.projectType) profile.projectType = files.has('package.json') ? 'node-project' : 'software-project';
|
|
192
|
+
|
|
193
|
+
const domains = [...(options.domains ?? [])];
|
|
194
|
+
if (profile.stack.some((name) => ['react', 'nextjs', 'vue', 'nuxt', 'angular', 'svelte'].includes(name))) domains.push('frontend', 'application', 'interface');
|
|
195
|
+
if (profile.stack.includes('material-ui')) domains.push('ui', 'design', 'component');
|
|
196
|
+
if (profile.projectType === 'library') domains.push('library', 'framework');
|
|
197
|
+
profile.domains = uniqueSlugs(domains, 12);
|
|
198
|
+
profile.stack = uniqueSlugs(profile.stack, 12);
|
|
199
|
+
profile.recommendedValidation = profile.validationCommands[0] ?? null;
|
|
200
|
+
return profile;
|
|
201
|
+
}
|
|
202
|
+
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
// Phase 2
|
|
2
|
-
// language-neutral
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Phase 2 evidence ledger. buildReportModel() turns analysis JSON into a
|
|
2
|
+
// language-neutral model (facts only). renderMarkdown() provides a stable
|
|
3
|
+
// inspection and audit view in the active language. It is deliberately not a
|
|
4
|
+
// user-facing response template: the agent composes that response adaptively
|
|
5
|
+
// from this model and the live project/story context. The ledger NEVER changes
|
|
6
|
+
// a count from the analysis; it only selects wording.
|
|
6
7
|
|
|
7
8
|
import { scoreReport, DIMENSIONS } from './score.mjs';
|
|
8
9
|
import { detectProblems } from './catalog.mjs';
|
|
9
10
|
import { buildVillains } from './villains.mjs';
|
|
10
|
-
import { classifyProject, progressionMetrics, PROGRESSION } from './classify.mjs';
|
|
11
|
+
import { classifyProject, localizeRationale, progressionMetrics, PROGRESSION } from './classify.mjs';
|
|
11
12
|
import { resolveTone, TUTIEN_EXPERIENCE } from './command.mjs';
|
|
12
13
|
|
|
13
14
|
// A context is sensitive (humor forced neutral) when unrecovered failures or
|
|
@@ -24,6 +25,13 @@ export function isSensitive(analysis, options = {}) {
|
|
|
24
25
|
return unrecovered || (analysis.conflicts ?? []).length > 0;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function resolveProjectHelp(projectHelp, profile) {
|
|
29
|
+
if (!profile) return projectHelp;
|
|
30
|
+
if (projectHelp === 'backbone.yml validate') return profile.recommendedValidation ?? null;
|
|
31
|
+
if (['sequential-thinking', 'clearthought', 'claim'].includes(projectHelp) && profile.kitInstalled !== true) return null;
|
|
32
|
+
return projectHelp;
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
export function buildReportModel(analysis, options = {}) {
|
|
28
36
|
const score = scoreReport(analysis);
|
|
29
37
|
const rawProblems = detectProblems(analysis);
|
|
@@ -73,19 +81,27 @@ export function buildReportModel(analysis, options = {}) {
|
|
|
73
81
|
: problems.length
|
|
74
82
|
? problems[0].meta.ifThen
|
|
75
83
|
: {
|
|
76
|
-
vi: 'Nếu bắt đầu một
|
|
84
|
+
vi: 'Nếu bắt đầu một nhiệm vụ mới, hãy viết tiêu chí hoàn thành trước khi sửa mã nguồn.',
|
|
77
85
|
en: 'If you start a new task, then write its done-criterion before editing code.'
|
|
78
86
|
};
|
|
79
87
|
|
|
88
|
+
const topProjectHelp = problems.length ? resolveProjectHelp(problems[0].meta.projectHelp, options.profile) : null;
|
|
80
89
|
const lesson = suppressed
|
|
81
90
|
? null
|
|
82
91
|
: problems.length
|
|
83
|
-
? { skill:
|
|
84
|
-
: { skill: 'backbone.yml validate', technique: { vi: 'Giữ vòng lặp: sửa nhỏ, chạy
|
|
92
|
+
? { skill: topProjectHelp, technique: problems[0].meta.counterTechnique }
|
|
93
|
+
: { skill: options.profile?.recommendedValidation ?? (options.profile ? null : 'backbone.yml validate'), technique: { vi: 'Giữ vòng lặp: sửa nhỏ, chạy kiểm chứng, xác nhận.', en: 'Keep the loop: small edit, run validation, confirm.' } };
|
|
85
94
|
|
|
86
95
|
return {
|
|
87
96
|
schemaVersion: 1,
|
|
88
97
|
experience: { ...TUTIEN_EXPERIENCE },
|
|
98
|
+
composition: {
|
|
99
|
+
finalResponse: 'agent-authored',
|
|
100
|
+
ledgerRole: 'evidence-scaffold-only',
|
|
101
|
+
adaptTo: ['current-user-request', 'project-specific-facts', 'living-chronicle', 'recent-response-rhythm'],
|
|
102
|
+
preserve: ['facts', 'confidence', 'policy', 'privacy', 'continuity'],
|
|
103
|
+
vary: ['structure', 'opening', 'scene', 'pacing', 'dialogue', 'technical-density', 'closing-image']
|
|
104
|
+
},
|
|
89
105
|
tone,
|
|
90
106
|
villainsShown: villains.length,
|
|
91
107
|
villainCards: villains,
|
|
@@ -122,7 +138,7 @@ export function buildReportModel(analysis, options = {}) {
|
|
|
122
138
|
priority: p.priority,
|
|
123
139
|
evidence: p.evidence,
|
|
124
140
|
counterTechnique: p.meta.counterTechnique,
|
|
125
|
-
projectHelp: p.meta.projectHelp,
|
|
141
|
+
projectHelp: resolveProjectHelp(p.meta.projectHelp, options.profile),
|
|
126
142
|
microQuest: p.meta.microQuest,
|
|
127
143
|
victory: p.meta.victory,
|
|
128
144
|
villain: v ? { name: v.archetype.name, gloss: v.archetype.gloss, role: v.role, tone: v.tone, challenge: v.challenge } : null
|
|
@@ -135,59 +151,78 @@ export function buildReportModel(analysis, options = {}) {
|
|
|
135
151
|
|
|
136
152
|
const L = {
|
|
137
153
|
vi: {
|
|
138
|
-
title: 'Tu
|
|
139
|
-
neutralTitle: 'Bản
|
|
154
|
+
title: 'Tu tiên tĩnh ký',
|
|
155
|
+
neutralTitle: 'Bản ghi rà soát quy trình',
|
|
140
156
|
opening: {
|
|
141
157
|
serene: 'Mây nhẹ qua sơn môn, một trang đạo ký lặng lẽ mở ra. Đây là cuộc dạo chơi phân loại nếp cộng tác: thở chậm, nhìn rõ, rồi chọn một bước vừa sức.',
|
|
142
158
|
spirited: 'Chuông ngọc khẽ ngân nơi sơn môn; đạo ký mở sang trang mới. Ta cùng nhìn dấu vết công việc bằng một chút hóm hỉnh và một bước tiến thật rõ ràng.',
|
|
143
159
|
neutral: 'Bản ghi này trình bày bằng giọng bình tĩnh, trực tiếp; phần diễn xướng tu tiên tạm dừng để ưu tiên an toàn và sự rõ ràng.'
|
|
144
160
|
},
|
|
145
|
-
thienCo: 'Thiên
|
|
146
|
-
canhGioi: 'Cảnh
|
|
147
|
-
linhThach: 'Linh
|
|
148
|
-
tamMa: 'Tâm
|
|
149
|
-
congPhap: 'Một
|
|
150
|
-
baiHoc: 'Công
|
|
151
|
-
phuongPhap: 'Đạo
|
|
152
|
-
closingTitle: 'Khép
|
|
161
|
+
thienCo: 'Thiên cơ trong tầm mắt',
|
|
162
|
+
canhGioi: 'Cảnh giới bên hiên mây',
|
|
163
|
+
linhThach: 'Linh thạch đã tiêu',
|
|
164
|
+
tamMa: 'Tâm ma trên đạo lộ',
|
|
165
|
+
congPhap: 'Một thức cho lần đột phá tới',
|
|
166
|
+
baiHoc: 'Công pháp mang theo hôm nay',
|
|
167
|
+
phuongPhap: 'Đạo điển và nguồn số liệu',
|
|
168
|
+
closingTitle: 'Khép lại đạo ký',
|
|
153
169
|
closing: {
|
|
154
|
-
clear: 'Đạo lộ
|
|
155
|
-
challenge: '
|
|
156
|
-
paused: 'Bản ghi dừng tại đây. Hãy
|
|
170
|
+
clear: 'Đạo lộ yên không có nghĩa đạo tâm được phép ngủ quên. Giữ bước sửa nhỏ và chứng cứ thật; chuông kiểm chứng vẫn treo dưới mái sơn môn, chờ lần ai đó định đi tắt.',
|
|
171
|
+
challenge: 'Tâm ma đã để dấu chân rõ đến thế mà còn vòng qua nó thì chỉ là tự dâng thêm lương khô cho địch. Hạ đúng một nút thắt bằng kiểm chứng thật; ngoài sơn môn, bóng áo của nó vẫn chưa tan.',
|
|
172
|
+
paused: 'Bản ghi dừng tại đây. Hãy hoàn tất bước rà soát hoặc ủy quyền cần thiết trước; không có cảnh giới nào quan trọng hơn một ranh giới an toàn.'
|
|
157
173
|
},
|
|
158
|
-
sessions: 'phiên', prompts: '
|
|
159
|
-
sessionOne: 'phiên', promptOne: '
|
|
174
|
+
sessions: 'phiên', prompts: 'lượt yêu cầu', commits: 'bản ghi Git',
|
|
175
|
+
sessionOne: 'phiên', promptOne: 'lượt yêu cầu', commitOne: 'bản ghi Git', windowOne: 'kỳ báo cáo',
|
|
160
176
|
window: 'Khoảng thời gian', confidence: 'Độ tin cậy',
|
|
161
|
-
reported: 'nhà cung cấp báo cáo (chính xác)', estimated: '
|
|
162
|
-
dedup: '
|
|
177
|
+
reported: 'Số liệu do nhà cung cấp báo cáo (chính xác)', estimated: 'Ước lượng cục bộ', unknown: 'Lượt chưa rõ số token',
|
|
178
|
+
dedup: 'Cập nhật luồng đã gộp',
|
|
163
179
|
score: 'Điểm', evidence: 'Bằng chứng', counter: 'Cách hóa giải', quest: 'Nhiệm vụ nhỏ', victory: 'Điều kiện thắng',
|
|
164
|
-
help: 'Trợ giúp sẵn có', ifthen: 'Nếu
|
|
180
|
+
help: 'Trợ giúp sẵn có', ifthen: 'Nếu…thì', notEnough: 'Chưa đủ thiên cơ để tính cảnh giới',
|
|
165
181
|
noProblems: 'Không phát hiện tâm ma nào vượt ngưỡng bằng chứng. Giữ vững đạo tâm.',
|
|
166
|
-
method: 'Phân tích tất định phiên bản 1
|
|
167
|
-
repeatEv: (n) => `${n} lần lặp
|
|
168
|
-
conflictEv: (n) => `${n} cặp chỉ thị mâu thuẫn (
|
|
169
|
-
unrecEv: (n) => `${n} thất bại chưa có lần
|
|
170
|
-
proofEv: (n) => `${n}
|
|
171
|
-
daoSection: 'Đạo
|
|
182
|
+
method: 'Phân tích tất định phiên bản 1. Số đếm là các dấu hiệu có mức tin cậy; phần diễn giải không thay đổi số liệu.',
|
|
183
|
+
repeatEv: (n) => `${n} lần lặp hoặc thử lại trong một nhiệm vụ (dấu hiệu).`,
|
|
184
|
+
conflictEv: (n) => `${n} cặp chỉ thị mâu thuẫn (dấu hiệu).`,
|
|
185
|
+
unrecEv: (n) => `${n} thất bại chưa có lần kiểm chứng đạt tương ứng.`,
|
|
186
|
+
proofEv: (n) => `${n} bản ghi Git nhưng không có sự kiện kiểm chứng.`,
|
|
187
|
+
daoSection: 'Đạo và truyền thừa',
|
|
172
188
|
faction: 'Đạo', affiliation: 'Thân phận', pathsL: 'Đạo tu',
|
|
173
|
-
affNote: 'Thân phận là vị thế tổ chức, không phải đạo đức
|
|
189
|
+
affNote: 'Thân phận là vị thế tổ chức, không phải đạo đức. Tán Tu không phải mặt đối lập của Tà Tu.',
|
|
174
190
|
tienCanh: 'Tiến cảnh',
|
|
175
191
|
lifetime: 'tích lũy', windowsL: 'kỳ báo cáo',
|
|
176
|
-
nghiepWarn: 'Nghiệp lực chưa hóa giải
|
|
177
|
-
suppressedProgression: 'Chính sách đang tạm dừng: không cộng Tu Vi, Công Đức hay các chỉ số tích cực
|
|
192
|
+
nghiepWarn: 'Nghiệp lực chưa hóa giải. Hãy ưu tiên xử lý các mục còn treo ở trên.',
|
|
193
|
+
suppressedProgression: 'Chính sách đang tạm dừng: không cộng Tu Vi, Công Đức hay các chỉ số tích cực; chỉ còn Nghiệp Lực.',
|
|
178
194
|
policyNotice: {
|
|
179
|
-
'declared-stop': 'Tà Đạo không phải con đường tu luyện: không cảnh giới, không Tu Vi
|
|
180
|
-
'needs-review': 'Phát hiện dấu hiệu chủ đích gây hại: chưa gán đạo, tạm dừng
|
|
181
|
-
'authorization-required': 'Công việc Ma Đạo cần ủy quyền: ghi lại phạm vi
|
|
195
|
+
'declared-stop': 'Tà Đạo không phải con đường tu luyện: không cảnh giới, không Tu Vi, không Công Đức và không cốt truyện. Dừng lại để con người rà soát hoặc xác nhận ủy quyền hợp pháp.',
|
|
196
|
+
'needs-review': 'Phát hiện dấu hiệu chủ đích gây hại: chưa gán đạo, tạm dừng phần trò chơi và cần con người rà soát trước khi tiếp tục.',
|
|
197
|
+
'authorization-required': 'Công việc Ma Đạo cần ủy quyền: ghi lại phạm vi công việc bằng `authorization=<slug>` trước khi mở khóa cảnh giới và tiến cảnh.'
|
|
182
198
|
},
|
|
183
199
|
suppressedRealmNotice: {
|
|
184
|
-
'declared-stop': 'Không tính cảnh giới cho Tà Đạo. Dừng lại
|
|
185
|
-
'needs-review': 'Không tính cảnh giới: cần
|
|
200
|
+
'declared-stop': 'Không tính cảnh giới cho Tà Đạo. Dừng lại để rà soát hoặc xác nhận ủy quyền.',
|
|
201
|
+
'needs-review': 'Không tính cảnh giới: dấu hiệu gây hại cần được con người rà soát.',
|
|
186
202
|
'authorization-required': 'Không tính cảnh giới: công việc Ma Đạo chưa có ủy quyền được ghi nhận.'
|
|
187
203
|
},
|
|
188
204
|
lineageIntro: 'Ba đường phân loại dưới đây chỉ nói về dự án và nếp làm việc; không phải nhãn dán cho con người.',
|
|
189
205
|
progressionIntro: 'Tiến cảnh là chiếc gương nhỏ để quan sát thói quen, không phải thước đo giá trị của đạo hữu.',
|
|
190
|
-
problemIntro: 'Mỗi tâm ma dưới đây chỉ là tên gọi vui cho một mẫu quy trình có bằng chứng
|
|
206
|
+
problemIntro: 'Mỗi tâm ma dưới đây chỉ là tên gọi vui cho một mẫu quy trình có bằng chứng. Nhận diện để buông nhẹ, không phải để tự trách.',
|
|
207
|
+
detailSeparator: ': ',
|
|
208
|
+
reasonSeparator: '; ',
|
|
209
|
+
coverageSeparator: ', ',
|
|
210
|
+
knowledge: {
|
|
211
|
+
tamPhap: 'Tâm pháp', congPhap: 'Công pháp', biThuat: 'Bí thuật', thanThong: 'Thần thông',
|
|
212
|
+
phapBao: 'Pháp bảo', thuatPhap: 'Thuật pháp', daoDien: 'Bí tịch và đạo điển'
|
|
213
|
+
},
|
|
214
|
+
confidenceLevels: { high: 'cao', medium: 'vừa', low: 'thấp' },
|
|
215
|
+
dimensionLabels: {
|
|
216
|
+
delivery: 'hoàn thành', validation: 'kiểm chứng', clarity: 'rõ ràng', recovery: 'khôi phục', safety: 'an toàn', automation: 'tự động hóa'
|
|
217
|
+
},
|
|
218
|
+
roleLabels: { boss: 'đại địch', minor: 'địch thủ' },
|
|
219
|
+
problemLabels: {
|
|
220
|
+
'repeated-failure': 'Lỗi lặp lại',
|
|
221
|
+
'too-many-prompts': 'Quá nhiều lượt yêu cầu',
|
|
222
|
+
'conflicting-instructions': 'Chỉ thị mâu thuẫn',
|
|
223
|
+
'unrecovered-failure': 'Thất bại chưa khôi phục',
|
|
224
|
+
'work-without-proof': 'Công việc thiếu bằng chứng'
|
|
225
|
+
}
|
|
191
226
|
},
|
|
192
227
|
en: {
|
|
193
228
|
title: 'Quiet Cultivation Chronicle',
|
|
@@ -206,8 +241,8 @@ const L = {
|
|
|
206
241
|
phuongPhap: 'Dao Record & Evidence Sources',
|
|
207
242
|
closingTitle: 'Closing the Dao Record',
|
|
208
243
|
closing: {
|
|
209
|
-
clear: '
|
|
210
|
-
challenge: '
|
|
244
|
+
clear: 'A quiet path is no excuse for a sleeping discipline. Keep the edits small and the proof honest; the validation bell is still hanging beneath the eaves, waiting for the next attempted shortcut.',
|
|
245
|
+
challenge: 'The flaw has left tracks clear enough that walking around it would only feed the enemy. Break one knot with real proof; beyond the gate, its sleeve has not yet vanished into the mist.',
|
|
211
246
|
paused: 'This record pauses here. Complete the required review or authorization first; no realm matters more than a sound safety boundary.'
|
|
212
247
|
},
|
|
213
248
|
sessions: 'sessions', prompts: 'prompts', commits: 'commits',
|
|
@@ -242,11 +277,25 @@ const L = {
|
|
|
242
277
|
},
|
|
243
278
|
lineageIntro: 'These three classifications describe the project and its working patterns; they are never labels for the person.',
|
|
244
279
|
progressionIntro: 'Progression is a small mirror for observing habits, not a measure of anyone\'s worth.',
|
|
245
|
-
problemIntro: 'Each heart demon below is a playful name for an evidence-backed workflow pattern—something to notice and release, never a reason for self-blame.'
|
|
280
|
+
problemIntro: 'Each heart demon below is a playful name for an evidence-backed workflow pattern—something to notice and release, never a reason for self-blame.',
|
|
281
|
+
detailSeparator: ' — ',
|
|
282
|
+
reasonSeparator: ' — ',
|
|
283
|
+
coverageSeparator: ' / ',
|
|
284
|
+
knowledge: {
|
|
285
|
+
tamPhap: 'Tâm Pháp', congPhap: 'Công Pháp', biThuat: 'Bí Thuật', thanThong: 'Thần Thông',
|
|
286
|
+
phapBao: 'Pháp Bảo', thuatPhap: 'Thuật Pháp', daoDien: 'Bí Tịch / Đạo Điển'
|
|
287
|
+
},
|
|
288
|
+
confidenceLevels: {},
|
|
289
|
+
dimensionLabels: {},
|
|
290
|
+
roleLabels: {},
|
|
291
|
+
problemLabels: {}
|
|
246
292
|
}
|
|
247
293
|
};
|
|
248
294
|
|
|
249
295
|
const pick = (obj, lang) => obj[lang] ?? obj.en ?? obj.vi;
|
|
296
|
+
const localized = (map, value) => map?.[String(value)] ?? value;
|
|
297
|
+
const joinDetail = (left, right, t) => `${left}${t.detailSeparator ?? ' — '}${right}`;
|
|
298
|
+
const joinReason = (left, right, t) => `${left}${t.reasonSeparator ?? ' — '}${right}`;
|
|
250
299
|
|
|
251
300
|
function bar(value) {
|
|
252
301
|
const filled = Math.round(value * 10);
|
|
@@ -257,28 +306,35 @@ function realmName(realm, lang) {
|
|
|
257
306
|
return lang === 'vi' ? realm.name : `${realm.name} — ${realm.gloss}`;
|
|
258
307
|
}
|
|
259
308
|
|
|
309
|
+
function missingReason(reason, lang) {
|
|
310
|
+
if (lang === 'vi' && reason === 'token usage coverage below 60%; supply provider usage metadata to compute a realm') {
|
|
311
|
+
return 'Độ phủ số liệu token dưới 60%; cần bổ sung dữ liệu sử dụng từ nhà cung cấp để tính cảnh giới.';
|
|
312
|
+
}
|
|
313
|
+
return reason;
|
|
314
|
+
}
|
|
315
|
+
|
|
260
316
|
function classificationLines(classification, lang, t) {
|
|
261
317
|
const c = classification;
|
|
262
318
|
const out = [`## ${t.daoSection}`, t.lineageIntro];
|
|
263
|
-
out.push(`- ${t.faction}:
|
|
319
|
+
out.push(`- ${t.faction}: ${joinDetail(`**${c.faction.name}**`, pick(c.faction.gloss, lang), t)} (${t.confidence} ${localized(t.confidenceLevels, c.faction.confidence)})`);
|
|
264
320
|
for (const line of pick(c.explanation, lang)) out.push(` - ${line}`);
|
|
265
321
|
if (c.policy.state !== 'clear') out.push(` - ${t.policyNotice[c.policy.state]}`);
|
|
266
|
-
out.push(`- ${t.affiliation}:
|
|
322
|
+
out.push(`- ${t.affiliation}: ${joinDetail(`**${c.affiliation.name}**`, pick(c.affiliation.gloss, lang), t)} (${t.confidence} ${localized(t.confidenceLevels, c.affiliation.confidence)}; ${localizeRationale(c.affiliation.rationale, lang)})`);
|
|
267
323
|
out.push(` - ${t.affNote}`);
|
|
268
324
|
if (c.paths.length) {
|
|
269
|
-
out.push(`- ${t.pathsL}: ${c.paths.map((p) => `**${p.name}** (${p.confidence})`).join(', ')}`);
|
|
270
|
-
for (const p of c.paths) out.push(` - ${p.name}: ${pick(p.gloss, lang)
|
|
325
|
+
out.push(`- ${t.pathsL}: ${c.paths.map((p) => `**${p.name}** (${localized(t.confidenceLevels, p.confidence)})`).join(', ')}`);
|
|
326
|
+
for (const p of c.paths) out.push(` - ${p.name}: ${joinReason(pick(p.gloss, lang), localizeRationale(p.rationale, lang), t)}`);
|
|
271
327
|
}
|
|
272
328
|
// Knowledge recommendations only under a clear policy.
|
|
273
329
|
const k = c.knowledge;
|
|
274
330
|
if (k) {
|
|
275
|
-
out.push(`-
|
|
276
|
-
out.push(`-
|
|
277
|
-
out.push(`-
|
|
278
|
-
out.push(`-
|
|
279
|
-
if (k.phapBao.length) out.push(`-
|
|
280
|
-
out.push(`-
|
|
281
|
-
out.push(`-
|
|
331
|
+
out.push(`- ${t.knowledge.tamPhap}: ${pick(k.tamPhap, lang)}`);
|
|
332
|
+
out.push(`- ${t.knowledge.congPhap}: ${pick(k.congPhap, lang)}`);
|
|
333
|
+
out.push(`- ${t.knowledge.biThuat}: ${pick(k.biThuat, lang)}`);
|
|
334
|
+
out.push(`- ${t.knowledge.thanThong}: ${pick(k.thanThong, lang)}`);
|
|
335
|
+
if (k.phapBao.length) out.push(`- ${t.knowledge.phapBao}: ${k.phapBao.map((s) => `\`${s}\``).join(', ')}`);
|
|
336
|
+
out.push(`- ${t.knowledge.thuatPhap}: ${pick(k.thuatPhap, lang)}`);
|
|
337
|
+
out.push(`- ${t.knowledge.daoDien}: ${pick(k.daoDien, lang)}`);
|
|
282
338
|
}
|
|
283
339
|
return out;
|
|
284
340
|
}
|
|
@@ -289,18 +345,18 @@ function cultivationLines(cultivation, lang, t) {
|
|
|
289
345
|
const gamified = cultivation.classification.policy.canGamify;
|
|
290
346
|
out.push('', `## ${t.tienCanh}`, t.progressionIntro);
|
|
291
347
|
if (gamified) {
|
|
292
|
-
out.push(`- ${PROGRESSION.tuVi.name}:
|
|
348
|
+
out.push(`- ${PROGRESSION.tuVi.name}: ${joinDetail(`+${pr.tuVi.window} (${t.lifetime} ${pr.tuVi.lifetime})`, pick(PROGRESSION.tuVi.gloss, lang), t)}`);
|
|
293
349
|
const windowUnit = pr.daoHanh.windows === 1 ? t.windowOne : t.windowsL;
|
|
294
|
-
out.push(`- ${PROGRESSION.daoHanh.name}: ${pr.daoHanh.windows} ${windowUnit}
|
|
295
|
-
out.push(`- ${PROGRESSION.ngoTinh.name} \`${bar(pr.ngoTinh)}\` ${Math.round(pr.ngoTinh * 100)}
|
|
296
|
-
out.push(`- ${PROGRESSION.doThuanThuc.name} \`${bar(pr.doThuanThuc.overall)}\` ${Math.round(pr.doThuanThuc.overall * 100)}
|
|
297
|
-
out.push(`- ${PROGRESSION.tamCanh.name} \`${bar(pr.tamCanh)}\` ${Math.round(pr.tamCanh * 100)}
|
|
298
|
-
out.push(`- ${PROGRESSION.congDuc.name}:
|
|
350
|
+
out.push(`- ${PROGRESSION.daoHanh.name}: ${joinDetail(`${pr.daoHanh.windows} ${windowUnit}`, pick(PROGRESSION.daoHanh.gloss, lang), t)}`);
|
|
351
|
+
out.push(`- ${joinDetail(`${PROGRESSION.ngoTinh.name} \`${bar(pr.ngoTinh)}\` ${Math.round(pr.ngoTinh * 100)}%`, pick(PROGRESSION.ngoTinh.gloss, lang), t)}`);
|
|
352
|
+
out.push(`- ${joinDetail(`${PROGRESSION.doThuanThuc.name} \`${bar(pr.doThuanThuc.overall)}\` ${Math.round(pr.doThuanThuc.overall * 100)}%`, pick(pr.doThuanThuc.note, lang), t)}`);
|
|
353
|
+
out.push(`- ${joinDetail(`${PROGRESSION.tamCanh.name} \`${bar(pr.tamCanh)}\` ${Math.round(pr.tamCanh * 100)}%`, pick(PROGRESSION.tamCanh.gloss, lang), t)}`);
|
|
354
|
+
out.push(`- ${PROGRESSION.congDuc.name}: ${joinDetail(`+${pr.congDuc.window} (${t.lifetime} ${pr.congDuc.lifetime})`, pick(PROGRESSION.congDuc.gloss, lang), t)}`);
|
|
299
355
|
} else {
|
|
300
356
|
// Suppressed policy: only Nghiệp Lực (risk) survives; no positive gains.
|
|
301
357
|
out.push(`- ${t.suppressedProgression}`);
|
|
302
358
|
}
|
|
303
|
-
out.push(`- ${PROGRESSION.nghiepLuc.name}: ${pr.nghiepLuc.window} (${t.lifetime} ${pr.nghiepLuc.lifetime})
|
|
359
|
+
out.push(`- ${PROGRESSION.nghiepLuc.name}: ${joinDetail(`${pr.nghiepLuc.window} (${t.lifetime} ${pr.nghiepLuc.lifetime})`, pick(PROGRESSION.nghiepLuc.gloss, lang), t)}`);
|
|
304
360
|
if (pr.nghiepLuc.window > 0) out.push(` - ${t.nghiepWarn}`);
|
|
305
361
|
return out;
|
|
306
362
|
}
|
|
@@ -338,21 +394,21 @@ export function renderMarkdown(model, language = 'en') {
|
|
|
338
394
|
const sessionUnit = cov.sessions === 1 ? t.sessionOne : t.sessions;
|
|
339
395
|
const promptUnit = cov.userPrompts === 1 ? t.promptOne : t.prompts;
|
|
340
396
|
const commitUnit = cov.commits === 1 ? t.commitOne : t.commits;
|
|
341
|
-
out.push(`- ${cov.sessions} ${sessionUnit}
|
|
397
|
+
out.push(`- ${cov.sessions} ${sessionUnit}${t.coverageSeparator}${cov.userPrompts} ${promptUnit}${t.coverageSeparator}${cov.commits} ${commitUnit}`);
|
|
342
398
|
out.push(`- ${t.window}: ${cov.window.start ?? '—'} … ${cov.window.end ?? '—'}`);
|
|
343
399
|
out.push(`- ${t.reported}: ${Math.round(cov.reportedTurnsPct * 100)}% · ${t.estimated}: ${Math.round(cov.estimatedTurnsPct * 100)}%`);
|
|
344
|
-
out.push(`- ${t.confidence}: ${cov.confidence}`, '');
|
|
400
|
+
out.push(`- ${t.confidence}: ${localized(t.confidenceLevels, cov.confidence)}`, '');
|
|
345
401
|
|
|
346
402
|
out.push(`## ${t.canhGioi}`);
|
|
347
403
|
if (model.suppressGamification) {
|
|
348
404
|
out.push(t.suppressedRealmNotice[model.policyState] ?? t.suppressedRealmNotice['declared-stop']);
|
|
349
405
|
} else if (!model.enoughEvidence) {
|
|
350
406
|
out.push(`**${realmName(model.realm, lang)}** — ${t.notEnough}.`);
|
|
351
|
-
for (const m of model.missing) out.push(`- ${m}`);
|
|
407
|
+
for (const m of model.missing) out.push(`- ${missingReason(m, lang)}`);
|
|
352
408
|
} else {
|
|
353
409
|
out.push(`**${realmName(model.realm, lang)}**${model.showScore ? ` · ${t.score}: ${model.score}/100` : ''}`);
|
|
354
410
|
for (const d of model.dimensions) {
|
|
355
|
-
out.push(`- ${d.key} \`${bar(d.value)}\` ${Math.round(d.value * 100)}% (${Math.round(d.weight * 100)}%)`);
|
|
411
|
+
out.push(`- ${localized(t.dimensionLabels, d.key)} \`${bar(d.value)}\` ${Math.round(d.value * 100)}% (${Math.round(d.weight * 100)}%)`);
|
|
356
412
|
}
|
|
357
413
|
}
|
|
358
414
|
out.push('');
|
|
@@ -376,15 +432,15 @@ export function renderMarkdown(model, language = 'en') {
|
|
|
376
432
|
for (const p of model.problems) {
|
|
377
433
|
if (p.villain) {
|
|
378
434
|
const label = lang === 'vi' ? p.villain.name : `${p.villain.name} — ${p.villain.gloss}`;
|
|
379
|
-
out.push(`### ${label} (${p.villain.role}, ${t.confidence} ${p.confidence})`);
|
|
435
|
+
out.push(`### ${label} (${localized(t.roleLabels, p.villain.role)}, ${t.confidence} ${p.confidence})`);
|
|
380
436
|
out.push(`> ${pick(p.villain.challenge, lang)}`);
|
|
381
437
|
} else {
|
|
382
|
-
out.push(`### ${p.problemType} (${t.confidence} ${p.confidence})`);
|
|
438
|
+
out.push(`### ${localized(t.problemLabels, p.problemType)} (${t.confidence} ${p.confidence})`);
|
|
383
439
|
}
|
|
384
440
|
// A challenge is always immediately followed by evidence + counter + victory.
|
|
385
441
|
out.push(`- ${t.evidence}: ${problemEvidenceLine(p, t)}`);
|
|
386
442
|
out.push(`- ${t.counter}: ${pick(p.counterTechnique, lang)}`);
|
|
387
|
-
out.push(`- ${t.help}: \`${p.projectHelp}\``);
|
|
443
|
+
if (p.projectHelp) out.push(`- ${t.help}: \`${p.projectHelp}\``);
|
|
388
444
|
out.push(`- ${t.quest}: ${pick(p.microQuest, lang)}`);
|
|
389
445
|
out.push(`- ${t.victory}: ${pick(p.victory, lang)}`);
|
|
390
446
|
}
|
|
@@ -395,7 +451,7 @@ export function renderMarkdown(model, language = 'en') {
|
|
|
395
451
|
out.push(`- **${t.ifthen}:** ${pick(model.ifThen, lang)}`, '');
|
|
396
452
|
|
|
397
453
|
out.push(`## ${t.baiHoc}`);
|
|
398
|
-
out.push(`-
|
|
454
|
+
out.push(`- ${model.lesson.skill ? joinDetail(`\`${model.lesson.skill}\``, pick(model.lesson.technique, lang), t) : pick(model.lesson.technique, lang)}`, '');
|
|
399
455
|
}
|
|
400
456
|
|
|
401
457
|
out.push(`## ${t.phuongPhap}`);
|