tillerhand 3.47.1 → 3.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.claude/agents/board-chair.md +9 -9
  2. package/.claude/agents/code-reviewer.md +2 -2
  3. package/.claude/agents/council-advisor.md +2 -2
  4. package/.claude/agents/security-auditor.md +6 -6
  5. package/.claude/agents/test-writer.md +1 -1
  6. package/.claude/board/board-ai.md +6 -6
  7. package/.claude/board/board-api.md +6 -6
  8. package/.claude/board/board-architect.md +7 -7
  9. package/.claude/board/board-devops.md +6 -6
  10. package/.claude/board/board-mobile.md +6 -6
  11. package/.claude/board/board-performance.md +6 -6
  12. package/.claude/board/board-privacy.md +6 -6
  13. package/.claude/board/board-product.md +7 -7
  14. package/.claude/board/board-qa.md +7 -7
  15. package/.claude/board/board-ux.md +6 -6
  16. package/.claude/board/council.md +32 -32
  17. package/.claude/board/rules.md +8 -8
  18. package/.claude/commands/adopt.md +10 -10
  19. package/.claude/commands/ask-mode.md +6 -5
  20. package/.claude/commands/board.md +1 -1
  21. package/.claude/commands/build.md +8 -8
  22. package/.claude/commands/design.md +8 -8
  23. package/.claude/commands/findings-all.md +2 -2
  24. package/.claude/commands/findings-security.md +1 -1
  25. package/.claude/commands/fix-bug.md +2 -2
  26. package/.claude/commands/handoff.md +12 -12
  27. package/.claude/commands/language.md +1 -1
  28. package/.claude/commands/maintain.md +2 -2
  29. package/.claude/commands/plan-task.md +3 -3
  30. package/.claude/commands/qa.md +5 -5
  31. package/.claude/commands/release.md +3 -3
  32. package/.claude/commands/research.md +2 -2
  33. package/.claude/commands/retro.md +2 -2
  34. package/.claude/commands/review-changes.md +2 -2
  35. package/.claude/commands/rollback.md +1 -1
  36. package/.claude/commands/start.md +20 -20
  37. package/.claude/commands/tools.md +8 -6
  38. package/.claude/commands/user-feedback.md +3 -3
  39. package/.claude/commands/wrap-up.md +3 -3
  40. package/.claude/findings/protocol.md +2 -2
  41. package/.claude/findings/report-template.md +1 -1
  42. package/.claude/hooks/commit-gate.mjs +11 -9
  43. package/.claude/hooks/council.mjs +51 -24
  44. package/.claude/hooks/dashboard-data.mjs +73 -53
  45. package/.claude/hooks/dashboard.mjs +80 -71
  46. package/.claude/hooks/destructive-guard.mjs +1 -1
  47. package/.claude/hooks/freeze-guard.mjs +1 -1
  48. package/.claude/hooks/gate.mjs +14 -11
  49. package/.claude/hooks/labels.mjs +177 -0
  50. package/.claude/hooks/messages.mjs +8 -7
  51. package/.claude/hooks/notify.mjs +2 -1
  52. package/.claude/hooks/prefs.mjs +24 -10
  53. package/.claude/hooks/preview.mjs +6 -6
  54. package/.claude/hooks/progress.mjs +27 -15
  55. package/.claude/hooks/qa-report.mjs +12 -10
  56. package/.claude/hooks/session-start.mjs +9 -7
  57. package/.claude/hooks/status-read.mjs +22 -18
  58. package/.claude/hooks/statusline.mjs +10 -8
  59. package/.claude/hooks/tools.mjs +13 -8
  60. package/.claude/output-styles/{anlatici.md → narrator.md} +2 -2
  61. package/.claude/skills/logo-design/SKILL.md +2 -2
  62. package/.claude/skills/source-research/SKILL.md +1 -1
  63. package/.claude/skills/ui-motion/SKILL.md +2 -2
  64. package/AGENTS.md +12 -12
  65. package/CLAUDE.md +1 -1
  66. package/README.md +2 -2
  67. package/README.tr.md +3 -3
  68. package/docs/decisions/adr-template.md +1 -1
  69. package/docs/decisions/log.md +2 -2
  70. package/docs/findings/README.md +1 -1
  71. package/docs/operations/accounts.md +7 -7
  72. package/docs/operations/deploy-setup.md +3 -3
  73. package/docs/operations/feature-flags.md +2 -2
  74. package/docs/operations/maintenance.md +1 -1
  75. package/docs/operations/release-checklist.md +2 -2
  76. package/docs/operations/spend-alarms.md +2 -2
  77. package/docs/product/feedback.md +4 -4
  78. package/docs/product/launch.md +1 -1
  79. package/docs/product/prd.md +10 -10
  80. package/docs/product/roadmap.md +8 -8
  81. package/docs/product/specs/_template.md +8 -8
  82. package/docs/product/unit-economics.md +10 -10
  83. package/docs/quality/metrics.md +2 -2
  84. package/docs/quality/test-plan.md +5 -5
  85. package/docs/security/checklist.md +2 -2
  86. package/docs/workflow/board.md +5 -5
  87. package/docs/workflow/cost-log.md +1 -1
  88. package/docs/workflow/decision-protocol.md +18 -16
  89. package/docs/workflow/learning.md +2 -2
  90. package/docs/workflow/mcp-catalog.md +1 -1
  91. package/docs/workflow/models.md +1 -1
  92. package/docs/workflow/progress.md +14 -14
  93. package/docs/workflow/skills.md +9 -9
  94. package/docs/workflow/usage-limits.md +5 -5
  95. package/kit/CHANGELOG.md +19 -0
  96. package/kit/VERSION +1 -1
  97. package/kit/guide.html +21 -21
  98. package/kit/guide.tr.html +21 -21
  99. package/kit/kit.mjs +29 -0
  100. package/kit/project/README.md +1 -1
  101. package/kit/templates/backend/docs/architecture/stack.md +7 -7
  102. package/kit/templates/backend/docs/architecture/structure.md +2 -2
  103. package/kit/templates/mobile/docs/architecture/stack.md +5 -5
  104. package/kit/templates/mobile/docs/architecture/structure.md +1 -1
  105. package/kit/templates/mobile/docs/design/design-guide.md +1 -1
  106. package/kit/templates/mobile/tests/security/rls.mjs +4 -4
  107. package/kit/templates/web-saas/docs/architecture/stack.md +12 -12
  108. package/kit/templates/web-saas/docs/architecture/structure.md +2 -2
  109. package/kit/templates/web-saas/docs/design/design-guide.md +1 -1
  110. package/kit/templates/web-saas/tests/security/rls.mjs +4 -4
  111. package/package.json +1 -1
@@ -7,9 +7,11 @@ import fs from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import { execFileSync } from 'node:child_process';
9
9
  import { readStatus, readText, sections, findSection, tally, tableRows } from './status-read.mjs';
10
+ import { verdict, isValue } from './labels.mjs';
10
11
  import { readLimits, summary, level, THRESHOLDS } from './usage.mjs';
11
12
  import { packageStatus, commandMode } from './tools.mjs';
12
13
  import { readModels, modelsFile, modelsNote, roleLabel } from './models.mjs';
14
+ import { OLD_SIZES } from './progress.mjs';
13
15
  import { readLang, t } from './lang.mjs';
14
16
  import { acceptanceStatus, accounts, flags, unitEconomics, feedback, learning, qaSummary, metrics, githubStatus, readContext, findingReports, FINDING_KINDS, producedAssets, costSummary, councilMinutes, spendAlarms, envWarnings, repeatedBugClasses } from './dashboard-data.mjs';
15
17
 
@@ -21,7 +23,7 @@ const root = path.resolve(args.find((a) => !a.startsWith('--')) ?? process.env.C
21
23
  const file = (f) => path.join(root, f);
22
24
  // Every visible text comes from messages.mjs in the project language (L = t with this project's root)
23
25
  const L = (key, vars) => t(root, key, vars);
24
- const dateKind = (k) => (k === 'bitiş' ? L('dashboard.dateExpiry') : L('dashboard.dateKeyRotation'));
26
+ const dateKind = (k) => (k === 'expiry' ? L('dashboard.dateExpiry') : L('dashboard.dateKeyRotation'));
25
27
  const kindName = (k) => (Object.hasOwn(FINDING_KINDS, k) ? L(`dashboard.findingKind.${k}`) : k);
26
28
  // The dashboard is made in the user's language: none before /start or /adopt asks it
27
29
  const lang = readLang(root);
@@ -63,21 +65,21 @@ const firstSentence = (b) => (b ?? '').split('\n').map((s) => s.trim()).find((s)
63
65
  const mvp = findSection(prd, '5. MVP').split(/^### /m).slice(1).map((p) => {
64
66
  const [title, ...body] = p.split('\n');
65
67
  const text = body.join('\n');
66
- return { title: title.trim(), priority: text.match(/\*\*Öncelik:\*\*\s*(.+)/)?.[1]?.trim() ?? '', ...tally(text) };
68
+ return { title: title.trim(), priority: text.match(/\*\*Priority:\*\*\s*(.+)/)?.[1]?.trim() ?? '', ...tally(text) };
67
69
  }).filter((o) => !o.title.includes('[') && o.title !== '...');
68
- const outOfScope = findSection(prd, '6. Kapsam dışı').split('\n').map((s) => s.trim()).filter((s) => /^- \S/.test(s) && !s.includes('[')).map((s) => s.slice(2));
70
+ const outOfScope = findSection(prd, '6. Out of scope').split('\n').map((s) => s.trim()).filter((s) => /^- \S/.test(s) && !s.includes('[')).map((s) => s.slice(2));
69
71
 
70
- const stack = tableRows(sections(readText(file('docs/architecture/stack.md')))['Seçilen teknolojiler'], 2).map(([layer, choice]) => ({ layer, choice }));
71
- const criticalFlows = tableRows(findSection(sections(readText(file('docs/quality/test-plan.md'))), '1. Kritik'), 6)
72
+ const stack = tableRows(sections(readText(file('docs/architecture/stack.md')))['Chosen technologies'], 2).map(([layer, choice]) => ({ layer, choice }));
73
+ const criticalFlows = tableRows(findSection(sections(readText(file('docs/quality/test-plan.md'))), '1. Critical'), 6)
72
74
  .map(([no, feed, , kind, fileName, state]) => ({ no, feed, kind, fileName, state }));
73
75
  const cost = tableRows(readText(file('docs/workflow/cost-log.md')), 6)
74
76
  .map(([date, job, size, startPct, endPct, diff]) => ({ date, job, size, diff: parseFloat(String(diff).replace('%', '').replace(',', '.')) }));
75
- const maintenance = tableRows(sections(readText(file('docs/operations/maintenance.md')))['Bakım kaydı'], 4)
77
+ const maintenance = tableRows(sections(readText(file('docs/operations/maintenance.md')))['Maintenance log'], 4)
76
78
  .map(([date, kind, findings, work]) => ({ date, kind, findings, work }));
77
- const unreleased = findSection(sections(readText(file('CHANGELOG.md'))), '[Yayınlanmamış]')
79
+ const unreleased = findSection(sections(readText(file('CHANGELOG.md'))), '[Unreleased]')
78
80
  .split('\n').map((s) => s.trim()).filter((s) => /^- \S/.test(s)).map((s) => s.slice(2));
79
81
  const lastRetro = Object.keys(sections(readText(file('docs/quality/retros.md')))).filter((b) => /^\d{4}-/.test(b)).pop() ?? '';
80
- const designDirection = tableRows(findSection(sections(readText(file('docs/design/design-guide.md'))), '1. Seçilen'), 2)
82
+ const designDirection = tableRows(findSection(sections(readText(file('docs/design/design-guide.md'))), '1. Chosen'), 2)
81
83
  .filter(([, choice]) => choice && !choice.startsWith('[')).map(([area, choice]) => ({ area, choice }));
82
84
 
83
85
  const assets = producedAssets(root);
@@ -114,8 +116,8 @@ const findings = findingReports(readText(file('docs/findings/README.md')));
114
116
  const alerts = [];
115
117
  const addAlert = (sev, text, action = '') => alerts.push({ sev, text, action });
116
118
  if (d.kitPending) addAlert('info', L('dashboard.alert.kitPending'), L('dashboard.act.startIdea'));
117
- if (d.handoffPending) addAlert('warn', L('dashboard.alert.handoffPending'), '/handoff devral');
118
- const lastReject = [...d.board.meetings].reverse().find((t) => /RET/i.test(t.result) && !/KOŞULLU/i.test(t.result));
119
+ if (d.handoffPending) addAlert('warn', L('dashboard.alert.handoffPending'), '/handoff take');
120
+ const lastReject = [...d.board.meetings].reverse().find((t) => verdict(t.result) === 'reject');
119
121
  if (lastReject && !lastReject.decision) addAlert('critical', L('dashboard.alert.boardReject', { kind: lastReject.kind, scope: lastReject.scope, critical: lastReject.critical || L('dashboard.criticalFinding') }), L('dashboard.act.askClaudeFix'));
120
122
  const limitLevel = level(limit);
121
123
  const limitShort = limit ? [[L('dashboard.limit5h'), limit.five_hour], [L('dashboard.limitWeeklyLower'), limit.seven_day]].filter(([, p]) => p)
@@ -128,9 +130,9 @@ for (const s of d.issues.slice(0, 5)) addAlert('warn', L('dashboard.alert.openIs
128
130
  for (const r of repeatedBugClasses(root)) addAlert('info', L('dashboard.alert.repeatedBug', { count: r.specs.length, cls: r.cls, specs: r.specs.join(', ') }), '/retro');
129
131
  if (git?.changed.length > 15) addAlert('info', L('dashboard.alert.uncommitted', { count: git.changed.length }), L('dashboard.act.commitOrWrapUp'));
130
132
  if (git?.progressStale >= 3) addAlert('info', L('dashboard.alert.progressStale', { count: git.progressStale }), '/wrap-up');
131
- const off = (a['Soru modu'] ?? '').match(/kapalı konular:\s*(.+)$/i)?.[1];
132
- if (off && !/^yok$/i.test(off)) addAlert('info', L('dashboard.alert.closedTopics', { topics: off }), L('dashboard.act.askModeOpen'));
133
- if (d.installed && d.board.state === 'kurulmadı' && !/başlangıç/i.test(a['Aktif faz'] ?? '')) addAlert('info', L('dashboard.alert.boardNotSetUp'), '/board setup');
133
+ const off = (a['Question mode'] ?? '').match(/(?:kapalı konular|closed topics):\s*(.+)$/i)?.[1];
134
+ if (off && !isValue(off, 'none')) addAlert('info', L('dashboard.alert.closedTopics', { topics: off }), L('dashboard.act.askModeOpen'));
135
+ if (d.installed && isValue(d.board.state, 'notSetUp') && !isValue(a['Active phase'], 'setup')) addAlert('info', L('dashboard.alert.boardNotSetUp'), '/board setup');
134
136
  if (github?.liveMonitoring?.length) addAlert('critical', L('dashboard.alert.liveMonitoring', { title: github.liveMonitoring[0].title }), L('dashboard.act.fixLive', { number: github.liveMonitoring[0].number }));
135
137
  else if (github?.liveRun?.conclusion === 'failure') addAlert('critical', L('dashboard.alert.liveRunFailed'), L('dashboard.act.fixLiveShort'));
136
138
  const lastMain = github?.runs?.find((c) => /^(main|master)$/.test(c.headBranch) && c.workflowName === 'CI');
@@ -150,8 +152,8 @@ if (fb.newCount) addAlert('info', L('dashboard.alert.newFeedback', { count: fb.n
150
152
  const enabledPackages = (() => { try { return packageStatus(root).filter((p) => p.enabled).map((p) => p.name); } catch { return []; } })();
151
153
  if (enabledPackages.length) addAlert('info', L('dashboard.alert.packagesOn', { names: enabledPackages.join(', ') }), L('dashboard.act.packagesOff'));
152
154
  const cmdMode = (() => { try { return commandMode(root).mode; } catch { return 'light'; } })();
153
- if (cmdMode !== 'full' && /^Tam\b/.test(a['Profil'] ?? '')) addAlert('info', L('dashboard.alert.cmdListLight'), L('dashboard.act.cmdFull'));
154
- else if (cmdMode !== 'light' && !/^Tam\b/.test(a['Profil'] ?? '')) addAlert('info', L('dashboard.alert.cmdListHeavy', { mode: cmdMode === 'full' ? L('dashboard.cmdModeFull') : L('dashboard.cmdModeMixed') }), L('dashboard.act.cmdLight'));
155
+ if (cmdMode !== 'full' && isValue(a['Profile'], 'full')) addAlert('info', L('dashboard.alert.cmdListLight'), L('dashboard.act.cmdFull'));
156
+ else if (cmdMode !== 'light' && !isValue(a['Profile'], 'full')) addAlert('info', L('dashboard.alert.cmdListHeavy', { mode: cmdMode === 'full' ? L('dashboard.cmdModeFull') : L('dashboard.cmdModeMixed') }), L('dashboard.act.cmdLight'));
155
157
  if (assets.codeMapDays > 30) addAlert('info', L('dashboard.alert.codeMapStale', { days: assets.codeMapDays }), L('dashboard.act.understand'));
156
158
  if (context?.percent >= 70 && Date.now() - context.readAt < 3600e3) addAlert('warn', L('dashboard.alert.contextFull', { percent: context.percent }), L('dashboard.act.wrapUpClear'));
157
159
  for (const r of Object.values(findings.latest)) if (r.critical > 0) addAlert('warn', L('dashboard.alert.findingsCritical', { kind: kindName(r.kind), critical: r.critical, health: r.health }), `docs/findings/${r.kind}/latest.md`);
@@ -174,17 +176,17 @@ const fixRatio = (kindCounts.feat ?? 0) + (kindCounts.fix ?? 0) ? Math.round(((k
174
176
 
175
177
  const feed = [
176
178
  ...(git?.commits ?? []).slice(0, 60).map((c) => ({ time: c.date, kind: 'commit', title: c.message, extra: c.hash })),
177
- ...d.decisions.map((k) => ({ time: k.date, kind: 'karar', title: `${k.question} → ${k.chosen}`, extra: `${k.topic} · ${k.decidedBy}` })),
178
- ...d.board.meetings.map((t) => ({ time: t.date, kind: 'kurul', title: `${t.kind}: ${t.scope} → ${t.result}`, extra: t.attendees })),
179
- ...maintenance.map((b) => ({ time: b.date, kind: 'bakım', title: `${b.kind}: ${b.findings}`, extra: b.work })),
179
+ ...d.decisions.map((k) => ({ time: k.date, kind: 'decision', title: `${k.question} → ${k.chosen}`, extra: `${k.topic} · ${k.decidedBy}` })),
180
+ ...d.board.meetings.map((t) => ({ time: t.date, kind: 'board', title: `${t.kind}: ${t.scope} → ${t.result}`, extra: t.attendees })),
181
+ ...maintenance.map((b) => ({ time: b.date, kind: 'maintenance', title: `${b.kind}: ${b.findings}`, extra: b.work })),
180
182
  ].filter((x) => x.time && !Number.isNaN(toDate(x.time).getTime()))
181
183
  .sort((x, y) => toDate(y.time) - toDate(x.time)).slice(0, 50);
182
184
 
183
- const autoDecisions = d.decisions.filter((k) => /otomatik/i.test(k.decidedBy)).length;
184
- const rejectCount = d.board.meetings.filter((t) => /RET/.test(t.result) && !/KOŞULLU/.test(t.result)).length;
185
+ const autoDecisions = d.decisions.filter((k) => isValue(k.decidedBy, 'automatic')).length;
186
+ const rejectCount = d.board.meetings.filter((t) => verdict(t.result) === 'reject').length;
185
187
  const mvpCriteria = mvp.reduce((t, o) => ({ done: t.done + o.done, total: t.total + o.total }), { done: 0, total: 0 });
186
188
  const avgCost = Object.entries(cost.reduce((m, r) => {
187
- if (!Number.isNaN(r.diff)) (m[r.size] ??= []).push(r.diff);
189
+ if (!Number.isNaN(r.diff)) (m[OLD_SIZES[r.size] ?? r.size] ??= []).push(r.diff);
188
190
  return m;
189
191
  }, Object.create(null))).map(([size, l]) => ({ size, avg: l.reduce((t, x) => t + x, 0) / l.length, n: l.length }));
190
192
 
@@ -195,26 +197,33 @@ const percent = (b, t) => (t ? Math.round((b / t) * 100) : 0);
195
197
  const progressBar = (b, t, cls = '') => `<div class="bar ${cls}" role="img" aria-label="${b}/${t}"><span style="width:${percent(b, t)}%"></span></div>`;
196
198
  const empty = (text, action = '') => `<p class="empty">${md(text)}${action ? ` <code>${e(action)}</code>` : ''}</p>`;
197
199
  const SEV = { critical: ['⛔', 'dashboard.sevCritical'], warn: ['⚠', 'dashboard.sevWarn'], info: ['ℹ', 'dashboard.sevInfo'], good: ['✓', 'dashboard.sevGood'] };
198
- const FEED_KEYS = { commit: 'dashboard.feedCommit', karar: 'dashboard.feedDecision', kurul: 'dashboard.feedBoard', 'bakım': 'dashboard.feedMaintenance' };
200
+ const FEED_KEYS = { commit: 'dashboard.feedCommit', decision: 'dashboard.feedDecision', board: 'dashboard.feedBoard', maintenance: 'dashboard.feedMaintenance' };
199
201
  const feedLabel = (k) => (Object.hasOwn(FEED_KEYS, k) ? L(FEED_KEYS[k]) : k);
200
- const onOff = (v) => (v === 'açık' ? L('dashboard.on') : v === 'kapalı' ? L('dashboard.off') : v);
202
+ const onOff = (v) => (isValue(v, 'on') ? L('dashboard.on') : isValue(v, 'off') ? L('dashboard.off') : v);
201
203
  // A text for the page's own script: a single-quoted JS string, no tag can end the script block
202
204
  const jsStr = (key) => `'${L(key).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/</g, '\\u003c')}'`;
205
+ // A board verdict in either language; an option name (decision council) or an unreadable result is neutral
206
+ const verdictBadge = (k) => badge({ approve: 'good', conditional: 'warn', reject: 'critical' }[verdict(k)] ?? 'info', k);
203
207
  const badge = (sev, caption) => `<span class="badge ${sev}"><span class="ic" aria-hidden="true">${SEV[sev][0]}</span> ${e(caption ?? L(SEV[sev][1]))}</span>`;
204
208
  const fmtDate = (t) => { const z = toDate(t); return Number.isNaN(z.getTime()) ? e(t) : z.toLocaleString(loc, { day: '2-digit', month: 'short', hour: t.length > 10 ? '2-digit' : undefined, minute: t.length > 10 ? '2-digit' : undefined }); };
205
209
  const limitSeverity = (p, k) => (p == null ? 'info' : p >= THRESHOLDS[k].red ? 'critical' : p >= THRESHOLDS[k].yellow ? 'warn' : 'good');
206
- const TOPICS = ['urun', 'teknik', 'tasarim', 'icerik', 'veri', 'guvenlik', 'maliyet'];
210
+ const TOPICS = ['product', 'tech', 'design', 'content', 'data', 'security', 'cost'];
211
+ // Decision log topics before 3.48, in the same order; they count under the English topic
212
+ const TOPICS_TR = ['urun', 'teknik', 'tasarim', 'icerik', 'veri', 'guvenlik', 'maliyet'];
213
+ const topicKey = (topic = '') => { const k = topic.trim().toLowerCase(); return TOPICS[TOPICS_TR.indexOf(k)] ?? k; };
207
214
 
208
215
  // The command to paste into Claude: "`/build G-013` …" or "/wrap-up ve /clear" → first command, without its <placeholder> or note
209
216
  const cmdOf = (s = '') => { const m = String(s).match(/`(\/[^`\n]+)`/) ?? String(s).match(/^(\/.*)$/); return m ? m[1].split(/ ve | ile | and | to |\s*\(/)[0].replace(/\s*[[<].*$/, '').trim() : ''; };
210
- const copyBtn = (s) => { const c = cmdOf(s); return c ? ` <button class="kopyala" type="button" data-cmd="${e(c)}" aria-label="${e(L('dashboard.copyAria', { command: c }))}">${e(L('dashboard.copy'))}</button>` : ''; };
211
- const alertRow = (x) => `<li class="${x.sev}"><span class="ic" aria-hidden="true">${SEV[x.sev][0]}</span><span><span class="sr-only">${L(SEV[x.sev][1])}: </span>${md(x.text)}</span>${x.action ? `<span class="eylem">→ ${md(x.action)}${copyBtn(x.action)}</span>` : ''}</li>`;
217
+ const copyBtn = (s) => { const c = cmdOf(s); return c ? ` <button class="copy-cmd" type="button" data-cmd="${e(c)}" aria-label="${e(L('dashboard.copyAria', { command: c }))}">${e(L('dashboard.copy'))}</button>` : ''; };
218
+ const alertRow = (x) => `<li class="${x.sev}"><span class="ic" aria-hidden="true">${SEV[x.sev][0]}</span><span><span class="sr-only">${L(SEV[x.sev][1])}: </span>${md(x.text)}</span>${x.action ? `<span class="action">→ ${md(x.action)}${copyBtn(x.action)}</span>` : ''}</li>`;
212
219
  const kpi = (label, value, sub = '', bar = '') => `<div class="kpi"><div class="kpi-l">${e(label)}</div><div class="kpi-v">${value}</div>${bar}<div class="kpi-s">${sub}</div></div>`;
213
220
 
214
221
  // ---------- sections ----------
215
- const activeIndex = Math.max(0, [{ name: 'Başlangıç' }, ...d.phases].findIndex((f) => (a['Aktif faz'] ?? '').toLowerCase().startsWith(f.name.toLowerCase().split(' ').slice(0, 2).join(' '))));
216
- const start = (a['Başlangıç adımı'] ?? '0/9').split('/').map(Number);
217
- const stageStrip = `<ol class="steps">${[{ name: 'Başlangıç', done: start[0] || 0, total: start[1] || 9 }, ...d.phases].map((f, i) =>
222
+ // A pre-3.48 value says "Faz 1"; the roadmap headings are read as "Phase 1"
223
+ const activePhase = (a['Active phase'] ?? '').replace(/^faz\b/i, 'Phase').replace(/^başlangıç(?![\p{L}\p{N}])/iu, 'Setup');
224
+ const activeIndex = Math.max(0, [{ name: 'Setup' }, ...d.phases].findIndex((f) => activePhase.toLowerCase().startsWith(f.name.toLowerCase().split(' ').slice(0, 2).join(' '))));
225
+ const start = (a['Setup step'] ?? '0/9').split('/').map(Number);
226
+ const stageStrip = `<ol class="steps">${[{ name: 'Setup', done: start[0] || 0, total: start[1] || 9 }, ...d.phases].map((f, i) =>
218
227
  `<li class="step${i === activeIndex ? ' now' : ''}${f.total && f.done === f.total ? ' done' : ''}"><span class="step-n">${i === 0 ? e(L('dashboard.stageStartLetter')) : i - 1}</span><div class="step-b"><b>${e(i === 0 ? L('dashboard.stageStart') : f.name.replace(/\s*\(.*\)$/, ''))}</b>${progressBar(f.done, f.total)}<span class="muted num">${f.done}/${f.total}</span></div></li>`).join('')}</ol>`;
219
228
 
220
229
  const overview = `
@@ -226,36 +235,36 @@ const overview = `
226
235
  <section class="card"><h3>${L('dashboard.stages')}</h3>${stageStrip}</section>`;
227
236
 
228
237
  function feedRow(x) {
229
- return `<li class="feed-i t-${x.kind.replace('ı', 'i')}"><span class="ft">${e(feedLabel(x.kind))}</span><div><div>${md(x.title)}</div><div class="muted small">${fmtDate(x.time)}${x.extra ? ' · ' + e(x.extra) : ''}</div></div></li>`;
238
+ return `<li class="feed-i t-${x.kind}"><span class="ft">${e(feedLabel(x.kind))}</span><div><div>${md(x.title)}</div><div class="muted small">${fmtDate(x.time)}${x.extra ? ' · ' + e(x.extra) : ''}</div></div></li>`;
230
239
  }
231
240
 
232
241
  const roadmapSection = d.phases.length ? d.phases.map((f) => `
233
- <details class="card faz" ${f.name.toLowerCase().startsWith((a['Aktif faz'] ?? '#').toLowerCase().split(' ').slice(0, 2).join(' ')) ? 'open' : ''}>
234
- <summary><b>${e(f.name)}</b><span class="faz-bar">${progressBar(f.done, f.total)}<span class="num muted">${f.done}/${f.total}</span></span></summary>
235
- <ul class="tasks">${f.tasks.map((g) => `<li class="${g.done ? 'ok' : ''}${(a['Aktif görev'] && g.text.startsWith(a['Aktif görev'])) ? ' active' : ''}"><span class="chk" aria-hidden="true">${g.done ? '✓' : ''}</span><span>${md(g.text)}</span></li>`).join('')}</ul>
242
+ <details class="card phase" ${f.name.toLowerCase().startsWith((activePhase || '#').toLowerCase().split(' ').slice(0, 2).join(' ')) ? 'open' : ''}>
243
+ <summary><b>${e(f.name)}</b><span class="phase-bar">${progressBar(f.done, f.total)}<span class="num muted">${f.done}/${f.total}</span></span></summary>
244
+ <ul class="tasks">${f.tasks.map((g) => `<li class="${g.done ? 'ok' : ''}${(a['Active task'] && g.text.startsWith(a['Active task'])) ? ' active' : ''}"><span class="chk" aria-hidden="true">${g.done ? '✓' : ''}</span><span>${md(g.text)}</span></li>`).join('')}</ul>
236
245
  </details>`).join('') : empty(L('dashboard.noRoadmap'));
237
246
 
238
247
  const decisionTable = d.decisions.length ? `
239
- <div class="filters" role="group" aria-label="${e(L('dashboard.topicFilter'))}"><button class="chip on" data-k="hepsi">${L('dashboard.all')} (${d.decisions.length})</button>${TOPICS.filter((k) => d.decisions.some((x) => x.topic === k)).map((k) => `<button class="chip" data-k="${k}">${k} (${d.decisions.filter((x) => x.topic === k).length})</button>`).join('')}<button class="chip" data-k="otomatik">${L('dashboard.auto')} (${autoDecisions})</button><button class="chip" data-k="tek">${L('dashboard.oneWay')} (${d.decisions.filter((k) => /^tek/i.test(k.door)).length})</button></div>
240
- <div class="tw"><table id="kararlar"><thead><tr><th>#</th><th>${L('dashboard.th.date')}</th><th>${L('dashboard.th.stage')}</th><th>${L('dashboard.th.topic')}</th><th>${L('dashboard.th.question')}</th><th>${L('dashboard.th.chosen')}</th><th>${L('dashboard.th.door')}</th><th>${L('dashboard.th.decidedBy')}</th></tr></thead><tbody>
241
- ${[...d.decisions].reverse().map((k) => `<tr data-k="${e(k.topic)}" data-v="${/otomatik/i.test(k.decidedBy) ? 'otomatik' : ''}" data-kapi="${/^tek/i.test(k.door) ? 'tek' : ''}"><td class="num">${e(k.no)}</td><td class="num nw">${e(k.date)}</td><td>${e(k.stage)}</td><td><span class="tag">${e(k.topic)}</span></td><td>${md(k.question)}</td><td><b>${md(k.chosen)}</b></td><td>${/^tek/i.test(k.door) ? badge('warn', L('dashboard.oneWay')) : k.door ? `<span class="muted small">${e(k.door)}</span>` : ''}</td><td>${/otomatik/i.test(k.decidedBy) ? badge('warn', L('dashboard.auto')) : badge('good', L('dashboard.user'))}</td></tr>`).join('')}
248
+ <div class="filters" role="group" aria-label="${e(L('dashboard.topicFilter'))}"><button class="chip on" data-k="all">${L('dashboard.all')} (${d.decisions.length})</button>${TOPICS.filter((k) => d.decisions.some((x) => topicKey(x.topic) === k)).map((k) => `<button class="chip" data-k="${k}">${k} (${d.decisions.filter((x) => topicKey(x.topic) === k).length})</button>`).join('')}<button class="chip" data-k="automatic">${L('dashboard.auto')} (${autoDecisions})</button><button class="chip" data-k="one-way">${L('dashboard.oneWay')} (${d.decisions.filter((k) => isValue(k.door, 'oneWay')).length})</button></div>
249
+ <div class="tw"><table id="decision-table"><thead><tr><th>#</th><th>${L('dashboard.th.date')}</th><th>${L('dashboard.th.stage')}</th><th>${L('dashboard.th.topic')}</th><th>${L('dashboard.th.question')}</th><th>${L('dashboard.th.chosen')}</th><th>${L('dashboard.th.door')}</th><th>${L('dashboard.th.decidedBy')}</th></tr></thead><tbody>
250
+ ${[...d.decisions].reverse().map((k) => `<tr data-k="${e(topicKey(k.topic))}" data-v="${isValue(k.decidedBy, 'automatic') ? 'automatic' : ''}" data-door="${isValue(k.door, 'oneWay') ? 'one-way' : ''}"><td class="num">${e(k.no)}</td><td class="num nw">${e(k.date)}</td><td>${e(k.stage)}</td><td><span class="tag">${e(k.topic)}</span></td><td>${md(k.question)}</td><td><b>${md(k.chosen)}</b></td><td>${isValue(k.door, 'oneWay') ? badge('warn', L('dashboard.oneWay')) : k.door ? `<span class="muted small">${e(k.door)}</span>` : ''}</td><td>${isValue(k.decidedBy, 'automatic') ? badge('warn', L('dashboard.auto')) : badge('good', L('dashboard.user'))}</td></tr>`).join('')}
242
251
  </tbody></table></div>` : empty(L('dashboard.noDecisions'));
243
252
 
244
253
  const boardSection = `
245
254
  <div class="grid2">
246
- <section class="card"><h3>${L('dashboard.members')} · ${e(d.board.state === 'kuruldu' ? L('dashboard.boardSetUp') : d.board.state === 'kurulmadı' ? L('dashboard.notSetUp') : d.board.state)}</h3>${d.board.members.length ? `<ul class="members">${d.board.members.map((u) => `<li><b>${e(u.member)}</b><span class="muted small">${e(u.model)} · ${e(u.focus)}</span></li>`).join('')}</ul>` : empty(L('dashboard.boardNotSetUp'), '/board setup')}
255
+ <section class="card"><h3>${L('dashboard.members')} · ${e(isValue(d.board.state, 'setUp') ? L('dashboard.boardSetUp') : isValue(d.board.state, 'notSetUp') ? L('dashboard.notSetUp') : d.board.state)}</h3>${d.board.members.length ? `<ul class="members">${d.board.members.map((u) => `<li><b>${e(u.member)}</b><span class="muted small">${e(u.model)} · ${e(u.focus)}</span></li>`).join('')}</ul>` : empty(L('dashboard.boardNotSetUp'), '/board setup')}
247
256
  <p class="muted small">${L('dashboard.commitReview', { value: e(onOff(d.board.commitReview)) })}</p></section>
248
- <section class="card"><h3>${L('dashboard.summary')}</h3><div class="kpis mini">${kpi(L('dashboard.kpiMeetings'), d.board.meetings.length)}${kpi('RET', rejectCount, rejectCount ? badge('critical', L('dashboard.criticalFinding')) : '')}${kpi(L('dashboard.kpiApproval'), d.board.meetings.length ? `%${percent(d.board.meetings.filter((t) => /^ONAY/i.test(t.result)).length, d.board.meetings.length)}` : '-')}</div></section>
257
+ <section class="card"><h3>${L('dashboard.summary')}</h3><div class="kpis mini">${kpi(L('dashboard.kpiMeetings'), d.board.meetings.length)}${kpi(L('dashboard.kpiRejects'), rejectCount, rejectCount ? badge('critical', L('dashboard.criticalFinding')) : '')}${kpi(L('dashboard.kpiApproval'), d.board.meetings.length ? `%${percent(d.board.meetings.filter((t) => verdict(t.result) === 'approve').length, d.board.meetings.length)}` : '-')}</div></section>
249
258
  </div>
250
259
  <section class="card"><h3>${L('dashboard.councilHeading')}</h3>${councils.length ? (() => {
251
260
  const latest = councils[0];
252
- const decisionBadge = (k) => (!k ? badge('info', L('dashboard.noDecision')) : /KOŞULLU/i.test(k) ? badge('warn', k) : /RET/i.test(k) ? badge('critical', k) : badge('good', k));
261
+ const decisionBadge = (k) => (!k ? badge('info', L('dashboard.noDecision')) : verdictBadge(k));
253
262
  return `<h4>${L('dashboard.latestCouncil', { topic: md(latest.topic) })}</h4><div class="kpis mini">${kpi(L('dashboard.th.decision'), decisionBadge(latest.decision))}${kpi(L('dashboard.th.consensus'), latest.consensus == null ? '-' : `%${latest.consensus}`, latest.consensus != null && latest.consensus < 60 ? badge('warn', L('dashboard.low')) : '')}${kpi(L('dashboard.kpiDisputed'), latest.disputed)}${kpi(L('dashboard.kpiEliminated'), latest.eliminated, L('dashboard.byCrossVote'))}</div>
254
263
  ${latest.ranking.length ? `<h4>${L('dashboard.overallRanking')}</h4><div class="rows">${latest.ranking.map((r, i) => `<div class="row"><span><b>${i + 1}.</b> ${L('dashboard.opinion', { letter: e(r.opinion) })}${r.who ? ` <span class="muted small">${e(r.who)}</span>` : ''}</span><span class="num muted">${L('dashboard.avg', { average: e(r.average) })}</span></div>`).join('')}</div>` : ''}
255
264
  <div class="tw"><table><thead><tr><th>${L('dashboard.th.date')}</th><th>${L('dashboard.th.kind')}</th><th>${L('dashboard.th.topic')}</th><th>${L('dashboard.th.decision')}</th><th>${L('dashboard.th.consensus')}</th><th>${L('dashboard.th.minutes')}</th></tr></thead><tbody>${councils.slice(0, 10).map((k) => `<tr><td class="num nw">${e(k.date)}</td><td>${e(k.kind)}</td><td>${md(k.topic)}</td><td>${decisionBadge(k.decision)}</td><td class="num">${k.consensus == null ? '-' : `%${k.consensus}`}</td><td><a href="${k.file.split('/').map(encodeURIComponent).join('/')}" target="_blank" rel="noopener">${L('dashboard.open')}</a></td></tr>`).join('')}</tbody></table></div>`;
256
- })() : empty(L('dashboard.noCouncil'), '/board council <soru>')}</section>
265
+ })() : empty(L('dashboard.noCouncil'), '/board council <question>')}</section>
257
266
  <section class="card"><h3>${L('dashboard.meetingLog')}</h3>${d.board.meetings.length ? `<div class="tw"><table><thead><tr><th>#</th><th>${L('dashboard.th.date')}</th><th>${L('dashboard.th.kind')}</th><th>${L('dashboard.th.scope')}</th><th>${L('dashboard.th.attendees')}</th><th>${L('dashboard.th.result')}</th><th>${L('dashboard.th.critical')}</th><th>${L('dashboard.th.userDecision')}</th></tr></thead><tbody>
258
- ${[...d.board.meetings].reverse().map((t) => `<tr><td class="num">${e(t.no)}</td><td class="num nw">${e(t.date)}</td><td>${e(t.kind)}</td><td>${md(t.scope)}</td><td>${e(t.attendees)}</td><td>${/KOŞULLU/i.test(t.result) ? badge('warn', t.result) : /RET/i.test(t.result) ? badge('critical', t.result) : badge('good', t.result)}</td><td>${md(t.critical)}</td><td>${md(t.decision)}</td></tr>`).join('')}
267
+ ${[...d.board.meetings].reverse().map((t) => `<tr><td class="num">${e(t.no)}</td><td class="num nw">${e(t.date)}</td><td>${e(t.kind)}</td><td>${md(t.scope)}</td><td>${e(t.attendees)}</td><td>${verdictBadge(t.result)}</td><td>${md(t.critical)}</td><td>${md(t.decision)}</td></tr>`).join('')}
259
268
  </tbody></table></div>` : empty(L('dashboard.noMeetings'))}</section>`;
260
269
 
261
270
  const quality = `
@@ -270,7 +279,7 @@ ${qa ? `<section class="card"><h3>${L('dashboard.lastQa', { name: e(qa.name) })}
270
279
  <section class="card"><h3>${L('dashboard.acceptanceTests')}</h3>${acceptance.total ? `<div class="kpis mini">${kpi(L('dashboard.th.criterion'), acceptance.total)}${kpi(L('dashboard.kpiTested'), `${acceptance.tested}/${acceptance.total}`, '', progressBar(acceptance.tested, acceptance.total))}${kpi(L('dashboard.kpiPassing'), `${acceptance.passing}/${acceptance.total}`, acceptance.reportTime ? L('dashboard.reportTime', { when: new Date(acceptance.reportTime).toLocaleString(loc, { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) }) : L('dashboard.noJunit'), progressBar(acceptance.passing, acceptance.total, 'good'))}</div>` : empty(L('dashboard.noAcceptanceIds'))}</section>
271
280
  <section class="card"><h3>${L('dashboard.metricsMonthly')}</h3>${metricRecords.length ? `<div class="tw"><table><thead><tr><th>${L('dashboard.th.period')}</th><th>${L('dashboard.th.task')}</th><th>${L('dashboard.th.avgMinutes')}</th><th>${L('dashboard.th.avgLimit')}</th><th>${L('dashboard.th.fixes')}</th><th>${L('dashboard.th.reverts')}</th><th>${L('dashboard.th.duplicateCode')}</th><th>${L('dashboard.th.openCritical')}</th><th>${L('dashboard.th.acceptance')}</th></tr></thead><tbody>${[...metricRecords].reverse().slice(0, 8).map((m) => `<tr><td class="nw">${e(m.period)}</td><td class="num">${e(m.taskCount)}</td><td class="num">${e(m.duration)}</td><td class="num">${e(m.limit)}</td><td class="num">${e(m.fixes)}</td><td class="num">${e(m.reverts)}</td><td class="num">${e(m.duplicates)}</td><td class="num">${e(m.critical)}</td><td class="num">${e(m.acceptance)}</td></tr>`).join('')}</tbody></table></div>` : empty(L('dashboard.noMetrics'))}</section>
272
281
  <section class="card"><h3>${L('dashboard.criticalFlowTests')}</h3>${criticalFlows.length ? `<div class="tw"><table><thead><tr><th>#</th><th>${L('dashboard.th.flow')}</th><th>${L('dashboard.th.testKind')}</th><th>${L('dashboard.th.file')}</th><th>${L('dashboard.status')}</th></tr></thead><tbody>${criticalFlows.map((k) => `<tr><td class="num">${e(k.no)}</td><td>${md(k.feed)}</td><td>${e(k.kind)}</td><td>${md(k.fileName)}</td><td>${/✅|yeşil|var/i.test(k.state) ? badge('good', L('dashboard.present')) : badge('warn', L('dashboard.missing'))}</td></tr>`).join('')}</tbody></table></div>` : empty(L('dashboard.noTestPlan'))}</section>
273
- <section class="card"><h3>${L('dashboard.maintenanceLog')}</h3>${maintenance.length ? `<ul class="feed compact">${[...maintenance].reverse().slice(0, 8).map((b) => feedRow({ time: b.date, kind: 'bakım', title: `${b.kind}: ${b.findings}`, extra: b.work })).join('')}</ul>` : empty(L('dashboard.noMaintenance'), '/maintain weekly')}
282
+ <section class="card"><h3>${L('dashboard.maintenanceLog')}</h3>${maintenance.length ? `<ul class="feed compact">${[...maintenance].reverse().slice(0, 8).map((b) => feedRow({ time: b.date, kind: 'maintenance', title: `${b.kind}: ${b.findings}`, extra: b.work })).join('')}</ul>` : empty(L('dashboard.noMaintenance'), '/maintain weekly')}
274
283
  ${lastRetro ? `<p class="muted small">${L('dashboard.lastRetro', { value: e(lastRetro) })}</p>` : ''}</section>`;
275
284
 
276
285
  const maxDay = Math.max(1, ...Object.values(dayCounts));
@@ -278,7 +287,7 @@ const activity = `
278
287
  <section class="card"><h3>${L('dashboard.commitsLast14')} <span class="muted small">${L('dashboard.totalParen', { total: days.reduce((t, g) => t + dayCounts[g], 0) })}</span></h3>
279
288
  <div class="spark" role="img" aria-label="${e(L('dashboard.sparkAria'))}">${days.map((g) => `<div class="sb" data-tip="${e(L('dashboard.commitTip', { date: toDate(g).toLocaleDateString(loc, { day: 'numeric', month: 'short', weekday: 'short' }), count: dayCounts[g] }))}"><span style="height:${dayCounts[g] ? Math.max(6, (dayCounts[g] / maxDay) * 100) : 0}%"></span></div>`).join('')}</div>
280
289
  <div class="spark-x muted small"><span>${toDate(days[0]).toLocaleDateString(loc, { day: 'numeric', month: 'short' })}</span><span>${L('dashboard.today')}</span></div>
281
- <details class="tablo-gor"><summary class="muted small">${L('dashboard.showTable')}</summary><div class="tw"><table><thead><tr><th>${L('dashboard.th.day')}</th><th>${L('dashboard.th.commit')}</th></tr></thead><tbody>${days.map((g) => `<tr><td class="num">${g}</td><td class="num">${dayCounts[g]}</td></tr>`).join('')}</tbody></table></div></details>
290
+ <details class="table-view"><summary class="muted small">${L('dashboard.showTable')}</summary><div class="tw"><table><thead><tr><th>${L('dashboard.th.day')}</th><th>${L('dashboard.th.commit')}</th></tr></thead><tbody>${days.map((g) => `<tr><td class="num">${g}</td><td class="num">${dayCounts[g]}</td></tr>`).join('')}</tbody></table></div></details>
282
291
  </section>
283
292
  <section class="card"><h3>${L('dashboard.feed')}</h3>${feed.length ? `<ul class="feed">${feed.map(feedRow).join('')}</ul>` : empty(L('dashboard.noActivity'))}</section>`;
284
293
 
@@ -286,7 +295,7 @@ const product = `
286
295
  <div class="grid2">
287
296
  <section class="card"><h3>${L('dashboard.product')}</h3>
288
297
  <h4>${L('dashboard.problem')}</h4>${firstSentence(findSection(prd, '1. Problem')) ? `<p>${md(firstSentence(findSection(prd, '1. Problem')))}</p>` : empty(L('dashboard.noPrd'), '/start')}
289
- <h4>${L('dashboard.valueProp')}</h4>${firstSentence(findSection(prd, '3. Değer')) ? `<p>${md(firstSentence(findSection(prd, '3. Değer')))}</p>` : empty('-')}
298
+ <h4>${L('dashboard.valueProp')}</h4>${firstSentence(findSection(prd, '3. Value')) ? `<p>${md(firstSentence(findSection(prd, '3. Value')))}</p>` : empty('-')}
290
299
  ${outOfScope.length ? `<h4>${L('dashboard.outOfScope')}</h4><ul>${outOfScope.map((k) => `<li>${md(k)}</li>`).join('')}</ul>` : ''}</section>
291
300
  <section class="card"><h3>${L('dashboard.mvpFeatures')}</h3>${mvp.length ? `<div class="rows">${mvp.map((o) => `<div class="row"><span>${md(o.title)} ${o.priority ? `<span class="tag">${e(o.priority)}</span>` : ''}</span>${progressBar(o.done, o.total)}<span class="num muted">${o.done}/${o.total}</span></div>`).join('')}</div>` : empty(L('dashboard.noMvp'))}
292
301
  ${acceptance.total ? `<h4>${L('dashboard.criteriaResults')}</h4><div class="tw"><table><thead><tr><th>${L('dashboard.th.criterion')}</th><th>PRD</th><th>${L('dashboard.th.test')}</th></tr></thead><tbody>${acceptance.list.map((k) => `<tr><td><code>${e(k.id)}</code> ${md(k.text)}</td><td>${k.checked ? '✓' : ''}</td><td>${k.result === 'passed' ? badge('good', L('dashboard.passedBadge')) : k.result === 'failed' ? badge('critical', L('dashboard.failedBadge')) : k.test ? badge('info', L('dashboard.noResult')) : badge('warn', L('dashboard.noTest'))}</td></tr>`).join('')}</tbody></table></div>` : ''}</section>
@@ -298,7 +307,7 @@ const product = `
298
307
  </div>
299
308
  <div class="grid2">
300
309
  <section class="card"><h3>${L('dashboard.unitEconomics')}</h3>${economics ? `<div class="kpis mini">${kpi(L('dashboard.kpiMargin'), `${economics.margin.toFixed(1)} ${e(economics.currency)}`, L('dashboard.perPayingUserMonth'))}${kpi(L('dashboard.kpiBreakEven'), economics.breakEven ?? '—', economics.breakEven ? L('dashboard.payingUsers') : L('dashboard.negativeMargin'))}${kpi(L('dashboard.atTarget', { target: economics.target }), `${economics.profit >= 0 ? '+' : ''}${Math.round(economics.profit)} ${e(economics.currency)}`, L('dashboard.monthlyProfitLoss'))}${economics.variableRatio != null ? kpi(L('dashboard.variableOverPrice'), `%${Math.round(economics.variableRatio)}`, economics.variableRatio > 30 ? badge('warn', L('dashboard.over30')) : '') : ''}</div>` : empty(L('dashboard.noInputs'), 'docs/product/unit-economics.md')}</section>
301
- <section class="card"><h3>${L('dashboard.userFeedback')}</h3>${fb.records.length ? `<div class="kpis mini">${kpi(L('dashboard.kpiTotal'), fb.records.length)}${kpi(L('dashboard.kpiNew'), fb.newCount)}${kpi(L('dashboard.kpiPlanned'), fb.records.filter((k) => /planland/i.test(k.state)).length)}</div>${fb.themes.length ? `<h4>${L('dashboard.topThemes')}</h4><div class="rows">${fb.themes.slice(0, 5).map((t) => `<div class="row"><span>${md(t.theme)}</span>${progressBar(t.count, fb.themes[0].count)}<span class="num muted">${t.count}</span></div>`).join('')}</div>` : ''}` : empty(L('dashboard.noFeedback'), '/user-feedback')}</section>
310
+ <section class="card"><h3>${L('dashboard.userFeedback')}</h3>${fb.records.length ? `<div class="kpis mini">${kpi(L('dashboard.kpiTotal'), fb.records.length)}${kpi(L('dashboard.kpiNew'), fb.newCount)}${kpi(L('dashboard.kpiPlanned'), fb.records.filter((k) => k.state === 'planned').length)}</div>${fb.themes.length ? `<h4>${L('dashboard.topThemes')}</h4><div class="rows">${fb.themes.slice(0, 5).map((t) => `<div class="row"><span>${md(t.theme)}</span>${progressBar(t.count, fb.themes[0].count)}<span class="num muted">${t.count}</span></div>`).join('')}</div>` : ''}` : empty(L('dashboard.noFeedback'), '/user-feedback')}</section>
302
311
  </div>
303
312
  ${learnt ? `<section class="card"><h3>${L('dashboard.learning')}</h3><div class="kpis mini">${kpi(L('dashboard.kpiRight'), learnt.right)}${kpi(L('dashboard.kpiWrong'), learnt.wrong)}${kpi(L('dashboard.kpiSuccess'), `%${percent(learnt.right, learnt.right + learnt.wrong)}`)}</div>${learnt.missed.length ? `<h4>${L('dashboard.oftenMissed')}</h4><div class="rows">${learnt.missed.slice(0, 5).map((m) => `<div class="row"><span>${md(m.concept)}</span>${progressBar(m.count, learnt.missed[0].count)}<span class="num muted">${m.count}</span></div>`).join('')}</div>` : ''}</section>` : ''}
304
313
  <div class="grid2">
@@ -354,9 +363,9 @@ const findingsSection = `
354
363
  <p class="muted small">${L('dashboard.scoringNote')}</p></section>`;
355
364
 
356
365
  const TABS = [
357
- ['genel', L('dashboard.tabOverview'), overview], ['yol', L('dashboard.tabRoadmap'), roadmapSection], ['kararlar', L('dashboard.tabDecisions'), decisionTable],
358
- ['kurul', L('dashboard.board'), boardSection], ['bulgular', L('dashboard.tabFindings'), findingsSection], ['kalite', L('dashboard.tabQuality'), quality], ['aktivite', L('dashboard.tabActivity'), activity],
359
- ['urun', L('dashboard.tabProduct'), product], ['operasyon', L('dashboard.tabOperations'), operations], ['limit', L('dashboard.tabLimit'), limitSection],
366
+ ['overview', L('dashboard.tabOverview'), overview], ['roadmap', L('dashboard.tabRoadmap'), roadmapSection], ['decisions', L('dashboard.tabDecisions'), decisionTable],
367
+ ['board', L('dashboard.board'), boardSection], ['findings', L('dashboard.tabFindings'), findingsSection], ['quality', L('dashboard.tabQuality'), quality], ['activity', L('dashboard.tabActivity'), activity],
368
+ ['product', L('dashboard.tabProduct'), product], ['operations', L('dashboard.tabOperations'), operations], ['limit', L('dashboard.tabLimit'), limitSection],
360
369
  ];
361
370
 
362
371
  const next = d.next[0] ?? (d.kitPending ? L('dashboard.startIdeaCode') : '-');
@@ -388,16 +397,16 @@ code{font:.86em var(--mono);background:var(--sunk);padding:.08em .38em;border-ra
388
397
  .now-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px 16px}
389
398
  .next{margin-top:12px;padding:10px 12px;border-radius:8px;background:var(--accent-soft)}
390
399
  .next .now-l{color:var(--accent)}
391
- .kopyala{font:600 .72rem/1 var(--sans);padding:4px 8px;margin-left:6px;border-radius:6px;border:1px solid var(--line);background:var(--surface);color:var(--fg);cursor:pointer;vertical-align:1px}
392
- .kopyala:hover{border-color:var(--accent);color:var(--accent)}
400
+ .copy-cmd{font:600 .72rem/1 var(--sans);padding:4px 8px;margin-left:6px;border-radius:6px;border:1px solid var(--line);background:var(--surface);color:var(--fg);cursor:pointer;vertical-align:1px}
401
+ .copy-cmd:hover{border-color:var(--accent);color:var(--accent)}
393
402
  .att{list-style:none;padding:0;gap:8px}.att li{display:grid;grid-template-columns:auto 1fr;gap:4px 10px;align-items:start;padding:8px 10px;border-radius:8px;background:var(--sunk)}
394
- .att li.critical{background:var(--crit-bg)}.att li.warn{background:var(--warn-bg)}.att .eylem{grid-column:2;font-size:.8rem;color:var(--muted)}
403
+ .att li.critical{background:var(--crit-bg)}.att li.warn{background:var(--warn-bg)}.att .action{grid-column:2;font-size:.8rem;color:var(--muted)}
395
404
  .badge{display:inline-flex;gap:4px;align-items:center;font:600 .74rem/1 var(--sans);padding:4px 8px;border-radius:999px;white-space:nowrap}
396
405
  .badge.good{background:var(--good-bg);color:var(--fg)}.badge.warn{background:var(--warn-bg);color:var(--fg)}.badge.critical{background:var(--crit-bg);color:var(--fg)}.badge.info{background:var(--info-bg);color:var(--fg)}
397
406
  .ic{font-weight:700}.good .ic{color:var(--good)}.warn .ic{color:#b07800}.critical .ic{color:var(--crit)}.info .ic{color:var(--muted)}
398
407
  @media (prefers-color-scheme:dark){.warn .ic{color:var(--warn)}}
399
408
  .health{font-size:.85rem;padding:6px 12px}
400
- .daha{margin-top:8px}.daha summary{cursor:pointer;font-size:.84rem;color:var(--accent);font-weight:600;padding:4px 0}.daha .att{margin-top:8px}
409
+ .more{margin-top:8px}.more summary{cursor:pointer;font-size:.84rem;color:var(--accent);font-weight:600;padding:4px 0}.more .att{margin-top:8px}
401
410
  .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}.kpis.mini{grid-template-columns:repeat(auto-fit,minmax(110px,1fr));margin-bottom:12px}
402
411
  .kpi{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:12px 14px;display:grid;gap:4px;align-content:start;min-width:0}
403
412
  .kpis.mini .kpi{background:var(--sunk);border:0}
@@ -418,8 +427,8 @@ code{font:.86em var(--mono);background:var(--sunk);padding:.08em .38em;border-ra
418
427
  .step.now .step-n{background:var(--accent);color:var(--surface);border-color:var(--accent)}.step.done .step-n{background:var(--good);color:#fff;border-color:var(--good)}
419
428
  .step-b{display:grid;gap:5px;min-width:0}.step-b b{font-size:.86rem}
420
429
  .rows{display:grid;gap:10px}.row{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(60px,1fr) auto;gap:10px;align-items:center}
421
- details.faz summary{display:flex;flex-wrap:wrap;gap:10px;justify-content:space-between;align-items:center;cursor:pointer;list-style:none}
422
- details.faz summary::-webkit-details-marker{display:none}.faz-bar{display:flex;gap:8px;align-items:center;min-width:200px}.faz-bar .bar{flex:1}
430
+ details.phase summary{display:flex;flex-wrap:wrap;gap:10px;justify-content:space-between;align-items:center;cursor:pointer;list-style:none}
431
+ details.phase summary::-webkit-details-marker{display:none}.phase-bar{display:flex;gap:8px;align-items:center;min-width:200px}.phase-bar .bar{flex:1}
423
432
  .tasks{list-style:none;padding:0;margin-top:12px;gap:2px}.tasks li{display:grid;grid-template-columns:20px 1fr;gap:8px;padding:6px 8px;border-radius:6px}
424
433
  .tasks li.ok{color:var(--muted)}.tasks li.active{background:var(--accent-soft)}
425
434
  .chk{width:16px;height:16px;border:1.5px solid var(--line);border-radius:4px;display:grid;place-items:center;font-size:.7rem;margin-top:2px}
@@ -433,7 +442,7 @@ th{font:600 .7rem/1.2 var(--mono);text-transform:uppercase;letter-spacing:.06em;
433
442
  .chip.on{background:var(--accent);border-color:var(--accent);color:var(--surface)}
434
443
  .feed{list-style:none;padding:0;gap:0}.feed-i{display:grid;grid-template-columns:64px 1fr;gap:10px;padding:8px 0;border-bottom:1px solid var(--line)}.feed-i:last-child{border-bottom:0}
435
444
  .ft{font:600 .68rem/1 var(--mono);text-transform:uppercase;letter-spacing:.05em;padding:4px 6px;border-radius:5px;background:var(--sunk);text-align:center;align-self:start}
436
- .t-karar .ft{background:var(--accent-soft);color:var(--accent)}.t-kurul .ft{background:var(--warn-bg)}.t-bakim .ft{background:var(--good-bg)}
445
+ .t-decision .ft{background:var(--accent-soft);color:var(--accent)}.t-board .ft{background:var(--warn-bg)}.t-maintenance .ft{background:var(--good-bg)}
437
446
  .members{list-style:none;padding:0;gap:8px}.members li{display:grid;gap:2px;padding:8px 10px;border-radius:8px;background:var(--sunk)}
438
447
  .dl{display:grid;grid-template-columns:auto 1fr;gap:6px 14px;margin:0}.dl dt{color:var(--muted);font-size:.84rem}.dl dd{margin:0}
439
448
  .links a{margin-right:8px}
@@ -441,7 +450,7 @@ th{font:600 .7rem/1.2 var(--mono);text-transform:uppercase;letter-spacing:.06em;
441
450
  .sb{height:100%;display:flex;align-items:flex-end;cursor:default;position:relative}.sb span{display:block;width:100%;background:var(--accent);border-radius:4px 4px 0 0}
442
451
  .sb:hover span{filter:brightness(1.15)}.spark-x{display:flex;justify-content:space-between;margin-top:4px}
443
452
  #tip{position:fixed;pointer-events:none;background:var(--fg);color:var(--bg);font-size:.78rem;padding:5px 8px;border-radius:6px;opacity:0;transition:opacity .1s;z-index:10}
444
- .tablo-gor{margin-top:10px}.empty{color:var(--muted);font-size:.88rem}
453
+ .table-view{margin-top:10px}.empty{color:var(--muted);font-size:.88rem}
445
454
  .lim{display:grid;gap:8px;padding:12px;border-radius:10px;background:var(--sunk)}.lim-h{display:flex;justify-content:space-between;align-items:center}
446
455
  footer{color:var(--muted);font-size:.78rem;padding-top:8px}
447
456
  @media (prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
@@ -453,27 +462,27 @@ footer{color:var(--muted);font-size:.78rem;padding-top:8px}
453
462
  ${badge(health, health === 'good' ? L('dashboard.sevGood') : health === 'warn' ? L('dashboard.healthWarn') : L('dashboard.healthCritical')).replace('class="badge', 'class="badge health')}
454
463
  ${d.kit?.type ? `<span class="pill">${e(d.kit.type)}${d.kit.addons?.length ? ' + ' + e(d.kit.addons.join(', ')) : ''}</span><span class="pill">${e(L('dashboard.kitVersion', { version: d.kit.version }))}</span>${d.kit.tool?.some((t) => t !== 'claude') ? `<span class="pill">${e(d.kit.tool.join(' + '))}</span>` : ''}` : `<span class="pill">${e(L('dashboard.kitNotInstalled'))}</span>`}
455
464
  ${git?.branch ? `<span class="pill">⎇ ${e(git.branch)}</span>` : ''}
456
- <label class="refresh"><input type="checkbox" id="oto" checked> ${e(L('dashboard.autoRefresh'))}</label>
465
+ <label class="refresh"><input type="checkbox" id="auto-refresh" checked> ${e(L('dashboard.autoRefresh'))}</label>
457
466
  </div>
458
467
  </header>
459
468
 
460
469
  <div class="hero">
461
- <section class="card" aria-labelledby="simdi">
462
- <div class="now-l" id="simdi">${L('dashboard.now')}</div>
463
- <div class="now-v">${e(a['Aktif faz'] ?? '-')} · ${md(a['Aktif görev'] ?? '-')}</div>
470
+ <section class="card" aria-labelledby="now">
471
+ <div class="now-l" id="now">${L('dashboard.now')}</div>
472
+ <div class="now-v">${e(a['Active phase'] ?? '-')} · ${md(a['Active task'] ?? '-')}</div>
464
473
  <div class="now-grid">
465
- <div><div class="now-l">${L('dashboard.questionMode')}</div><div>${e(a['Soru modu'] ?? '-')}</div></div>
466
- <div><div class="now-l">${L('dashboard.profile')}</div><div>${e(a['Profil'] ?? '-')}</div></div>
467
- <div><div class="now-l">${L('dashboard.lastUpdate')}</div><div class="num">${e(a['Son güncelleme'] ?? '-')}</div></div>
468
- ${/başlangıç/i.test(a['Aktif faz'] ?? '') ? `<div><div class="now-l">${L('dashboard.startStep')}</div><div class="num">${e(a['Başlangıç adımı'] ?? '-')}</div></div>` : ''}
474
+ <div><div class="now-l">${L('dashboard.questionMode')}</div><div>${e(a['Question mode'] ?? '-')}</div></div>
475
+ <div><div class="now-l">${L('dashboard.profile')}</div><div>${e(a['Profile'] ?? '-')}</div></div>
476
+ <div><div class="now-l">${L('dashboard.lastUpdate')}</div><div class="num">${e(a['Last updated'] ?? '-')}</div></div>
477
+ ${isValue(a['Active phase'], 'setup') ? `<div><div class="now-l">${L('dashboard.startStep')}</div><div class="num">${e(a['Setup step'] ?? '-')}</div></div>` : ''}
469
478
  </div>
470
479
  <div class="next"><div class="now-l">${L('dashboard.nextStep')}</div><div>${md(next)}${copyBtn(next)}</div>${cmdOf(next) ? `<div class="muted small">${e(L('dashboard.pasteHint'))}</div>` : ''}</div>
471
480
  ${d.state ? `<div class="now-l" style="margin-top:14px">${L('dashboard.status')}</div><p style="margin-top:4px">${md(d.state)}</p>` : ''}
472
481
  ${d.lastSession.length ? `<div class="now-l" style="margin-top:10px">${L('dashboard.lastSession')}</div><ul style="margin-top:4px">${d.lastSession.slice(0, 4).map((x) => `<li>${md(x)}</li>`).join('')}</ul>` : ''}
473
482
  </section>
474
- <section class="card" aria-labelledby="dikkat">
475
- <h3 id="dikkat">${L('dashboard.attention')} <span class="muted small">(${alerts.length})</span></h3>
476
- ${alerts.length ? `<ul class="att">${alerts.slice(0, 6).map(alertRow).join('')}</ul>${alerts.length > 6 ? `<details class="daha"><summary>${L('dashboard.moreItems', { count: alerts.length - 6 })}</summary><ul class="att">${alerts.slice(6).map(alertRow).join('')}</ul></details>` : ''}` : `<p>${badge('good', L('dashboard.allGood'))} ${L('dashboard.allGoodText')}</p>`}
483
+ <section class="card" aria-labelledby="attention">
484
+ <h3 id="attention">${L('dashboard.attention')} <span class="muted small">(${alerts.length})</span></h3>
485
+ ${alerts.length ? `<ul class="att">${alerts.slice(0, 6).map(alertRow).join('')}</ul>${alerts.length > 6 ? `<details class="more"><summary>${L('dashboard.moreItems', { count: alerts.length - 6 })}</summary><ul class="att">${alerts.slice(6).map(alertRow).join('')}</ul></details>` : ''}` : `<p>${badge('good', L('dashboard.allGood'))} ${L('dashboard.allGoodText')}</p>`}
477
486
  </section>
478
487
  </div>
479
488
 
@@ -521,14 +530,14 @@ ${TABS.map(([id, , content], i) => `<div class="panel" role="tabpanel" id="p-${i
521
530
  c.addEventListener('click', function () {
522
531
  document.querySelectorAll('.filters .chip').forEach(function (x) { x.classList.toggle('on', x === c); });
523
532
  var k = c.dataset.k;
524
- document.querySelectorAll('#kararlar tbody tr').forEach(function (r) {
525
- r.hidden = !(k === 'hepsi' || r.dataset.k === k || (k === 'otomatik' && r.dataset.v === 'otomatik') || (k === 'tek' && r.dataset.kapi === 'tek'));
533
+ document.querySelectorAll('#decision-table tbody tr').forEach(function (r) {
534
+ r.hidden = !(k === 'all' || r.dataset.k === k || (k === 'automatic' && r.dataset.v === 'automatic') || (k === 'one-way' && r.dataset.door === 'one-way'));
526
535
  });
527
536
  });
528
537
  });
529
538
 
530
539
  // file:// may refuse the clipboard API: fall back to a selected textarea
531
- document.querySelectorAll('.kopyala').forEach(function (b) {
540
+ document.querySelectorAll('.copy-cmd').forEach(function (b) {
532
541
  b.addEventListener('click', function () {
533
542
  var c = b.dataset.cmd;
534
543
  function done(ok) { b.textContent = ok ? ${jsStr('dashboard.copied')} : ${jsStr('dashboard.copyFailed')}; setTimeout(function () { b.textContent = ${jsStr('dashboard.copy')}; }, 1500); }
@@ -543,7 +552,7 @@ ${TABS.map(([id, , content], i) => `<div class="panel" role="tabpanel" id="p-${i
543
552
  el.addEventListener('mouseleave', function () { tip.style.opacity = 0; });
544
553
  });
545
554
 
546
- var auto = document.getElementById('oto'), timer;
555
+ var auto = document.getElementById('auto-refresh'), timer;
547
556
  try { auto.checked = localStorage.getItem('durum-oto') !== '0'; } catch (er) {}
548
557
  function schedule() { clearTimeout(timer); if (auto.checked) timer = setTimeout(function () { location.reload(); }, 30000); }
549
558
  auto.addEventListener('change', function () { try { localStorage.setItem('durum-oto', auto.checked ? '1' : '0'); } catch (er) {} schedule(); });
@@ -218,7 +218,7 @@ export function scopeIssue(command, dir, root) {
218
218
  const vk = path.join(os.userInfo().homedir, '.vibe-kit');
219
219
  const touchesApproval = (y) => VIBE_KIT.test(y) || !/^\.\.|^$/.test(path.relative(y, vk)) && !path.isAbsolute(path.relative(y, vk));
220
220
  for (const h of targets) {
221
- if (touchesApproval(h) || touchesApproval(realPath(h))) return { decision: 'ask', reason: /tercihler\.md$/i.test(h)
221
+ if (touchesApproval(h) || touchesApproval(realPath(h))) return { decision: 'ask', reason: /(preferences|tercihler)\.md$/i.test(h)
222
222
  ? t(root, 'guard.destructive.prefsFile', { path: h })
223
223
  : t(root, 'guard.destructive.gateApproval', { path: h }) };
224
224
  const rel = relPath(realPath(h), groot);
@@ -134,7 +134,7 @@ function hook() {
134
134
  const paths = g.files.map(({ path: p }) => realPath(path.resolve(g.dir, p)));
135
135
  // Gate command approval record (gate.mjs, in the user folder ~/.vibe-kit/): the AI must not write it by hand
136
136
  const approval = paths.find((p) => /[\\/]\.vibe-kit([\\/]|$)/i.test(p));
137
- if (approval) decide(g.tool, 'ask', /tercihler\.md$/i.test(approval)
137
+ if (approval) decide(g.tool, 'ask', /(preferences|tercihler)\.md$/i.test(approval)
138
138
  ? t(g.root, 'guard.freeze.prefsFile', { path: approval })
139
139
  : t(g.root, 'guard.freeze.gateApproval', { path: approval }));
140
140
  if (g.tool !== 'claude') {
@@ -2,7 +2,7 @@
2
2
  // Deterministic gate: lint, type check, tests and Semgrep run before the board meets; the output is input for the members.
3
3
  // Why: the model should not search for what lint/type/test catch, and a red check should not be left to a model decision (F-03).
4
4
  // node .claude/hooks/gate.mjs --run exit code: 0 green, 1 red
5
- // Commands are read from AGENTS.md → Komutlar (lint, tip kontrol, test); an empty one ([ ]) counts as "unset".
5
+ // Commands are read from AGENTS.md → Commands (lint, type check, test); an empty one ([ ]) counts as "unset".
6
6
  // If Semgrep is installed it runs on changed code files (override with SEMGREP=<executable>); otherwise skipped, CI runs it.
7
7
  // The result goes to .claude/state/gate.json with the same change signature as the commit gate; the commit gate stops
8
8
  // the commit on a red or stale result. A green check's output is not written, the last 30 lines of a red one are.
@@ -18,12 +18,15 @@ import { spawnSync } from 'node:child_process';
18
18
  import { changes } from './commit-gate.mjs';
19
19
  import { isCodeFile } from './edit-rounds.mjs';
20
20
  import { findRoot, isMain } from './shared.mjs';
21
+ import { normalize } from './labels.mjs';
21
22
  import { specInvariants } from './dashboard-data.mjs';
22
23
 
23
- // Keys of the AGENTS.md → Komutlar block (Turkish template text, matched as written)
24
- const CHECKS = ['lint', 'tip kontrol', 'test'];
24
+ // Keys of the AGENTS.md → Commands block (Turkish keys of pre-3.48 projects are read through normalize)
25
+ const CHECKS = ['lint', 'type check', 'test'];
25
26
  // The approval also covers the dev command and the preview address: preview.mjs runs the one and opens the other
26
- const APPROVED = [...CHECKS, 'geliştirme', 'önizleme'];
27
+ const APPROVED = [...CHECKS, 'dev', 'preview'];
28
+ // The digest keeps the pre-3.48 names, so an approval given before the English labels stays valid
29
+ const DIGEST_NAME = { 'type check': 'tip kontrol', dev: 'geliştirme', preview: 'önizleme' };
27
30
  const TIMEOUT = 10 * 60 * 1000;
28
31
  const BUFFER = 64 * 1024 * 1024;
29
32
  // A red check shows its last 30 lines; a longer output is kept whole in a file, read only when the tail is not enough
@@ -37,9 +40,9 @@ function failed(root, name, r) {
37
40
  return { output: lines.slice(-30).join('\n'), note: `last 30 lines; full output in ${rel}` };
38
41
  }
39
42
 
40
- // Only the "## Komutlar" section is read: a "test:" line elsewhere must not count as a command
43
+ // Only the "## Commands" section is read: a "test:" line elsewhere must not count as a command
41
44
  export function readCommands(md, names = CHECKS) {
42
- const section = md.match(/^## Komutlar[^\n]*\n([\s\S]*?)(?=^## |(?![\s\S]))/m)?.[1] ?? '';
45
+ const section = normalize(md).match(/^## Commands[^\n]*\n([\s\S]*?)(?=^## |(?![\s\S]))/m)?.[1] ?? '';
43
46
  return names.map((name) => {
44
47
  const value = section.match(new RegExp(`^${name}:[ \\t]*(.*?)[ \\t\\r]*$`, 'm'))?.[1];
45
48
  return { name, command: value && !/^\[.*\]$/.test(value) ? value : null };
@@ -54,7 +57,7 @@ export function approvalPath(root) {
54
57
  const key = crypto.createHash('sha256').update(realRoot.toLowerCase()).digest('hex').slice(0, 32);
55
58
  return path.join(os.userInfo().homedir, '.vibe-kit', 'gate-approval', `${key}.json`);
56
59
  }
57
- const digest = (list) => crypto.createHash('sha256').update(JSON.stringify(list.map((k) => [k.name, k.command]))).digest('hex');
60
+ const digest = (list) => crypto.createHash('sha256').update(JSON.stringify(list.map((k) => [DIGEST_NAME[k.name] ?? k.name, k.command]))).digest('hex');
58
61
  export const commandList = (list) => list.filter((k) => k.command).map((k) => `- ${k.name}: ${k.command}`).join('\n');
59
62
  export const readAgents = (root) => { try { return fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8'); } catch { return ''; } };
60
63
  export const approvedCommands = (root) => readCommands(readAgents(root), APPROVED);
@@ -73,7 +76,7 @@ export function isApproved(root, list = approvedCommands(root)) {
73
76
  }
74
77
 
75
78
  function check(root, { name, command }) {
76
- if (!command) return { name, status: 'unset', note: 'AGENTS.md → Komutlar' };
79
+ if (!command) return { name, status: 'unset', note: 'AGENTS.md → Commands' };
77
80
  const start = Date.now();
78
81
  // The command comes from AGENTS.md (a rule file, writes need approval); pipes and && need a shell
79
82
  const r = spawnSync(command, { cwd: root, shell: true, encoding: 'utf8', timeout: TIMEOUT, maxBuffer: BUFFER });
@@ -154,7 +157,7 @@ export function absPathCheck(root, files) {
154
157
  return { name, status: 'passed', ...(notes.length && { note: `${notes.length} lines have a path that looks like a user folder; fix it if it is machine-specific (does not block)`, output: list(notes) }) };
155
158
  }
156
159
 
157
- export const approvalNeeded = (list) => `command approval needed\nAGENTS.md → Komutlar is new or changed; these commands do not run unapproved:\n${commandList(list)}\n` +
160
+ export const approvalNeeded = (list) => `command approval needed\nAGENTS.md → Commands is new or changed; these commands do not run unapproved:\n${commandList(list)}\n` +
158
161
  'Show the commands to the user and ask. If they approve: node .claude/hooks/gate.mjs --approve, then run the command again.';
159
162
 
160
163
  // Spec drift (F-23): a "must not change" behavior whose test is missing or red stops the gate; no result is only a note
@@ -166,7 +169,7 @@ function specCheck(root, since) {
166
169
  const unknown = list.filter((k) => k.test && !k.result).length;
167
170
  const notes = [`${list.length} behaviors that must not change`,
168
171
  unknown && `${unknown} tests have no result in this run (the test command writes no JUnit report)`,
169
- unreadable.length && `${unreadable.length} lines could not be read, write them as "- Ö-01.1: ...": ${unreadable.map((u) => `${u.spec}: ${u.line}`).join(' · ')}`];
172
+ unreadable.length && `${unreadable.length} lines could not be read, write them as "- AC-01.1: ...": ${unreadable.map((u) => `${u.spec}: ${u.line}`).join(' · ')}`];
170
173
  return { name: 'Spec', status: bad.length ? 'failed' : 'passed', note: notes.filter(Boolean).join('; '),
171
174
  output: bad.length ? bad.map((k) => `${k.id} (${k.spec}): ${k.test ? 'test red' : 'no test'}`).join('\n') : undefined };
172
175
  }
@@ -193,7 +196,7 @@ export function runGate(root) {
193
196
  fs.mkdirSync(path.dirname(file), { recursive: true });
194
197
  fs.writeFileSync(file, JSON.stringify({ signature, date: new Date().toISOString(), passed, empty, checks }, null, 2));
195
198
  const result = checks.some((k) => k.status === 'failed') ? 'Result: RED. The commit gate stays closed: fix it and run the gate again.'
196
- : empty ? 'Result: EMPTY, no check ran. Fill in AGENTS.md → Komutlar; the commit gate stays closed (if the user wants, commit-gate.mjs --skip).'
199
+ : empty ? 'Result: EMPTY, no check ran. Fill in AGENTS.md → Commands; the commit gate stays closed (if the user wants, commit-gate.mjs --skip).'
197
200
  : 'Result: GREEN.';
198
201
  return { passed, text: [`## Gate · ${files.length} files changed`, ...checks.map(line), result].join('\n') };
199
202
  }