tillerhand 3.48.0 → 3.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/board-chair.md +1 -1
- package/.claude/agents/security-auditor.md +1 -1
- package/.claude/agents/test-writer.md +1 -1
- package/.claude/board/board-product.md +1 -1
- package/.claude/board/council.md +1 -1
- package/.claude/board/rules.md +6 -6
- package/.claude/commands/adopt.md +3 -3
- package/.claude/commands/ask-mode.md +4 -4
- package/.claude/commands/board.md +1 -1
- package/.claude/commands/build.md +1 -1
- package/.claude/commands/design.md +4 -4
- package/.claude/commands/findings-business.md +1 -1
- package/.claude/commands/findings-legal.md +1 -1
- package/.claude/commands/findings-perf.md +1 -1
- package/.claude/commands/findings-security.md +2 -2
- package/.claude/commands/findings-tech.md +1 -1
- package/.claude/commands/findings-ux.md +1 -1
- package/.claude/commands/fix-bug.md +2 -2
- package/.claude/commands/handoff.md +2 -2
- package/.claude/commands/kit-update.md +2 -2
- package/.claude/commands/qa.md +1 -1
- package/.claude/commands/release.md +2 -2
- package/.claude/commands/research.md +3 -3
- package/.claude/commands/retro.md +1 -1
- package/.claude/commands/rollback.md +3 -3
- package/.claude/commands/start.md +4 -4
- package/.claude/commands/tools.md +1 -1
- package/.claude/commands/user-feedback.md +1 -1
- package/.claude/findings/lenses/business.md +1 -1
- package/.claude/findings/protocol.md +2 -2
- package/.claude/hooks/budget.mjs +1 -1
- package/.claude/hooks/checkpoint.mjs +2 -2
- package/.claude/hooks/commit-gate.mjs +14 -15
- package/.claude/hooks/council.mjs +37 -52
- package/.claude/hooks/dashboard-data.mjs +34 -42
- package/.claude/hooks/dashboard.mjs +18 -20
- package/.claude/hooks/destructive-guard.mjs +2 -2
- package/.claude/hooks/flow.mjs +1 -1
- package/.claude/hooks/freeze-guard.mjs +2 -2
- package/.claude/hooks/gate.mjs +18 -11
- package/.claude/hooks/labels.mjs +92 -152
- package/.claude/hooks/lang.mjs +25 -9
- package/.claude/hooks/messages.mjs +496 -494
- package/.claude/hooks/notify.mjs +1 -1
- package/.claude/hooks/prefs.mjs +10 -18
- package/.claude/hooks/preview.mjs +1 -1
- package/.claude/hooks/progress.mjs +7 -9
- package/.claude/hooks/qa-report.mjs +1 -3
- package/.claude/hooks/session-start.mjs +2 -3
- package/.claude/hooks/status-read.mjs +5 -5
- package/.claude/hooks/statusline.mjs +4 -4
- package/.claude/hooks/tools.mjs +3 -7
- package/.claude/hooks/usage.mjs +2 -2
- package/.claude/i18n/tr.json +495 -0
- package/.claude/security-patterns.json +1 -1
- package/.claude/skills/logo-design/SKILL.md +2 -2
- package/.claude/skills/motion-video/SKILL.md +1 -1
- package/.claude/skills/source-research/SKILL.md +1 -1
- package/.claude/skills/ui-motion/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +1 -1
- package/README.md +2 -2
- package/README.tr.md +2 -2
- package/docs/architecture/overview.md +1 -1
- package/docs/decisions/log.md +1 -1
- package/docs/legal/privacy-kvkk.md +35 -35
- package/docs/operations/deploy-setup.md +1 -1
- package/docs/operations/feature-flags.md +1 -1
- package/docs/product/glossary.md +3 -3
- package/docs/product/launch.md +1 -1
- package/docs/product/prd.md +2 -2
- package/docs/product/roadmap.md +1 -1
- package/docs/product/specs/_template.md +1 -1
- package/docs/product/unit-economics.md +1 -1
- package/docs/quality/test-plan.md +5 -5
- package/docs/workflow/decision-protocol.md +1 -2
- package/docs/workflow/mcp-catalog.md +1 -1
- package/docs/workflow/skills.md +2 -2
- package/docs/workflow/usage-limits.md +1 -1
- package/kit/CHANGELOG.md +37 -0
- package/kit/VERSION +1 -1
- package/kit/guide.html +56 -56
- package/kit/guide.tr.html +10 -10
- package/kit/kit.mjs +7 -42
- package/kit/templates/addons/ai/.env.example.append +3 -3
- package/kit/templates/addons/ai/ai/evals/README.md +5 -5
- package/kit/templates/addons/ai/ai/evals/cases.jsonl +3 -3
- package/kit/templates/addons/ai/docs/ai/guide.md +1 -1
- package/kit/templates/addons/tr/docs/legal/turkiye.md +55 -55
- package/kit/templates/addons/tr/types/backend/.env.example.append +2 -2
- package/kit/templates/addons/tr/types/backend/app/shared/payments.py +1 -1
- package/kit/templates/addons/tr/types/backend/tests/security/test_payment_webhook.py +3 -3
- package/kit/templates/addons/tr/types/web-saas/.env.example.append +2 -2
- package/kit/templates/addons/tr/types/web-saas/.github/workflows/payment-webhook.yml +3 -3
- package/kit/templates/addons/tr/types/web-saas/src/lib/payments/webhook-signature.ts +1 -1
- package/kit/templates/addons/tr/types/web-saas/tests/security/payment-webhook.mjs +4 -4
- package/kit/templates/backend/.github/dependabot.yml +1 -1
- package/kit/templates/backend/.github/workflows/ci.yml +4 -4
- package/kit/templates/backend/.github/workflows/claude-ci-fix.yml +10 -10
- package/kit/templates/backend/.github/workflows/claude-maintenance.yml +1 -1
- package/kit/templates/backend/.github/workflows/live-monitoring.yml +2 -2
- package/kit/templates/backend/.pre-commit-config.yaml +1 -1
- package/kit/templates/backend/docs/architecture/stack.md +3 -3
- package/kit/templates/mobile/.github/dependabot.yml +1 -1
- package/kit/templates/mobile/.github/workflows/ci.yml +6 -6
- package/kit/templates/mobile/.github/workflows/claude-ci-fix.yml +10 -10
- package/kit/templates/mobile/.github/workflows/claude-maintenance.yml +1 -1
- package/kit/templates/mobile/docs/architecture/stack.md +4 -4
- package/kit/templates/mobile/docs/architecture/structure.md +1 -1
- package/kit/templates/mobile/docs/design/design-guide.md +1 -1
- package/kit/templates/web-saas/.github/dependabot.yml +1 -1
- package/kit/templates/web-saas/.github/workflows/ci.yml +6 -6
- package/kit/templates/web-saas/.github/workflows/claude-ci-fix.yml +10 -10
- package/kit/templates/web-saas/.github/workflows/claude-maintenance.yml +1 -1
- package/kit/templates/web-saas/.github/workflows/live-monitoring.yml +2 -2
- package/kit/templates/web-saas/docs/architecture/stack.md +4 -4
- package/kit/templates/web-saas/docs/design/design-guide.md +1 -1
- package/kit/tools/antigravity/.agents/hooks.json +5 -5
- package/package.json +1 -1
|
@@ -11,8 +11,7 @@ import { verdict, isValue } from './labels.mjs';
|
|
|
11
11
|
import { readLimits, summary, level, THRESHOLDS } from './usage.mjs';
|
|
12
12
|
import { packageStatus, commandMode } from './tools.mjs';
|
|
13
13
|
import { readModels, modelsFile, modelsNote, roleLabel } from './models.mjs';
|
|
14
|
-
import {
|
|
15
|
-
import { readLang, t } from './lang.mjs';
|
|
14
|
+
import { readLang, t, locale } from './lang.mjs';
|
|
16
15
|
import { acceptanceStatus, accounts, flags, unitEconomics, feedback, learning, qaSummary, metrics, githubStatus, readContext, findingReports, FINDING_KINDS, producedAssets, costSummary, councilMinutes, spendAlarms, envWarnings, repeatedBugClasses } from './dashboard-data.mjs';
|
|
17
16
|
|
|
18
17
|
const args = process.argv.slice(2);
|
|
@@ -28,8 +27,8 @@ const kindName = (k) => (Object.hasOwn(FINDING_KINDS, k) ? L(`dashboard.findingK
|
|
|
28
27
|
// The dashboard is made in the user's language: none before /start or /adopt asks it
|
|
29
28
|
const lang = readLang(root);
|
|
30
29
|
if (!lang) { if (!quiet) console.log('No language chosen yet: the dashboard is made after /start or /adopt asks it.'); process.exit(0); }
|
|
31
|
-
// Dates follow the conversation language
|
|
32
|
-
const loc = (
|
|
30
|
+
// Dates follow the conversation language
|
|
31
|
+
const loc = locale(lang);
|
|
33
32
|
|
|
34
33
|
// ---------- data ----------
|
|
35
34
|
const d = readStatus(root);
|
|
@@ -119,6 +118,8 @@ if (d.kitPending) addAlert('info', L('dashboard.alert.kitPending'), L('dashboard
|
|
|
119
118
|
if (d.handoffPending) addAlert('warn', L('dashboard.alert.handoffPending'), '/handoff take');
|
|
120
119
|
const lastReject = [...d.board.meetings].reverse().find((t) => verdict(t.result) === 'reject');
|
|
121
120
|
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'));
|
|
121
|
+
// A result verdict() cannot read is not an approval: the user checks it (fail-closed)
|
|
122
|
+
for (const t of d.board.meetings.filter((m) => m.result && verdict(m.result) === 'unknown')) addAlert('warn', L('dashboard.alert.unknownVerdict', { kind: t.kind, scope: t.scope, result: t.result }), 'docs/workflow/board.md');
|
|
122
123
|
const limitLevel = level(limit);
|
|
123
124
|
const limitShort = limit ? [[L('dashboard.limit5h'), limit.five_hour], [L('dashboard.limitWeeklyLower'), limit.seven_day]].filter(([, p]) => p)
|
|
124
125
|
.map(([name, p]) => `${name} %${Math.round(p.percent)}${p.resetsAt ? ` ${L('dashboard.resetsParen', { when: new Date(p.resetsAt * 1000).toLocaleString(loc, { weekday: 'short', hour: '2-digit', minute: '2-digit' }) })}` : ''}`).join(', ') : '';
|
|
@@ -130,7 +131,7 @@ for (const s of d.issues.slice(0, 5)) addAlert('warn', L('dashboard.alert.openIs
|
|
|
130
131
|
for (const r of repeatedBugClasses(root)) addAlert('info', L('dashboard.alert.repeatedBug', { count: r.specs.length, cls: r.cls, specs: r.specs.join(', ') }), '/retro');
|
|
131
132
|
if (git?.changed.length > 15) addAlert('info', L('dashboard.alert.uncommitted', { count: git.changed.length }), L('dashboard.act.commitOrWrapUp'));
|
|
132
133
|
if (git?.progressStale >= 3) addAlert('info', L('dashboard.alert.progressStale', { count: git.progressStale }), '/wrap-up');
|
|
133
|
-
const off = (a['Question mode'] ?? '').match(/
|
|
134
|
+
const off = (a['Question mode'] ?? '').match(/closed topics:\s*(.+)$/i)?.[1];
|
|
134
135
|
if (off && !isValue(off, 'none')) addAlert('info', L('dashboard.alert.closedTopics', { topics: off }), L('dashboard.act.askModeOpen'));
|
|
135
136
|
if (d.installed && isValue(d.board.state, 'notSetUp') && !isValue(a['Active phase'], 'setup')) addAlert('info', L('dashboard.alert.boardNotSetUp'), '/board setup');
|
|
136
137
|
if (github?.liveMonitoring?.length) addAlert('critical', L('dashboard.alert.liveMonitoring', { title: github.liveMonitoring[0].title }), L('dashboard.act.fixLive', { number: github.liveMonitoring[0].number }));
|
|
@@ -186,7 +187,7 @@ const autoDecisions = d.decisions.filter((k) => isValue(k.decidedBy, 'automatic'
|
|
|
186
187
|
const rejectCount = d.board.meetings.filter((t) => verdict(t.result) === 'reject').length;
|
|
187
188
|
const mvpCriteria = mvp.reduce((t, o) => ({ done: t.done + o.done, total: t.total + o.total }), { done: 0, total: 0 });
|
|
188
189
|
const avgCost = Object.entries(cost.reduce((m, r) => {
|
|
189
|
-
if (!Number.isNaN(r.diff)) (m[
|
|
190
|
+
if (!Number.isNaN(r.diff)) (m[r.size] ??= []).push(r.diff);
|
|
190
191
|
return m;
|
|
191
192
|
}, Object.create(null))).map(([size, l]) => ({ size, avg: l.reduce((t, x) => t + x, 0) / l.length, n: l.length }));
|
|
192
193
|
|
|
@@ -202,25 +203,22 @@ const feedLabel = (k) => (Object.hasOwn(FEED_KEYS, k) ? L(FEED_KEYS[k]) : k);
|
|
|
202
203
|
const onOff = (v) => (isValue(v, 'on') ? L('dashboard.on') : isValue(v, 'off') ? L('dashboard.off') : v);
|
|
203
204
|
// A text for the page's own script: a single-quoted JS string, no tag can end the script block
|
|
204
205
|
const jsStr = (key) => `'${L(key).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/</g, '\\u003c')}'`;
|
|
205
|
-
// A
|
|
206
|
-
const verdictBadge = (k) => badge({ approve: 'good', conditional: 'warn', reject: 'critical' }[verdict(k)] ??
|
|
206
|
+
// A council decision: an option name or an unreadable result is neutral. A meeting result verdict() cannot read is a warning.
|
|
207
|
+
const verdictBadge = (k, other = 'info') => badge({ approve: 'good', conditional: 'warn', reject: 'critical' }[verdict(k)] ?? other, k);
|
|
207
208
|
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>`;
|
|
208
209
|
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 }); };
|
|
209
210
|
const limitSeverity = (p, k) => (p == null ? 'info' : p >= THRESHOLDS[k].red ? 'critical' : p >= THRESHOLDS[k].yellow ? 'warn' : 'good');
|
|
210
211
|
const TOPICS = ['product', 'tech', 'design', 'content', 'data', 'security', 'cost'];
|
|
211
|
-
|
|
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; };
|
|
212
|
+
const topicKey = (topic = '') => topic.trim().toLowerCase();
|
|
214
213
|
|
|
215
|
-
// The command to paste into Claude: "`/build G-013` …" or "/wrap-up
|
|
216
|
-
const cmdOf = (s = '') => { const m = String(s).match(/`(\/[^`\n]+)`/) ?? String(s).match(/^(\/.*)$/); return m ? m[1].split(
|
|
214
|
+
// The command to paste into Claude: "`/build G-013` …" or "/wrap-up and /clear" → first command, without its <placeholder> or note
|
|
215
|
+
const cmdOf = (s = '') => { const m = String(s).match(/`(\/[^`\n]+)`/) ?? String(s).match(/^(\/.*)$/); return m ? m[1].split(/\s+\S+\s+\/| and | to |\s*\(/)[0].replace(/\s*[[<].*$/, '').trim() : ''; };
|
|
217
216
|
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
217
|
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>`;
|
|
219
218
|
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>`;
|
|
220
219
|
|
|
221
220
|
// ---------- sections ----------
|
|
222
|
-
|
|
223
|
-
const activePhase = (a['Active phase'] ?? '').replace(/^faz\b/i, 'Phase').replace(/^başlangıç(?![\p{L}\p{N}])/iu, 'Setup');
|
|
221
|
+
const activePhase = a['Active phase'] ?? '';
|
|
224
222
|
const activeIndex = Math.max(0, [{ name: 'Setup' }, ...d.phases].findIndex((f) => activePhase.toLowerCase().startsWith(f.name.toLowerCase().split(' ').slice(0, 2).join(' '))));
|
|
225
223
|
const start = (a['Setup step'] ?? '0/9').split('/').map(Number);
|
|
226
224
|
const stageStrip = `<ol class="steps">${[{ name: 'Setup', done: start[0] || 0, total: start[1] || 9 }, ...d.phases].map((f, i) =>
|
|
@@ -264,7 +262,7 @@ const boardSection = `
|
|
|
264
262
|
<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>`;
|
|
265
263
|
})() : empty(L('dashboard.noCouncil'), '/board council <question>')}</section>
|
|
266
264
|
<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>
|
|
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('')}
|
|
265
|
+
${[...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, 'warn')}</td><td>${md(t.critical)}</td><td>${md(t.decision)}</td></tr>`).join('')}
|
|
268
266
|
</tbody></table></div>` : empty(L('dashboard.noMeetings'))}</section>`;
|
|
269
267
|
|
|
270
268
|
const quality = `
|
|
@@ -278,7 +276,7 @@ const quality = `
|
|
|
278
276
|
${qa ? `<section class="card"><h3>${L('dashboard.lastQa', { name: e(qa.name) })}</h3><div class="kpis mini">${kpi(L('dashboard.passed'), qa.passed)}${kpi(L('dashboard.failed'), qa.failed, qa.failed ? badge('critical', L('dashboard.withEvidence')) : '')}${kpi(L('dashboard.notRun'), qa.notRun)}</div>${qa.failures.length ? `<ul>${qa.failures.slice(0, 5).map((x) => `<li>${md(x.criterion)}${x.note ? ` <span class="muted small">${e(x.note)}</span>` : ''}</li>`).join('')}</ul>` : ''}<p class="muted small"><a href="${e(qa.file)}" target="_blank" rel="noopener">${L('dashboard.openReport')}</a></p></section>` : ''}
|
|
279
277
|
<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>
|
|
280
278
|
<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>
|
|
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>${/✅|
|
|
279
|
+
<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>${/✅|green|present/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>
|
|
282
280
|
<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')}
|
|
283
281
|
${lastRetro ? `<p class="muted small">${L('dashboard.lastRetro', { value: e(lastRetro) })}</p>` : ''}</section>`;
|
|
284
282
|
|
|
@@ -302,7 +300,7 @@ const product = `
|
|
|
302
300
|
</div>
|
|
303
301
|
<div class="grid2">
|
|
304
302
|
<section class="card"><h3>${L('dashboard.technology')}</h3>${stack.length && !stack.every((s) => !s.choice) ? `<dl class="dl">${stack.map((s) => `<dt>${e(s.layer)}</dt><dd>${md(s.choice || '-')}</dd>`).join('')}</dl>` : empty(L('dashboard.noStack'))}</section>
|
|
305
|
-
<section class="card"><h3>${L('dashboard.design')}</h3>${designDirection.length ? `<dl class="dl">${designDirection.map((t) => `<dt>${e(t.area)}</dt><dd>${md(t.choice)}</dd>`).join('')}</dl>` : empty(L('dashboard.noDesign'), '/design
|
|
303
|
+
<section class="card"><h3>${L('dashboard.design')}</h3>${designDirection.length ? `<dl class="dl">${designDirection.map((t) => `<dt>${e(t.area)}</dt><dd>${md(t.choice)}</dd>`).join('')}</dl>` : empty(L('dashboard.noDesign'), '/design general')}
|
|
306
304
|
${previews.length ? `<h4>${L('dashboard.previews')}</h4><ul class="links">${previews.map((o) => `<li><b>${e(o.name)}</b> ${o.files.map((f) => `<a href=".previews/${encodeURIComponent(o.name)}/${encodeURIComponent(f)}" target="_blank" rel="noopener">${e(f.replace(/\.html$/, ''))}</a>`).join(' ')}</li>`).join('')}</ul>` : ''}</section>
|
|
307
305
|
</div>
|
|
308
306
|
<div class="grid2">
|
|
@@ -553,9 +551,9 @@ ${TABS.map(([id, , content], i) => `<div class="panel" role="tabpanel" id="p-${i
|
|
|
553
551
|
});
|
|
554
552
|
|
|
555
553
|
var auto = document.getElementById('auto-refresh'), timer;
|
|
556
|
-
try { auto.checked = localStorage.getItem('
|
|
554
|
+
try { auto.checked = localStorage.getItem('status-auto') !== '0'; } catch (er) {}
|
|
557
555
|
function schedule() { clearTimeout(timer); if (auto.checked) timer = setTimeout(function () { location.reload(); }, 30000); }
|
|
558
|
-
auto.addEventListener('change', function () { try { localStorage.setItem('
|
|
556
|
+
auto.addEventListener('change', function () { try { localStorage.setItem('status-auto', auto.checked ? '1' : '0'); } catch (er) {} schedule(); });
|
|
559
557
|
schedule();
|
|
560
558
|
})();
|
|
561
559
|
</script>
|
|
@@ -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: /
|
|
221
|
+
if (touchesApproval(h) || touchesApproval(realPath(h))) return { decision: 'ask', reason: /preferences[.]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);
|
|
@@ -281,7 +281,7 @@ export const unquote = (s) => s.replace(/\$'((?:[^'\\]|\\.)*)'/g, '$1').replace(
|
|
|
281
281
|
const RANK = { deny: 2, ask: 1 };
|
|
282
282
|
const strictest = (...s) => s.filter(Boolean).sort((a, b) => RANK[b.decision] - RANK[a.decision])[0] ?? null;
|
|
283
283
|
|
|
284
|
-
// A commit message that only names a command (git commit -m "commit-gate.mjs --skip
|
|
284
|
+
// A commit message that only names a command (git commit -m "commit-gate.mjs --skip explained") is text, not a call.
|
|
285
285
|
// Emptied only when the WHOLE command is one plain commit (optionally after "git add … &&") and the message is its last
|
|
286
286
|
// word: then nothing else can hide in it. No quote, $, `, (, #, <, > before the message: a quote opened earlier
|
|
287
287
|
// would make a real command look like the message. The message must be safe in bash, PowerShell and cmd alike: no
|
package/.claude/hooks/flow.mjs
CHANGED
|
@@ -23,7 +23,7 @@ const MEMBERS = [
|
|
|
23
23
|
['board-product', /^docs\/product\//i],
|
|
24
24
|
];
|
|
25
25
|
// Auth, payment, webhook, upload, secrets: security-auditor joins on top of the member count (rules.md §3)
|
|
26
|
-
const SENSITIVE = /auth|login|session|passw|payment
|
|
26
|
+
const SENSITIVE = /auth|login|session|passw|payment|checkout|billing|webhook|upload|secret|token|crypto|\.env|iyzico|paytr|stripe/i;
|
|
27
27
|
|
|
28
28
|
// Same bands as the cost table in docs/workflow/usage-limits.md §3 (1-2, 3-6, more files)
|
|
29
29
|
export function flow({ files = 0, paths, installed = [], context, level: lvl }) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// node .claude/hooks/freeze-guard.mjs --status print state (lock and plan)
|
|
11
11
|
// Scope shield (F-06): after /plan-task approval the plan's file list is saved; while a plan is active a write to an
|
|
12
12
|
// EXISTING file not on the list is asked. New files are the structure guard's job; docs/, CHANGELOG.md are free. No plan: silent.
|
|
13
|
-
// State: .claude/state/plan.json → { "task": "G-012", "paths": ["src/a.ts", "src/features/
|
|
13
|
+
// State: .claude/state/plan.json → { "task": "G-012", "paths": ["src/a.ts", "src/features/invoice/"] } (ending in /: folder)
|
|
14
14
|
// node .claude/hooks/freeze-guard.mjs --plan-save <task> <path...> save when no plan is active (asked: the user sees the list)
|
|
15
15
|
// node .claude/hooks/freeze-guard.mjs --plan-add <path...> widen the list (with user approval)
|
|
16
16
|
// node .claude/hooks/freeze-guard.mjs --plan-end close when the task is done (asked)
|
|
@@ -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', /
|
|
137
|
+
if (approval) decide(g.tool, 'ask', /preferences[.]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') {
|
package/.claude/hooks/gate.mjs
CHANGED
|
@@ -18,15 +18,12 @@ 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';
|
|
22
21
|
import { specInvariants } from './dashboard-data.mjs';
|
|
23
22
|
|
|
24
|
-
// Keys of the AGENTS.md → Commands block
|
|
23
|
+
// Keys of the AGENTS.md → Commands block
|
|
25
24
|
const CHECKS = ['lint', 'type check', 'test'];
|
|
26
25
|
// The approval also covers the dev command and the preview address: preview.mjs runs the one and opens the other
|
|
27
26
|
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' };
|
|
30
27
|
const TIMEOUT = 10 * 60 * 1000;
|
|
31
28
|
const BUFFER = 64 * 1024 * 1024;
|
|
32
29
|
// 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
|
|
@@ -42,13 +39,21 @@ function failed(root, name, r) {
|
|
|
42
39
|
|
|
43
40
|
// Only the "## Commands" section is read: a "test:" line elsewhere must not count as a command
|
|
44
41
|
export function readCommands(md, names = CHECKS) {
|
|
45
|
-
const section =
|
|
42
|
+
const section = md.match(/^## Commands[^\n]*\n([\s\S]*?)(?=^## |(?![\s\S]))/m)?.[1] ?? '';
|
|
46
43
|
return names.map((name) => {
|
|
47
44
|
const value = section.match(new RegExp(`^${name}:[ \\t]*(.*?)[ \\t\\r]*$`, 'm'))?.[1];
|
|
48
45
|
return { name, command: value && !/^\[.*\]$/.test(value) ? value : null };
|
|
49
46
|
});
|
|
50
47
|
}
|
|
51
48
|
|
|
49
|
+
// Keys under "## Commands" the kit does not know (a label written in another language): named, not silently ignored
|
|
50
|
+
const KNOWN_KEYS = ['dev', 'preview', 'test', 'lint', 'type check', 'build'];
|
|
51
|
+
export function unknownCommandKeys(md) {
|
|
52
|
+
const section = md.match(/^## Commands[^\n]*\n([\s\S]*?)(?=^## |(?![\s\S]))/m)?.[1] ?? '';
|
|
53
|
+
const fenced = [...section.matchAll(/^```[^\n]*\n([\s\S]*?)^```/gm)].map((m) => m[1]).join('\n'); // prose around the block is not a key
|
|
54
|
+
return [...fenced.matchAll(/^([\p{L}][\p{L}\p{N} _-]*?)[ \t]*:(?!\/\/)/gmu)].map((m) => m[1]).filter((k) => !KNOWN_KEYS.includes(k));
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
// The approval record lives outside the project, in the user folder (keyed by a digest of the project's real path): no repo,
|
|
53
58
|
// archive or .git can carry an approval. The home folder comes from the OS (os.userInfo); HOME/USERPROFILE cannot redirect it.
|
|
54
59
|
export function approvalPath(root) {
|
|
@@ -57,7 +62,7 @@ export function approvalPath(root) {
|
|
|
57
62
|
const key = crypto.createHash('sha256').update(realRoot.toLowerCase()).digest('hex').slice(0, 32);
|
|
58
63
|
return path.join(os.userInfo().homedir, '.vibe-kit', 'gate-approval', `${key}.json`);
|
|
59
64
|
}
|
|
60
|
-
const digest = (list) => crypto.createHash('sha256').update(JSON.stringify(list.map((k) => [
|
|
65
|
+
const digest = (list) => crypto.createHash('sha256').update(JSON.stringify(list.map((k) => [k.name, k.command]))).digest('hex');
|
|
61
66
|
export const commandList = (list) => list.filter((k) => k.command).map((k) => `- ${k.name}: ${k.command}`).join('\n');
|
|
62
67
|
export const readAgents = (root) => { try { return fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8'); } catch { return ''; } };
|
|
63
68
|
export const approvedCommands = (root) => readCommands(readAgents(root), APPROVED);
|
|
@@ -113,7 +118,7 @@ function semgrep(root, files) {
|
|
|
113
118
|
// boundary after it; any slash style, JSON-escaped too; case-insensitive only on Windows).
|
|
114
119
|
// Note only: a generic C:\Users\<name>, /home/<name>/, /Users/<name>/ (a container path like /home/node/app or a route
|
|
115
120
|
// is not a machine path, so it never blocks).
|
|
116
|
-
// "kit:
|
|
121
|
+
// "kit:path-allowed" on the line or the line above allows a deliberate one (Dockerfile and JSON take no inline comment).
|
|
117
122
|
// Only added lines of tracked files are read (an old path elsewhere in the file is not this change's); a new file is
|
|
118
123
|
// read whole. Paths are read from the git top level, so a project in a monorepo subfolder is checked too.
|
|
119
124
|
const ESCAPE = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -146,13 +151,13 @@ export function absPathCheck(root, files) {
|
|
|
146
151
|
} catch { continue; }
|
|
147
152
|
lines.forEach((l, i) => {
|
|
148
153
|
if (added.has(f) && !added.get(f).has(i + 1)) return;
|
|
149
|
-
if (l.includes('kit:
|
|
154
|
+
if (l.includes('kit:path-allowed') || lines[i - 1]?.includes('kit:path-allowed')) return;
|
|
150
155
|
const at = `${f}:${i + 1}: ${l.trim().slice(0, 120)}`;
|
|
151
156
|
if (mine?.test(l)) red.push(at); else if (generic.test(l)) notes.push(at);
|
|
152
157
|
});
|
|
153
158
|
}
|
|
154
159
|
const list = (a) => [...a.slice(0, 15), a.length > 15 ? `… and ${a.length - 15} more lines` : ''].filter(Boolean).join('\n');
|
|
155
|
-
if (red.length) return { name, status: 'failed', note: 'path specific to this machine; use import.meta.url, __dirname, path.join(root, …), os.tmpdir(). If deliberate, write kit:
|
|
160
|
+
if (red.length) return { name, status: 'failed', note: 'path specific to this machine; use import.meta.url, __dirname, path.join(root, …), os.tmpdir(). If deliberate, write kit:path-allowed on the line or the line above',
|
|
156
161
|
output: list(red) };
|
|
157
162
|
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) }) };
|
|
158
163
|
}
|
|
@@ -187,7 +192,7 @@ export function runGate(root) {
|
|
|
187
192
|
const list = approvedCommands(root);
|
|
188
193
|
if (!isApproved(root, list)) return { passed: false, text: `## Gate · ${approvalNeeded(list)}` };
|
|
189
194
|
const checks = [...list.filter((k) => CHECKS.includes(k.name)).map((k) => check(root, k)), semgrep(root, files)];
|
|
190
|
-
const empty = !checks.some((k) => k.status === 'passed');
|
|
195
|
+
const empty = !checks.some((k) => CHECKS.includes(k.name) && k.status === 'passed'); // Semgrep alone does not fill a gate
|
|
191
196
|
const spec = specCheck(root, start); // after the test command, which may have refreshed the JUnit report; it does not fill an empty gate
|
|
192
197
|
if (spec) checks.push(spec);
|
|
193
198
|
checks.push(absPathCheck(root, files)); // after "empty": it does not fill a gate with no lint/test either
|
|
@@ -198,7 +203,9 @@ export function runGate(root) {
|
|
|
198
203
|
const result = checks.some((k) => k.status === 'failed') ? 'Result: RED. The commit gate stays closed: fix it and run the gate again.'
|
|
199
204
|
: empty ? 'Result: EMPTY, no check ran. Fill in AGENTS.md → Commands; the commit gate stays closed (if the user wants, commit-gate.mjs --skip).'
|
|
200
205
|
: 'Result: GREEN.';
|
|
201
|
-
|
|
206
|
+
const unknown = unknownCommandKeys(readAgents(root));
|
|
207
|
+
const warning = unknown.length ? [`⚠ AGENTS.md → Commands has keys the gate does not read: ${unknown.join(', ')}. Use dev, preview, test, lint, type check, build.`] : [];
|
|
208
|
+
return { passed, text: [`## Gate · ${files.length} files changed`, ...checks.map(line), ...warning, result].join('\n') };
|
|
202
209
|
}
|
|
203
210
|
|
|
204
211
|
if (isMain(import.meta.url)) {
|
package/.claude/hooks/labels.mjs
CHANGED
|
@@ -1,174 +1,114 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Readers call normalize() first and then only know the English labels. Turkish reading goes away in 4.0.
|
|
1
|
+
// Labels and values hooks read from project files. They stay English in every project language.
|
|
3
2
|
|
|
4
3
|
export const LABELS = {
|
|
5
|
-
lastUpdated:
|
|
6
|
-
activePhase:
|
|
7
|
-
activeTask:
|
|
8
|
-
setupStep:
|
|
9
|
-
questionMode:
|
|
10
|
-
profile:
|
|
11
|
-
currentState:
|
|
12
|
-
doneLastSession:
|
|
13
|
-
nextStep:
|
|
14
|
-
pendingDecisions:
|
|
15
|
-
whereWeLeftOff:
|
|
16
|
-
openIssues:
|
|
17
|
-
status:
|
|
18
|
-
commitReview:
|
|
19
|
-
members:
|
|
20
|
-
meetingLog:
|
|
21
|
-
decisionLog:
|
|
22
|
-
phase:
|
|
23
|
-
commands:
|
|
24
|
-
dev:
|
|
25
|
-
preview:
|
|
26
|
-
typeCheck:
|
|
27
|
-
type:
|
|
28
|
-
class:
|
|
29
|
-
mustNotChange:
|
|
30
|
-
priority:
|
|
31
|
-
outOfScope:
|
|
32
|
-
chosenTech:
|
|
33
|
-
maintenanceLog:
|
|
34
|
-
flags:
|
|
35
|
-
records:
|
|
36
|
-
unreleased:
|
|
37
|
-
currency:
|
|
38
|
-
price:
|
|
39
|
-
paymentFee:
|
|
40
|
-
fixedCost:
|
|
41
|
-
variableCost:
|
|
42
|
-
targetUsers:
|
|
43
|
-
freeShare:
|
|
44
|
-
accounts:
|
|
45
|
-
inbox:
|
|
46
|
-
themes:
|
|
47
|
-
reports:
|
|
48
|
-
alarms:
|
|
49
|
-
chosenDirection:
|
|
50
|
-
valueProposition:
|
|
51
|
-
criticalFlows:
|
|
52
|
-
learnedRules:
|
|
53
|
-
rule:
|
|
54
|
-
environments:
|
|
4
|
+
lastUpdated: 'Last updated',
|
|
5
|
+
activePhase: 'Active phase',
|
|
6
|
+
activeTask: 'Active task',
|
|
7
|
+
setupStep: 'Setup step',
|
|
8
|
+
questionMode: 'Question mode',
|
|
9
|
+
profile: 'Profile',
|
|
10
|
+
currentState: 'Current state',
|
|
11
|
+
doneLastSession: 'Done last session',
|
|
12
|
+
nextStep: 'Next step',
|
|
13
|
+
pendingDecisions: 'Pending decisions',
|
|
14
|
+
whereWeLeftOff: 'Where we left off',
|
|
15
|
+
openIssues: 'Open issues / blockers',
|
|
16
|
+
status: 'Status',
|
|
17
|
+
commitReview: 'Commit review',
|
|
18
|
+
members: 'Members',
|
|
19
|
+
meetingLog: 'Meeting log',
|
|
20
|
+
decisionLog: 'Decision log',
|
|
21
|
+
phase: 'Phase',
|
|
22
|
+
commands: 'Commands',
|
|
23
|
+
dev: 'dev',
|
|
24
|
+
preview: 'preview',
|
|
25
|
+
typeCheck: 'type check',
|
|
26
|
+
type: 'Type',
|
|
27
|
+
class: 'Class',
|
|
28
|
+
mustNotChange: 'Behavior that must not change',
|
|
29
|
+
priority: 'Priority',
|
|
30
|
+
outOfScope: 'Out of scope',
|
|
31
|
+
chosenTech: 'Chosen technologies',
|
|
32
|
+
maintenanceLog: 'Maintenance log',
|
|
33
|
+
flags: 'Flags',
|
|
34
|
+
records: 'Records',
|
|
35
|
+
unreleased: 'Unreleased',
|
|
36
|
+
currency: 'Currency',
|
|
37
|
+
price: 'Price (per user, monthly)',
|
|
38
|
+
paymentFee: 'Payment fee (%)',
|
|
39
|
+
fixedCost: 'Fixed monthly cost',
|
|
40
|
+
variableCost: 'Variable cost per user (monthly)',
|
|
41
|
+
targetUsers: 'Target active users',
|
|
42
|
+
freeShare: 'Free user share (%)',
|
|
43
|
+
accounts: 'Accounts',
|
|
44
|
+
inbox: 'Inbox',
|
|
45
|
+
themes: 'Themes',
|
|
46
|
+
reports: 'Reports',
|
|
47
|
+
alarms: 'Alarms',
|
|
48
|
+
chosenDirection: 'Chosen direction',
|
|
49
|
+
valueProposition: 'Value proposition',
|
|
50
|
+
criticalFlows: 'Critical flows',
|
|
51
|
+
learnedRules: 'Learned rules',
|
|
52
|
+
rule: 'Rule',
|
|
53
|
+
environments: 'Environments (dev / prod)',
|
|
55
54
|
};
|
|
56
55
|
|
|
57
|
-
// Values hooks read at known label or column positions. Readers compare with isValue()
|
|
56
|
+
// Values hooks read at known label or column positions. Readers compare with isValue().
|
|
58
57
|
export const VALUES = {
|
|
59
|
-
on:
|
|
60
|
-
off:
|
|
61
|
-
none:
|
|
62
|
-
setup:
|
|
63
|
-
notSetUp:
|
|
64
|
-
setUp:
|
|
65
|
-
full:
|
|
66
|
-
light:
|
|
67
|
-
standard:
|
|
68
|
-
waiting:
|
|
69
|
-
automatic:
|
|
70
|
-
user:
|
|
71
|
-
oneWay:
|
|
72
|
-
twoWay:
|
|
73
|
-
yes:
|
|
74
|
-
no:
|
|
75
|
-
right:
|
|
76
|
-
wrong:
|
|
77
|
-
passed:
|
|
78
|
-
failed:
|
|
79
|
-
notRun:
|
|
80
|
-
bug:
|
|
81
|
-
feature:
|
|
82
|
-
request:
|
|
83
|
-
complaint:
|
|
84
|
-
praise:
|
|
85
|
-
question:
|
|
86
|
-
new:
|
|
87
|
-
planned:
|
|
88
|
-
done:
|
|
89
|
-
rejected:
|
|
90
|
-
merged:
|
|
58
|
+
on: 'on',
|
|
59
|
+
off: 'off',
|
|
60
|
+
none: 'none',
|
|
61
|
+
setup: 'Setup',
|
|
62
|
+
notSetUp: 'not set up',
|
|
63
|
+
setUp: 'set up',
|
|
64
|
+
full: 'Full',
|
|
65
|
+
light: 'Light',
|
|
66
|
+
standard: 'Standard',
|
|
67
|
+
waiting: 'waiting',
|
|
68
|
+
automatic: 'automatic',
|
|
69
|
+
user: 'user',
|
|
70
|
+
oneWay: 'one-way',
|
|
71
|
+
twoWay: 'two-way',
|
|
72
|
+
yes: 'yes',
|
|
73
|
+
no: 'no',
|
|
74
|
+
right: 'right',
|
|
75
|
+
wrong: 'wrong',
|
|
76
|
+
passed: 'passed',
|
|
77
|
+
failed: 'failed',
|
|
78
|
+
notRun: 'not run',
|
|
79
|
+
bug: 'bug',
|
|
80
|
+
feature: 'feature',
|
|
81
|
+
request: 'request',
|
|
82
|
+
complaint: 'complaint',
|
|
83
|
+
praise: 'praise',
|
|
84
|
+
question: 'question',
|
|
85
|
+
new: 'new',
|
|
86
|
+
planned: 'planned',
|
|
87
|
+
done: 'done',
|
|
88
|
+
rejected: 'rejected',
|
|
89
|
+
merged: 'merged',
|
|
91
90
|
};
|
|
92
91
|
|
|
93
|
-
// The value
|
|
94
|
-
// "açık · kapalı konular: yok" is on, "açıklama" is not.
|
|
92
|
+
// The value, case-insensitive, whole or followed by a non-letter: "on · closed topics: none" is on, "online" is not.
|
|
95
93
|
export function isValue(text, key) {
|
|
96
94
|
if (!Object.hasOwn(VALUES, key)) throw new Error(`unknown value: ${key}`);
|
|
97
95
|
if (typeof text !== 'string') return false;
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
return Object.values(VALUES[key]).flatMap(lower).some((w) =>
|
|
102
|
-
texts.some((s) => s.startsWith(w) && !/^[\p{L}\p{N}]/u.test(s.slice(w.length))));
|
|
96
|
+
const s = text.trim().toLowerCase();
|
|
97
|
+
const w = VALUES[key].toLowerCase();
|
|
98
|
+
return s.startsWith(w) && !/^[\p{L}\p{N}]/u.test(s.slice(w.length));
|
|
103
99
|
}
|
|
104
100
|
|
|
105
101
|
export function label(key) {
|
|
106
102
|
if (!Object.hasOwn(LABELS, key)) throw new Error(`unknown label: ${key}`);
|
|
107
|
-
return LABELS[key]
|
|
103
|
+
return LABELS[key];
|
|
108
104
|
}
|
|
109
105
|
|
|
110
|
-
const
|
|
111
|
-
const COMMAND_KEYS = ['dev', 'preview', 'typeCheck'];
|
|
112
|
-
// Longest first so a shorter label never wins over a longer one that starts the same way.
|
|
113
|
-
const PAIRS = Object.entries(LABELS).filter(([k]) => !COMMAND_KEYS.includes(k)).map(([, v]) => v)
|
|
114
|
-
.sort((a, b) => b.tr.length - a.tr.length);
|
|
115
|
-
const BOLD = PAIRS.map(({ tr, en }) => [new RegExp(`^\\*\\*${esc(tr)}:\\*\\*`), `**${en}:**`]);
|
|
116
|
-
// The label must end at a non-letter: "## Faz 1" changes, "## Fazla mesai" does not. A PRD heading may be numbered
|
|
117
|
-
// ("## 6. Kapsam dışı"), the CHANGELOG one is bracketed ("## [Yayınlanmamış]").
|
|
118
|
-
const HEADING = [
|
|
119
|
-
[new RegExp(`^## \\[${esc(LABELS.unreleased.tr)}\\]`), `## [${LABELS.unreleased.en}]`],
|
|
120
|
-
...PAIRS.map(({ tr, en }) => [new RegExp(`^## (\\d+\\. )?${esc(tr)}(?![\\p{L}\\p{N}])`, 'u'), `## $1${en}`]),
|
|
121
|
-
];
|
|
122
|
-
const COMMAND = COMMAND_KEYS.map((k) => [new RegExp(`^${esc(LABELS[k].tr)}:`), `${LABELS[k].en}:`]);
|
|
123
|
-
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
124
|
-
const COMMANDS_HEADING = /^## (Komutlar|Commands)(?![\p{L}\p{N}])/u;
|
|
106
|
+
const VERDICTS = [['CONDITIONAL', 'conditional'], ['APPROVE', 'approve'], ['REJECT', 'reject']];
|
|
125
107
|
|
|
126
|
-
|
|
127
|
-
for (const [re, to] of rules) if (re.test(line)) return line.replace(re, to);
|
|
128
|
-
return line;
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
// Rewrites Turkish labels to English only at label positions: a line-start **Label:**, a "## Label" heading,
|
|
132
|
-
// and the keys of the AGENTS.md Commands block. Prose, tables, other code blocks and values stay byte-identical.
|
|
133
|
-
export function normalize(text) {
|
|
134
|
-
const bom = text.startsWith('') ? '' : '';
|
|
135
|
-
const parts = text.slice(bom.length).split(/(\r?\n)/);
|
|
136
|
-
let fence = false;
|
|
137
|
-
let commands = false;
|
|
138
|
-
for (let i = 0; i < parts.length; i += 2) {
|
|
139
|
-
const line = parts[i];
|
|
140
|
-
if (FENCE.test(line)) { fence = !fence; continue; }
|
|
141
|
-
if (!fence && /^#{1,2} /.test(line)) commands = COMMANDS_HEADING.test(line);
|
|
142
|
-
if (commands) parts[i] = swap(line, [...COMMAND, ...HEADING]);
|
|
143
|
-
else if (!fence) parts[i] = swap(line, [...BOLD, ...HEADING]);
|
|
144
|
-
}
|
|
145
|
-
return bom + parts.join('');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// The label a writer uses for key in this file: the language the file already has for it (English if both, or neither).
|
|
149
|
-
// Same positions as normalize: a line-start **Label:** or a "## Label" heading outside code blocks.
|
|
150
|
-
export function labelIn(text, key) {
|
|
151
|
-
const { en, tr } = LABELS[key] ?? {};
|
|
152
|
-
if (!Object.hasOwn(LABELS, key)) throw new Error(`unknown label: ${key}`);
|
|
153
|
-
const at = (word) => new RegExp(`^(\\*\\*${esc(word)}:\\*\\*|## (\\d+\\. )?\\[?${esc(word)}(?![\\p{L}\\p{N}]))`, 'u');
|
|
154
|
-
const [isEn, isTr] = [at(en), at(tr)];
|
|
155
|
-
let fence = false, found = en;
|
|
156
|
-
for (const line of text.replace(/^/, '').split(/\r?\n/)) {
|
|
157
|
-
if (FENCE.test(line)) { fence = !fence; continue; }
|
|
158
|
-
if (fence) continue;
|
|
159
|
-
if (isEn.test(line)) return en;
|
|
160
|
-
if (isTr.test(line)) found = tr;
|
|
161
|
-
}
|
|
162
|
-
return found;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const VERDICTS = [['KOŞULLU ONAY', 'conditional'], ['CONDITIONAL', 'conditional'], ['KOŞULLU', 'conditional'],
|
|
166
|
-
['APPROVE', 'approve'], ['ONAY', 'approve'], ['REJECT', 'reject'], ['RET', 'reject']];
|
|
167
|
-
|
|
168
|
-
// Board result in either language. Fail-closed: anything that is not one clean verdict is 'unknown'.
|
|
108
|
+
// Board result. Fail-closed: anything that is not one clean verdict is 'unknown'.
|
|
169
109
|
export function verdict(text) {
|
|
170
110
|
if (typeof text !== 'string') return 'unknown';
|
|
171
|
-
// "**Result:**
|
|
111
|
+
// "**Result:** REJECT", "**REJECT**"; after the word only the end, a dash, a bracket or ": , ; ." ("REJECT: security")
|
|
172
112
|
const s = text.trim().replace(/^\*\*[^*]+:\*\*\s*/, '').replace(/^\*\*([^*]+)\*\*/, '$1').toUpperCase();
|
|
173
113
|
for (const [word, result] of VERDICTS) {
|
|
174
114
|
if (s.startsWith(word) && /^(?:\s*(?:$|[—–(-])|[:,;.])/.test(s.slice(word.length))) return result;
|