tillerhand 3.47.1 → 3.48.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/board-chair.md +9 -9
- package/.claude/agents/code-reviewer.md +2 -2
- package/.claude/agents/council-advisor.md +2 -2
- package/.claude/agents/security-auditor.md +6 -6
- package/.claude/agents/test-writer.md +1 -1
- package/.claude/board/board-ai.md +6 -6
- package/.claude/board/board-api.md +6 -6
- package/.claude/board/board-architect.md +7 -7
- package/.claude/board/board-devops.md +6 -6
- package/.claude/board/board-mobile.md +6 -6
- package/.claude/board/board-performance.md +6 -6
- package/.claude/board/board-privacy.md +6 -6
- package/.claude/board/board-product.md +7 -7
- package/.claude/board/board-qa.md +7 -7
- package/.claude/board/board-ux.md +6 -6
- package/.claude/board/council.md +32 -32
- package/.claude/board/rules.md +8 -8
- package/.claude/commands/adopt.md +10 -10
- package/.claude/commands/ask-mode.md +6 -5
- package/.claude/commands/board.md +1 -1
- package/.claude/commands/build.md +8 -8
- package/.claude/commands/design.md +8 -8
- package/.claude/commands/findings-all.md +2 -2
- package/.claude/commands/findings-security.md +1 -1
- package/.claude/commands/fix-bug.md +2 -2
- package/.claude/commands/handoff.md +12 -12
- package/.claude/commands/language.md +1 -1
- package/.claude/commands/maintain.md +2 -2
- package/.claude/commands/plan-task.md +3 -3
- package/.claude/commands/qa.md +5 -5
- package/.claude/commands/release.md +3 -3
- package/.claude/commands/research.md +2 -2
- package/.claude/commands/retro.md +2 -2
- package/.claude/commands/review-changes.md +2 -2
- package/.claude/commands/rollback.md +1 -1
- package/.claude/commands/start.md +20 -20
- package/.claude/commands/tools.md +8 -6
- package/.claude/commands/user-feedback.md +3 -3
- package/.claude/commands/wrap-up.md +3 -3
- package/.claude/findings/protocol.md +2 -2
- package/.claude/findings/report-template.md +1 -1
- package/.claude/hooks/commit-gate.mjs +11 -9
- package/.claude/hooks/council.mjs +51 -24
- package/.claude/hooks/dashboard-data.mjs +73 -53
- package/.claude/hooks/dashboard.mjs +80 -71
- package/.claude/hooks/destructive-guard.mjs +1 -1
- package/.claude/hooks/freeze-guard.mjs +1 -1
- package/.claude/hooks/gate.mjs +14 -11
- package/.claude/hooks/labels.mjs +177 -0
- package/.claude/hooks/messages.mjs +8 -7
- package/.claude/hooks/notify.mjs +2 -1
- package/.claude/hooks/prefs.mjs +24 -10
- package/.claude/hooks/preview.mjs +6 -6
- package/.claude/hooks/progress.mjs +27 -15
- package/.claude/hooks/qa-report.mjs +12 -10
- package/.claude/hooks/session-start.mjs +9 -7
- package/.claude/hooks/status-read.mjs +22 -18
- package/.claude/hooks/statusline.mjs +10 -8
- package/.claude/hooks/tools.mjs +13 -8
- package/.claude/output-styles/{anlatici.md → narrator.md} +2 -2
- package/.claude/skills/logo-design/SKILL.md +2 -2
- package/.claude/skills/source-research/SKILL.md +1 -1
- package/.claude/skills/ui-motion/SKILL.md +2 -2
- package/AGENTS.md +12 -12
- package/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/README.tr.md +3 -3
- package/docs/decisions/adr-template.md +1 -1
- package/docs/decisions/log.md +2 -2
- package/docs/findings/README.md +1 -1
- package/docs/operations/accounts.md +7 -7
- package/docs/operations/deploy-setup.md +3 -3
- package/docs/operations/feature-flags.md +2 -2
- package/docs/operations/maintenance.md +1 -1
- package/docs/operations/release-checklist.md +2 -2
- package/docs/operations/spend-alarms.md +2 -2
- package/docs/product/feedback.md +4 -4
- package/docs/product/launch.md +1 -1
- package/docs/product/prd.md +10 -10
- package/docs/product/roadmap.md +8 -8
- package/docs/product/specs/_template.md +8 -8
- package/docs/product/unit-economics.md +10 -10
- package/docs/quality/metrics.md +2 -2
- package/docs/quality/test-plan.md +5 -5
- package/docs/security/checklist.md +2 -2
- package/docs/workflow/board.md +5 -5
- package/docs/workflow/cost-log.md +1 -1
- package/docs/workflow/decision-protocol.md +18 -16
- package/docs/workflow/learning.md +2 -2
- package/docs/workflow/mcp-catalog.md +1 -1
- package/docs/workflow/models.md +1 -1
- package/docs/workflow/progress.md +14 -14
- package/docs/workflow/skills.md +9 -9
- package/docs/workflow/usage-limits.md +5 -5
- package/kit/CHANGELOG.md +28 -0
- package/kit/VERSION +1 -1
- package/kit/guide.html +21 -21
- package/kit/guide.tr.html +21 -21
- package/kit/kit.mjs +29 -0
- package/kit/project/README.md +1 -1
- 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/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 +1 -1
- package/kit/templates/backend/.pre-commit-config.yaml +1 -1
- package/kit/templates/backend/docs/architecture/stack.md +7 -7
- package/kit/templates/backend/docs/architecture/structure.md +2 -2
- package/kit/templates/mobile/.github/workflows/ci.yml +4 -4
- 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 +5 -5
- package/kit/templates/mobile/docs/architecture/structure.md +1 -1
- package/kit/templates/mobile/docs/design/design-guide.md +1 -1
- package/kit/templates/mobile/tests/security/rls.mjs +4 -4
- package/kit/templates/web-saas/.github/workflows/ci.yml +4 -4
- 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 +1 -1
- package/kit/templates/web-saas/docs/architecture/stack.md +12 -12
- package/kit/templates/web-saas/docs/architecture/structure.md +2 -2
- package/kit/templates/web-saas/docs/design/design-guide.md +1 -1
- package/kit/templates/web-saas/tests/security/rls.mjs +4 -4
- package/package.json +1 -1
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { execFileSync } from 'node:child_process';
|
|
6
|
-
import { readText, sections, tableRows, labels } from './status-read.mjs';
|
|
6
|
+
import { readText, sections, findSection, tableRows, labels } from './status-read.mjs';
|
|
7
|
+
import { normalize, isValue, VALUES } from './labels.mjs';
|
|
7
8
|
import { t } from './lang.mjs';
|
|
8
9
|
|
|
9
10
|
const DAY = 864e5;
|
|
@@ -11,13 +12,13 @@ const isDate = (s) => /^\d{4}-\d{2}-\d{2}$/.test(String(s).trim());
|
|
|
11
12
|
export const daysLeft = (date, now = Date.now()) => Math.ceil((Date.parse(`${date}T00:00:00`) - now) / DAY);
|
|
12
13
|
|
|
13
14
|
// ---------- acceptance tests ----------
|
|
14
|
-
// A criterion id is "
|
|
15
|
-
// Not after a letter: "ISO-27.1" or "
|
|
16
|
-
const CRITERION_ID = /(?<![A-Za-z])[ÖO][-_](\d{2})[._](\d+)/g;
|
|
15
|
+
// A criterion id is "AC-01.1" in the PRD and "AC-01.1" or "AC_01_1" in tests; before 3.48 "Ö-01.1" / "O_01_1".
|
|
16
|
+
// All four normalize to the key "O_01_1". Not after a letter: "ISO-27.1", "DEMO_01_2" or "MAC_01_1" is not a criterion
|
|
17
|
+
const CRITERION_ID = /(?<![A-Za-z])(?:AC|[ÖO])[-_](\d{2})[._](\d+)/g;
|
|
17
18
|
const key = (m1, m2) => `O_${m1}_${m2}`;
|
|
18
19
|
|
|
19
20
|
export function prdCriteria(prdText) {
|
|
20
|
-
return [...prdText.matchAll(/^\s*- \[( |x)\]\s*(
|
|
21
|
+
return [...prdText.matchAll(/^\s*- \[( |x)\]\s*((?:AC|Ö)-(\d{2})\.(\d+))\s*:?\s*(.*)$/gim)]
|
|
21
22
|
.filter((m) => !m[5].startsWith('['))
|
|
22
23
|
.map((m) => ({ id: m[2], key: key(m[3], m[4]), text: m[5].trim(), checked: m[1].toLowerCase() === 'x' }));
|
|
23
24
|
}
|
|
@@ -81,8 +82,8 @@ export function junitResults(xmlTexts) {
|
|
|
81
82
|
export function qaSummary(text) {
|
|
82
83
|
const rows = tableRows(text, 4).map(([criterion, result, evidence, note]) => ({ criterion, result, evidence, note }));
|
|
83
84
|
if (!rows.length) return null;
|
|
84
|
-
const count = (
|
|
85
|
-
return { passed: count('
|
|
85
|
+
const count = (k) => rows.filter((x) => isValue(x.result, k)).length;
|
|
86
|
+
return { passed: count('passed'), failed: count('failed'), notRun: count('notRun'), failures: rows.filter((x) => isValue(x.result, 'failed')) };
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
function junitFiles(root) {
|
|
@@ -111,21 +112,21 @@ export function acceptanceStatus(root) {
|
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
// "Must not change" lines of the specs (F-23): each
|
|
115
|
+
// "Must not change" lines of the specs (F-23): each AC id, whether a test carries it and its JUnit result.
|
|
115
116
|
// since: only reports written at or after this time count (the gate passes its own start: an old red report must not
|
|
116
117
|
// stop a commit whose tests pass now). Files starting with "_" (the template) are skipped. unreadable: lines that name
|
|
117
|
-
// an id but not in the "-
|
|
118
|
+
// an id but not in the "- AC-01.1: ..." form, so the check would silently skip them.
|
|
118
119
|
export function specInvariants(root, since = 0) {
|
|
119
120
|
const dir = path.join(root, 'docs/product/specs');
|
|
120
121
|
let files = [];
|
|
121
122
|
try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.md') && !f.startsWith('_')).sort(); } catch { return { invariants: [], unreadable: [] }; }
|
|
122
123
|
const list = [], unreadable = [];
|
|
123
124
|
for (const f of files) {
|
|
124
|
-
const body = sections(readText(path.join(dir, f)))['
|
|
125
|
+
const body = sections(readText(path.join(dir, f)))['Behavior that must not change'] ?? '';
|
|
125
126
|
for (const line of body.split(/\r?\n/)) {
|
|
126
|
-
const m = line.match(/^\s*-\s*(
|
|
127
|
+
const m = line.match(/^\s*-\s*((?:AC|Ö)-(\d{2})\.(\d+))/);
|
|
127
128
|
if (m) list.push({ spec: f, id: m[1], key: key(m[2], m[3]) });
|
|
128
|
-
else if (/[ÖO][-_]\d{2}[._]\d+/.test(line)) unreadable.push({ spec: f, line: line.trim() });
|
|
129
|
+
else if (/(?<![A-Za-z])(?:AC|[ÖO])[-_]\d{2}[._]\d+/.test(line)) unreadable.push({ spec: f, line: line.trim() });
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
if (!list.length) return { invariants: [], unreadable };
|
|
@@ -135,9 +136,11 @@ export function specInvariants(root, since = 0) {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
// Bug classes seen in two or more bug specs (F-21): a repeat is the cue to propose a rule. Only the fixed list counts
|
|
138
|
-
// ("diğer" and free text never do). A group closes once every spec has a "**
|
|
139
|
+
// ("diğer"/"other" and free text never do). A group closes once every spec has a "**Rule:**" answer (rule added or not needed);
|
|
139
140
|
// a new undecided spec in a decided class opens it again (the rule did not prevent the bug).
|
|
140
|
-
export const BUG_CLASSES = ['
|
|
141
|
+
export const BUG_CLASSES = ['empty-input', 'validation', 'authorization', 'date-time', 'locale', 'sorting', 'concurrency', 'configuration', 'external-service', 'contract', 'ui-state'];
|
|
142
|
+
// Pre-3.48 specs name the class in Turkish, in the same order; both count as one class under the English name
|
|
143
|
+
const BUG_CLASSES_TR = ['boş-girdi', 'doğrulama', 'yetki', 'tarih-saat', 'yerel-dil', 'sıralama', 'eşzamanlılık', 'yapılandırma', 'dış-servis', 'sözleşme', 'arayüz-durumu'];
|
|
141
144
|
export function repeatedBugClasses(root) {
|
|
142
145
|
const dir = path.join(root, 'docs/product/specs');
|
|
143
146
|
let files = [];
|
|
@@ -145,12 +148,13 @@ export function repeatedBugClasses(root) {
|
|
|
145
148
|
const byClass = new Map();
|
|
146
149
|
for (const f of files) {
|
|
147
150
|
const text = readText(path.join(dir, f));
|
|
148
|
-
if (
|
|
149
|
-
const raw = text.match(/^\*\*
|
|
151
|
+
if (!isValue(text.match(/^\*\*Type:\*\*[ \t]*(.*)$/im)?.[1], 'bug')) continue;
|
|
152
|
+
const raw = text.match(/^\*\*Class:\*\*[ \t]*`?([^\s`,.[\]]+)/im)?.[1] ?? '';
|
|
150
153
|
// YETKI and YETKİ both mean yetki: try the Turkish and the plain lowercase
|
|
151
|
-
const cls = [raw.toLocaleLowerCase('tr'), raw.toLowerCase()]
|
|
154
|
+
const cls = [raw.toLocaleLowerCase('tr'), raw.toLowerCase()]
|
|
155
|
+
.map((c) => (BUG_CLASSES.includes(c) ? c : BUG_CLASSES[BUG_CLASSES_TR.indexOf(c)])).find(Boolean);
|
|
152
156
|
if (!cls) continue;
|
|
153
|
-
const decided = /^\*\*
|
|
157
|
+
const decided = /^\*\*Rule:\*\*[ \t]*[^\s[]/im.test(text);
|
|
154
158
|
byClass.set(cls, [...(byClass.get(cls) ?? []), { f, decided }]);
|
|
155
159
|
}
|
|
156
160
|
return [...byClass].filter(([, list]) => list.length > 1 && list.some((s) => !s.decided)).map(([cls, list]) => ({ cls, specs: list.map((s) => s.f) }));
|
|
@@ -158,19 +162,19 @@ export function repeatedBugClasses(root) {
|
|
|
158
162
|
|
|
159
163
|
// ---------- accounts and flags ----------
|
|
160
164
|
export function accounts(text, now = Date.now()) {
|
|
161
|
-
return tableRows(sections(text)['
|
|
165
|
+
return tableRows(sections(normalize(text))['Accounts'], 8)
|
|
162
166
|
.filter(([service]) => service && !service.startsWith('['))
|
|
163
167
|
.map(([service, purpose, owner, twoFactor, plan, expiry, keyRotation]) => {
|
|
164
|
-
const dates = [['
|
|
168
|
+
const dates = [['expiry', expiry], ['keyRotation', keyRotation]].filter(([, t]) => isDate(t))
|
|
165
169
|
.map(([kind, t]) => ({ kind, date: t, left: daysLeft(t, now) }));
|
|
166
|
-
return { service, purpose, owner, twoFactor, plan, dates, noTwoFactor:
|
|
170
|
+
return { service, purpose, owner, twoFactor, plan, dates, noTwoFactor: isValue(twoFactor, 'no') || isValue(twoFactor, 'none') };
|
|
167
171
|
});
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
// Dev/prod split and the RLS attacker test: the prod ref must not be in the AI's MCP config; with Supabase an RLS test must exist
|
|
171
175
|
export function envWarnings(root) {
|
|
172
176
|
const u = [];
|
|
173
|
-
const prodRef = tableRows(sections(readText(path.join(root, 'docs/operations/accounts.md')))['
|
|
177
|
+
const prodRef = tableRows(sections(readText(path.join(root, 'docs/operations/accounts.md')))['Environments (dev / prod)'], 4)
|
|
174
178
|
// A real ref (Supabase: 20 characters); "-", "yok", "n/a" would match anywhere in .mcp.json
|
|
175
179
|
.find(([environment, , ref]) => environment === 'prod' && /^[a-z0-9][a-z0-9._:/-]{7,}$/i.test(ref ?? ''))?.[2];
|
|
176
180
|
if (prodRef && readText(path.join(root, '.mcp.json')).includes(prodRef)) {
|
|
@@ -183,24 +187,24 @@ export function envWarnings(root) {
|
|
|
183
187
|
}
|
|
184
188
|
|
|
185
189
|
export function flags(text, now = Date.now()) {
|
|
186
|
-
return tableRows(sections(text)['
|
|
190
|
+
return tableRows(sections(normalize(text))['Flags'], 7).filter(([name]) => name)
|
|
187
191
|
.map(([name, purpose, state, owner, added, removal, killSwitchTested]) => ({
|
|
188
192
|
name, purpose, state, owner, added, removal, killSwitchTested,
|
|
189
193
|
isOverdue: isDate(removal) && daysLeft(removal, now) < 0,
|
|
190
|
-
untested:
|
|
194
|
+
untested: !(isValue(killSwitchTested, 'yes') || /^(✅|var\b)/i.test(killSwitchTested ?? '')),
|
|
191
195
|
}));
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
// ---------- unit economics ----------
|
|
195
199
|
export function unitEconomics(text) {
|
|
196
|
-
const e = labels(text);
|
|
200
|
+
const e = labels(normalize(text));
|
|
197
201
|
const s = (k) => { const v = parseFloat(String(e[k] ?? '').replace(/[^\d.,-]/g, '').replace(',', '.')); return Number.isFinite(v) ? v : 0; };
|
|
198
|
-
const price = s('
|
|
199
|
-
const commission = s('
|
|
200
|
-
const fixedCost = s('
|
|
201
|
-
const variableCost = s('
|
|
202
|
-
const target = s('
|
|
203
|
-
const freeShare = Math.min(99, s('
|
|
202
|
+
const price = s('Price (per user, monthly)');
|
|
203
|
+
const commission = s('Payment fee (%)');
|
|
204
|
+
const fixedCost = s('Fixed monthly cost');
|
|
205
|
+
const variableCost = s('Variable cost per user (monthly)');
|
|
206
|
+
const target = s('Target active users');
|
|
207
|
+
const freeShare = Math.min(99, s('Free user share (%)'));
|
|
204
208
|
if (!price && !fixedCost && !variableCost) return null;
|
|
205
209
|
const margin = price * (1 - commission / 100) - variableCost;
|
|
206
210
|
// Every paying user also carries the variable cost of the free users
|
|
@@ -210,7 +214,7 @@ export function unitEconomics(text) {
|
|
|
210
214
|
const revenue = paying * price * (1 - commission / 100);
|
|
211
215
|
const cost = fixedCost + target * variableCost;
|
|
212
216
|
return {
|
|
213
|
-
currency: e['
|
|
217
|
+
currency: e['Currency'] || '',
|
|
214
218
|
price, margin, effectiveMargin,
|
|
215
219
|
breakEven: effectiveMargin > 0 ? Math.ceil(fixedCost / effectiveMargin) : null,
|
|
216
220
|
target, revenue, cost, profit: revenue - cost,
|
|
@@ -219,19 +223,24 @@ export function unitEconomics(text) {
|
|
|
219
223
|
}
|
|
220
224
|
|
|
221
225
|
// ---------- feedback and metrics ----------
|
|
226
|
+
const FEEDBACK_KINDS = ['bug', 'request', 'complaint', 'praise', 'question'];
|
|
227
|
+
const FEEDBACK_STATES = ['new', 'planned', 'done', 'rejected', 'merged'];
|
|
228
|
+
// A known value in either language becomes the English one; anything else stays as written
|
|
229
|
+
const inEnglish = (v, keys) => VALUES[keys.find((k) => isValue(v, k))]?.en ?? v;
|
|
222
230
|
export function feedback(text) {
|
|
223
|
-
const b = sections(text);
|
|
224
|
-
const records = tableRows(b['
|
|
225
|
-
|
|
231
|
+
const b = sections(normalize(text));
|
|
232
|
+
const records = tableRows(b['Inbox'], 9).map(([no, date, source, user, summary, kind, theme, priority, state]) =>
|
|
233
|
+
({ no, date, summary, kind: inEnglish(kind, FEEDBACK_KINDS), theme, priority, state: inEnglish(state, FEEDBACK_STATES) }));
|
|
234
|
+
const themes = tableRows(b['Themes'], 3).map(([theme, count, feature, decision]) => ({ theme, count: parseInt(count, 10) || 0, feature, decision }))
|
|
226
235
|
.sort((x, y) => y.count - x.count);
|
|
227
|
-
return { records, themes, newCount: records.filter((k) =>
|
|
236
|
+
return { records, themes, newCount: records.filter((k) => k.state === 'new').length };
|
|
228
237
|
}
|
|
229
238
|
|
|
230
239
|
// Comprehension check answers (F-22, narrator mode); null without rows so the card is left out
|
|
231
240
|
export function learning(text) {
|
|
232
|
-
// Only
|
|
233
|
-
const rows = tableRows(sections(text)['
|
|
234
|
-
.map(([date, task, concept, result]) => ({ concept, right:
|
|
241
|
+
// Only right / wrong rows count; a skipped or malformed answer is not a wrong one
|
|
242
|
+
const rows = tableRows(sections(normalize(text))['Records'], 4).filter(([, , , result]) => isValue(result, 'right') || isValue(result, 'wrong'))
|
|
243
|
+
.map(([date, task, concept, result]) => ({ concept, right: isValue(result, 'right') }));
|
|
235
244
|
if (!rows.length) return null;
|
|
236
245
|
const missed = new Map();
|
|
237
246
|
for (const r of rows) if (!r.right) missed.set(r.concept, (missed.get(r.concept) ?? 0) + 1);
|
|
@@ -240,17 +249,24 @@ export function learning(text) {
|
|
|
240
249
|
}
|
|
241
250
|
|
|
242
251
|
export function metrics(text) {
|
|
243
|
-
return tableRows(sections(text)['
|
|
252
|
+
return tableRows(sections(normalize(text))['Records'], 9)
|
|
244
253
|
.map(([period, taskCount, duration, limit, fixes, reverts, duplicates, critical, acceptance]) => ({ period, taskCount, duration, limit, fixes, reverts, duplicates, critical, acceptance }));
|
|
245
254
|
}
|
|
246
255
|
|
|
247
256
|
// ---------- finding reports ----------
|
|
248
|
-
export const FINDING_KINDS = { tech: '
|
|
257
|
+
export const FINDING_KINDS = { tech: 'Tech', business: 'Business', security: 'Security', ux: 'UX', perf: 'Performance', legal: 'Legal' };
|
|
258
|
+
// Pre-3.48 report tables name the area in Turkish
|
|
259
|
+
const FINDING_KINDS_TR = { tech: 'Teknik', business: 'İş', security: 'Güvenlik', ux: 'UX', perf: 'Performans', legal: 'Hukuk' };
|
|
260
|
+
const findingKind = (v) => {
|
|
261
|
+
const low = [v.toLocaleLowerCase('tr'), v.toLowerCase()];
|
|
262
|
+
const named = Object.keys(FINDING_KINDS).find((k) => [k, FINDING_KINDS[k], FINDING_KINDS_TR[k]].some((n) => low.includes(n.toLocaleLowerCase('tr')) || low.includes(n.toLowerCase())));
|
|
263
|
+
return named ?? low[1];
|
|
264
|
+
};
|
|
249
265
|
export function findingReports(text) {
|
|
250
266
|
const count = (x) => { const n = parseInt(String(x ?? '').trim().replace(/^[^\d-]+/, ''), 10); return Number.isFinite(n) ? n : null; };
|
|
251
|
-
const rows = tableRows(sections(text)['
|
|
267
|
+
const rows = tableRows(sections(normalize(text))['Reports'], 7)
|
|
252
268
|
.map(([date, kind, health, previous, findings, critical, report]) => ({
|
|
253
|
-
date, kind: kind
|
|
269
|
+
date, kind: findingKind(kind), health: count(health), previous: count(previous), findings: count(findings), critical: count(critical) ?? 0, report,
|
|
254
270
|
}))
|
|
255
271
|
.filter((r) => r.date && r.health != null);
|
|
256
272
|
const latest = {};
|
|
@@ -283,7 +299,7 @@ export function producedAssets(root) {
|
|
|
283
299
|
|
|
284
300
|
// ---------- Spend alarms (docs/operations/spend-alarms.md) ----------
|
|
285
301
|
export function spendAlarms(text, now = Date.now()) {
|
|
286
|
-
return tableRows(sections(text)['
|
|
302
|
+
return tableRows(sections(normalize(text))['Alarms'], 6).filter(([service]) => service)
|
|
287
303
|
.map(([service, kind, amount, thresholds, where, lastCheck]) => ({
|
|
288
304
|
service, kind, amount, thresholds, where, lastCheck,
|
|
289
305
|
unset: !/\d/.test(amount ?? ''),
|
|
@@ -300,17 +316,21 @@ export function councilMinutes(root) {
|
|
|
300
316
|
const text = readText(path.join(folder, f));
|
|
301
317
|
const a = {};
|
|
302
318
|
for (const [, k, v] of text.matchAll(/\*\*([^*:]+):\*\*\s*([^·\n]*)/g)) a[k.trim()] ??= v.trim();
|
|
303
|
-
|
|
304
|
-
const
|
|
319
|
+
// English minutes, or Turkish ones written before 3.48 (they stay as they are: a council record is history)
|
|
320
|
+
const pick = (en, tr) => a[en] ?? a[tr];
|
|
321
|
+
const b = sections(text);
|
|
322
|
+
const ranking = tableRows(findSection(b, '3.'), 3).filter(([g]) => /^[A-Z]$/.test(g));
|
|
323
|
+
const idsByLetter = Object.fromEntries([...(b['IDs'] ?? b['Kimlikler'] ?? '').matchAll(/([A-Z])\s*=\s*([^·\n]+)/g)].map((m) => [m[1], m[2].trim()]));
|
|
324
|
+
const consensus = pick('Consensus', 'Uzlaşma')?.match(/\d+/)?.[0];
|
|
305
325
|
return {
|
|
306
326
|
file: `docs/workflow/council/${f}`,
|
|
307
|
-
topic: text.match(/^#\s+Konsey:\s*(.+)$/m)?.[1] ?? f.replace(/\.md$/, ''),
|
|
308
|
-
date:
|
|
309
|
-
kind:
|
|
310
|
-
decision:
|
|
311
|
-
consensus:
|
|
312
|
-
disputed: Number(
|
|
313
|
-
eliminated: Number(
|
|
327
|
+
topic: text.match(/^#\s+(?:Council|Konsey):\s*(.+)$/m)?.[1] ?? f.replace(/\.md$/, ''),
|
|
328
|
+
date: pick('Date', 'Tarih') ?? f.slice(0, 10),
|
|
329
|
+
kind: pick('Type', 'Tür') ?? '-',
|
|
330
|
+
decision: pick('Decision', 'Karar') || pick('Recommendation', 'Öneri') || null,
|
|
331
|
+
consensus: consensus ? Number(consensus) : null,
|
|
332
|
+
disputed: Number(pick('Disputed', 'Tartışmalı') ?? 0) || 0,
|
|
333
|
+
eliminated: Number(pick('Eliminated', 'Elenen') ?? 0) || 0,
|
|
314
334
|
ranking: ranking.map(([g, avg]) => ({ opinion: g, who: idsByLetter[g] ?? null, average: avg })),
|
|
315
335
|
};
|
|
316
336
|
}).sort((x, y) => y.date.localeCompare(x.date) || y.file.localeCompare(x.file));
|