katman-mcp 0.1.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/LICENSE +13 -0
- package/README.md +160 -0
- package/dist/cli.js +71 -0
- package/dist/context.js +43 -0
- package/dist/crawlers.js +86 -0
- package/dist/index.js +25 -0
- package/dist/license.js +448 -0
- package/dist/meta.js +28 -0
- package/dist/net.js +128 -0
- package/dist/server.js +17 -0
- package/dist/state.js +81 -0
- package/dist/tools/account/account.js +64 -0
- package/dist/tools/account/cli.js +21 -0
- package/dist/tools/account/index.js +48 -0
- package/dist/tools/account/sync.js +142 -0
- package/dist/tools/account/upsell.js +290 -0
- package/dist/tools/crawl/audit/analyze.js +719 -0
- package/dist/tools/crawl/audit/bots.js +114 -0
- package/dist/tools/crawl/audit/checks.js +126 -0
- package/dist/tools/crawl/audit/html.js +382 -0
- package/dist/tools/crawl/audit/index.js +364 -0
- package/dist/tools/crawl/audit/model.js +1 -0
- package/dist/tools/crawl/audit/report.js +199 -0
- package/dist/tools/crawl/audit/robots.js +199 -0
- package/dist/tools/crawl/audit/sitemap.js +147 -0
- package/dist/tools/crawl/cli.js +106 -0
- package/dist/tools/crawl/common/advice.js +61 -0
- package/dist/tools/crawl/common/findings.js +57 -0
- package/dist/tools/crawl/common/routes.js +80 -0
- package/dist/tools/crawl/common/text.js +89 -0
- package/dist/tools/crawl/common/urls.js +64 -0
- package/dist/tools/crawl/index.js +226 -0
- package/dist/tools/crawl/indexnow.js +271 -0
- package/dist/tools/crawl/keywords.js +178 -0
- package/dist/tools/crawl/scan/framework.js +645 -0
- package/dist/tools/crawl/scan/index.js +175 -0
- package/dist/tools/crawl/scan/project.js +180 -0
- package/dist/tools/crawl/scan/routes.js +354 -0
- package/dist/tools/crawl/scan/signals.js +414 -0
- package/dist/tools/method/cli.js +42 -0
- package/dist/tools/method/core/brief.js +302 -0
- package/dist/tools/method/core/extract.js +435 -0
- package/dist/tools/method/core/generate.js +504 -0
- package/dist/tools/method/core/lint.js +556 -0
- package/dist/tools/method/core/plan.js +405 -0
- package/dist/tools/method/core/prompt.js +150 -0
- package/dist/tools/method/core/questions.js +196 -0
- package/dist/tools/method/core/report.js +182 -0
- package/dist/tools/method/core/research-run.js +148 -0
- package/dist/tools/method/core/research.js +448 -0
- package/dist/tools/method/core/snippets.js +292 -0
- package/dist/tools/method/core/start.js +277 -0
- package/dist/tools/method/data/anatomy.js +141 -0
- package/dist/tools/method/data/appendices.js +202 -0
- package/dist/tools/method/data/cloudflare.js +192 -0
- package/dist/tools/method/data/evidence.js +137 -0
- package/dist/tools/method/data/geo-jobs.js +128 -0
- package/dist/tools/method/data/index.js +19 -0
- package/dist/tools/method/data/landing.js +67 -0
- package/dist/tools/method/data/lessons.js +127 -0
- package/dist/tools/method/data/measurement.js +142 -0
- package/dist/tools/method/data/method.js +141 -0
- package/dist/tools/method/data/offsite.js +148 -0
- package/dist/tools/method/data/pages.js +167 -0
- package/dist/tools/method/data/prompts.js +633 -0
- package/dist/tools/method/data/publish.js +113 -0
- package/dist/tools/method/data/render.js +117 -0
- package/dist/tools/method/data/research.js +131 -0
- package/dist/tools/method/data/roadmap.js +105 -0
- package/dist/tools/method/data/traps.js +74 -0
- package/dist/tools/method/data/verify.js +88 -0
- package/dist/tools/method/data/viral-list.js +39 -0
- package/dist/tools/method/index.js +209 -0
- package/dist/tools/method/lib/calls.js +53 -0
- package/dist/tools/method/lib/framework.js +525 -0
- package/dist/tools/method/lib/l10n.js +17 -0
- package/dist/tools/method/lib/routes.js +118 -0
- package/dist/tools/method/lib/state.js +454 -0
- package/dist/tools/method/lib/text.js +165 -0
- package/dist/tools/method/mcp/prompts.js +79 -0
- package/dist/tools/method/mcp/resources.js +50 -0
- package/dist/tools/visibility/analyze.js +463 -0
- package/dist/tools/visibility/cli.js +86 -0
- package/dist/tools/visibility/estimate.js +105 -0
- package/dist/tools/visibility/index.js +108 -0
- package/dist/tools/visibility/judge.js +156 -0
- package/dist/tools/visibility/manual.js +275 -0
- package/dist/tools/visibility/models.js +277 -0
- package/dist/tools/visibility/monitoring.js +165 -0
- package/dist/tools/visibility/openrouter.js +449 -0
- package/dist/tools/visibility/render.js +295 -0
- package/dist/tools/visibility/run.js +684 -0
- package/dist/tools/visibility/summary.js +164 -0
- package/dist/types.js +3 -0
- package/dist/util.js +92 -0
- package/package.json +54 -0
- package/server.json +34 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { logEvent, readText, writeJson, writeText, readJson } from "../../../state.js";
|
|
2
|
+
import { mdTable } from "../../../util.js";
|
|
3
|
+
import { calls } from "../lib/calls.js";
|
|
4
|
+
import { fileHints, isCsr, pageLocation } from "../lib/framework.js";
|
|
5
|
+
import { t } from "../lib/l10n.js";
|
|
6
|
+
import { appPrefixes, findPage, knownPublicPaths, noindexRoutes, pathKnown } from "../lib/routes.js";
|
|
7
|
+
import { loadConfigSafe, loadLatestAudit, loadResearchSafe, loadRuns, loadScan, runDate, staticDirFor } from "../lib/state.js";
|
|
8
|
+
import { isFilled, isoDay, slugify } from "../lib/text.js";
|
|
9
|
+
import { PAGE_TYPES } from "../data/pages.js";
|
|
10
|
+
import { PROMPTS } from "../data/prompts.js";
|
|
11
|
+
import { FIRST_30_DAYS } from "../data/roadmap.js";
|
|
12
|
+
import { MEASURE_FREQUENCY, API_PROXY_NOTE } from "../data/measurement.js";
|
|
13
|
+
import { researchCompleteness } from "./research.js";
|
|
14
|
+
import { keySituationQuestionIds } from "./questions.js";
|
|
15
|
+
import { siteUrlOf, situationUrl, verifyCalls } from "./prompt.js";
|
|
16
|
+
const SEV_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
17
|
+
/** `- [x] … <!-- id:P2 -->` → Map(P2 → true). */
|
|
18
|
+
export function parsePlanChecks(md) {
|
|
19
|
+
const m = new Map();
|
|
20
|
+
if (!md)
|
|
21
|
+
return m;
|
|
22
|
+
for (const line of md.split(/\r?\n/)) {
|
|
23
|
+
const r = /^\s*[-*]\s+\[([ xX])\]\s.*<!--\s*id:([^\s>]+)\s*-->/.exec(line);
|
|
24
|
+
if (r)
|
|
25
|
+
m.set(r[2], r[1].toLowerCase() === "x");
|
|
26
|
+
}
|
|
27
|
+
return m;
|
|
28
|
+
}
|
|
29
|
+
/** Display a call without the machine-specific project_root. */
|
|
30
|
+
export function fmtCallShort(c) {
|
|
31
|
+
const { project_root: _root, ...rest } = c.args;
|
|
32
|
+
void _root;
|
|
33
|
+
return `\`${c.tool}\`${Object.keys(rest).length ? ` \`${JSON.stringify(rest)}\`` : ""}`;
|
|
34
|
+
}
|
|
35
|
+
function weekOf(id) {
|
|
36
|
+
const p = PROMPTS.find((x) => x.id === id);
|
|
37
|
+
return (p?.week ?? 2);
|
|
38
|
+
}
|
|
39
|
+
export function buildPlanData(c) {
|
|
40
|
+
const lang = c.lang;
|
|
41
|
+
const L = (en, tr) => (lang === "tr" ? tr : en);
|
|
42
|
+
const r = c.research;
|
|
43
|
+
const scan = c.scan;
|
|
44
|
+
const audit = c.audit;
|
|
45
|
+
const sd = c.staticDir ?? "";
|
|
46
|
+
const siteUrl = siteUrlOf(r, c.config);
|
|
47
|
+
const now = c.now ?? new Date();
|
|
48
|
+
const primary = (r?.site.languages[0] ?? "en").toLowerCase().startsWith("tr") ? "tr" : "en";
|
|
49
|
+
const known = knownPublicPaths(scan, audit);
|
|
50
|
+
const langs = r?.site.languages ?? [];
|
|
51
|
+
const surl = (s) => situationUrl(s, langs);
|
|
52
|
+
const notes = [];
|
|
53
|
+
// ------------------------------------------------------------ page plan
|
|
54
|
+
const pages = [];
|
|
55
|
+
const noEvidence = !scan && !audit;
|
|
56
|
+
const statusOf = (src) => (src === "live" ? "live" : src === "scan" ? "in code" : noEvidence ? "unknown" : "missing");
|
|
57
|
+
const typeDef = (k) => PAGE_TYPES.find((p) => p.key === k);
|
|
58
|
+
const add = (key, matchKey, fallbackUrl, note) => {
|
|
59
|
+
const def = typeDef(key);
|
|
60
|
+
const found = matchKey ? findPage(known, matchKey) : undefined;
|
|
61
|
+
const url = found?.path ?? fallbackUrl;
|
|
62
|
+
const status = statusOf(found?.source);
|
|
63
|
+
pages.push({ key, name: t(def.name, lang), url, status, schema: def.schema, note, ...(status === "missing" && scan ? { file: pageLocation(scan, url, sd).files[0] } : {}) });
|
|
64
|
+
};
|
|
65
|
+
add("home", "home", "/", L("States who you are in one sentence (the category in the title); links to every important page.", "Kim olduğunuzu tek cümleyle söyler (title’da kategori); bütün önemli sayfalara link verir."));
|
|
66
|
+
for (const s of r?.situations ?? []) {
|
|
67
|
+
const url = surl(s);
|
|
68
|
+
const src = s.status === "live" ? "live" : pathKnown(known, url);
|
|
69
|
+
pages.push({
|
|
70
|
+
key: "situation",
|
|
71
|
+
name: t(typeDef("situation").name, lang),
|
|
72
|
+
url,
|
|
73
|
+
status: statusOf(src),
|
|
74
|
+
schema: typeDef("situation").schema,
|
|
75
|
+
note: `“${s.sentence}”`,
|
|
76
|
+
situationId: s.id,
|
|
77
|
+
...(src || noEvidence ? {} : { file: pageLocation(scan, url, sd).files[0] }),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (!r?.situations.length) {
|
|
81
|
+
pages.push({ key: "situation", name: t(typeDef("situation").name, lang), url: L("(one per situation)", "(her durum için bir tane)"), status: "unknown", schema: typeDef("situation").schema, note: L("Add 10–12 customer situations to research first.", "Önce araştırmaya 10–12 müşteri durumu ekleyin.") });
|
|
82
|
+
}
|
|
83
|
+
add("pricing", "pricing", primary === "tr" ? "/fiyatlandirma" : "/pricing", L("Prices in plain text; the same figures as llms.txt and schema.", "Fiyat düz metinde; llms.txt ve schema ile aynı rakam."));
|
|
84
|
+
const catSlug = isFilled(r?.brand.category) ? slugify(r.brand.category, 4) : "";
|
|
85
|
+
add("comparison", "comparison", catSlug ? (primary === "tr" ? `/rehber/${catSlug}-araclari` : `/guides/${catSlug}-tools`) : t(typeDef("comparison").url, lang), L("Neutral: competitors with source + date, us not first (P7).", "Tarafsız: rakipler kaynak + tarihle, biz ilk sırada değiliz (P7)."));
|
|
86
|
+
add("faq", "faq", primary === "tr" ? "/sss" : "/faq", L("Real customer questions; the first sentence of each answer is the answer.", "Müşterinin gerçek soruları; her cevabın ilk cümlesi cevaptır."));
|
|
87
|
+
add("about", "about", primary === "tr" ? "/hakkimizda" : "/about", L("Who builds it, the company, the not-X note, social accounts.", "Kim yapıyor, şirket, “X değildir” notu, sosyal hesaplar."));
|
|
88
|
+
add("guide", "guide", primary === "tr" ? "/rehber/..." : "/guides/...", L("Something others want to link to (backlinks come from here).", "Başkasının link vermek isteyeceği şey (backlink buradan gelir)."));
|
|
89
|
+
add("contact", "contact", primary === "tr" ? "/iletisim" : "/contact", L("E-mail as plain text; privacy and terms pages too.", "E-posta düz metin; gizlilik ve şartlar sayfaları da."));
|
|
90
|
+
const llmsExists = !!(scan?.files.llms.exists || audit?.site.llmsTxt.exists);
|
|
91
|
+
pages.push({ key: "llms", name: "llms.txt", url: "/llms.txt", status: audit?.site.llmsTxt.exists ? "live" : llmsExists ? "in code" : noEvidence ? "unknown" : "missing", schema: [], note: L("The one sentence, pricing, not-X, the important pages (P8).", "Tek cümle, fiyat, “X değildir”, önemli sayfalar (P8).") });
|
|
92
|
+
const scanSoft404 = !!scan && (/catch-all|fallback/i.test(scan.files.notFound.path ?? "") || scan.findings.some((f) => /soft-?404/i.test(f.id)));
|
|
93
|
+
const nfStatus = audit?.site.soft404 === true ? "soft 404" : audit?.site.soft404 === false ? "live" : scanSoft404 ? "soft 404" : scan?.files.notFound.exists ? "in code" : noEvidence ? "unknown" : "missing";
|
|
94
|
+
pages.push({ key: "notfound", name: t(typeDef("notfound").name, lang), url: t(typeDef("notfound").url, lang), status: nfStatus, schema: [], note: L("A real 404 status, with links to the important pages (P11).", "Gerçek 404 durum kodu, önemli sayfalara linkle (P11).") });
|
|
95
|
+
// ------------------------------------------------------------ keep out of the index
|
|
96
|
+
const ap = appPrefixes(scan);
|
|
97
|
+
const disallow = ap.prefixes.map((p) => `${p}/`);
|
|
98
|
+
const noindex = noindexRoutes(scan, ap.prefixes);
|
|
99
|
+
if (noEvidence)
|
|
100
|
+
notes.push(L("No scan or audit yet, so which pages exist is unknown: run katman_scan_code and katman_audit_site, then katman_plan again.", "Henüz tarama ya da denetim yok, hangi sayfaların var olduğu bilinmiyor: katman_scan_code ve katman_audit_site çalıştırıp katman_plan’ı yeniden çağırın."));
|
|
101
|
+
if (ap.defaulted)
|
|
102
|
+
notes.push(L("No scan yet: the Disallow list uses the book's defaults (/app/, /admin/, /api/, /dashboard/). Run katman_scan_code for the real app routes.", "Henüz tarama yok: Disallow listesi kitabın varsayılanlarını kullanıyor (/app/, /admin/, /api/, /dashboard/). Gerçek uygulama yolları için katman_scan_code çalıştırın."));
|
|
103
|
+
if (ap.conflicts.length)
|
|
104
|
+
notes.push(L(`App routes share a prefix with public pages under ${ap.conflicts.join(", ")}: Disallow the specific app paths by hand, never the whole prefix.`, `${ap.conflicts.join(", ")} altında uygulama yolları herkese açık sayfalarla aynı öneki paylaşıyor: tüm öneki değil, belirli uygulama yollarını elle kapatın.`));
|
|
105
|
+
// ------------------------------------------------------------ tasks
|
|
106
|
+
const tasks = [];
|
|
107
|
+
const shortVerify = (id, situation) => {
|
|
108
|
+
const vc = verifyCalls(id, c.root, siteUrl, situation, sd, langs).map(fmtCallShort);
|
|
109
|
+
if (vc.length)
|
|
110
|
+
return vc.join(" · ");
|
|
111
|
+
return PROMPTS.find((p) => p.id === id).verifiedBy.map((v) => (v.tool === "manual" ? t(v.check, lang) : v.tool)).join(" + ");
|
|
112
|
+
};
|
|
113
|
+
const promptTask = (id, extra = {}) => {
|
|
114
|
+
const def = PROMPTS.find((p) => p.id === id);
|
|
115
|
+
return {
|
|
116
|
+
id,
|
|
117
|
+
week: def.week,
|
|
118
|
+
prompt: id,
|
|
119
|
+
title: `${id} · ${t(def.title, lang)}`,
|
|
120
|
+
files: fileHints(id, scan, sd),
|
|
121
|
+
accept: t(def.accept, lang),
|
|
122
|
+
verifiedBy: id === "P14" ? `${shortVerify(id)} · ${L("release script / CI", "yayın betiği / CI")}: \`npx -y katman-mcp audit ${siteUrl ?? "<site URL>"} --strict\`` : shortVerify(id),
|
|
123
|
+
done: false,
|
|
124
|
+
source: "roadmap",
|
|
125
|
+
...extra,
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
const toolTask = (id, week, tool, title, accept, verifiedBy, files = []) => ({
|
|
129
|
+
id, week, ...(tool ? { tool } : {}), title, files, accept, verifiedBy, done: false, source: "roadmap",
|
|
130
|
+
});
|
|
131
|
+
const completeness = researchCompleteness(r);
|
|
132
|
+
// Week 1
|
|
133
|
+
tasks.push(toolTask("research", 1, "katman_research", L("Fill research: one sentence, not-X, 10–12 situations, competitors, proof (ch. 7)", "Araştırmayı doldur: tek cümle, “X değildir”, 10–12 durum, rakipler, kanıt (07)"), L("katman_research shows 100%: name, category, the 4-part sentence, not-X, ≥10 unbranded situations, ≥3 competitors with url + date, the question set.", "katman_research %100 gösteriyor: ad, kategori, 4 parçalı cümle, “X değildir”, ≥10 markasız durum, url + tarihli ≥3 rakip, soru seti."), fmtCallShort(calls.researchShow(c.root))));
|
|
134
|
+
tasks.push(toolTask("measure-1", 1, "katman_visibility_run", L("First measurement with the question set (ch. 15)", "Soru setiyle ilk ölçüm (15)"), L("A run is saved in .katman/runs with today's date; from now on the 12 + 2 questions stay fixed.", "Bugünün tarihiyle .katman/runs’a bir ölçüm kaydedildi; bundan sonra 12 + 2 soru sabit."), `${fmtCallShort(calls.visibilityDryRun(c.root))} ${L("or", "ya da")} ${fmtCallShort(calls.manualCheck(c.root))}`));
|
|
135
|
+
tasks.push(toolTask("traps", 1, "katman_audit_site", L("Test the 6 traps (ch. 5): text in JavaScript, bots at the door, soft 404s, open preview copy, links that aren't links, missing from Bing/Brave", "6 tuzağı test et (05): JavaScript’te metin, kapıdaki botlar, sahte 404, açık önizleme kopyası, link olmayan linkler, Bing/Brave’de yokluk"), L("Text visible with JavaScript off; AI bots get 200; an unknown URL returns 404; the preview copy redirects or is noindex; menu links are <a href>; site: searches in Bing and Brave show pages.", "JavaScript kapalıyken metin görünüyor; yapay zekâ botları 200 alıyor; olmayan adres 404; önizleme kopyası yönleniyor ya da noindex; menü linkleri <a href>; Bing ve Brave’de site: aramasında sayfalar çıkıyor."), `${fmtCallShort(calls.scan(c.root))} · ${fmtCallShort(calls.audit(c.root, siteUrl ?? "[FILL: site URL]"))} · ${L("site: searches by hand", "site: aramaları elle")}`));
|
|
136
|
+
tasks.push(promptTask("P0"));
|
|
137
|
+
tasks.push(promptTask("P1"));
|
|
138
|
+
// Week 2
|
|
139
|
+
const p2Findings = [...(scan?.findings ?? []), ...(audit?.findings ?? [])].some((f) => f.prompt === "P2");
|
|
140
|
+
// lead-approved CSR thresholds: critical only on strong evidence (< 50 JS-off words); 50–99 is a thin page
|
|
141
|
+
const lowWords = (audit?.pages ?? []).filter((p) => p.status === 200 && p.wordsJsOff < 50).length;
|
|
142
|
+
const needP2 = !scan || isCsr(scan) || p2Findings || lowWords > 0;
|
|
143
|
+
if (needP2)
|
|
144
|
+
tasks.push(promptTask("P2", isCsr(scan) || lowWords > 0 ? { week: 1 } : {}));
|
|
145
|
+
else
|
|
146
|
+
notes.push(L(`P2 isn't needed: the scan says the public pages are rendered as ${scan.rendering.verdict} HTML. Re-check with the JS-off word count after big changes.`, `P2 gerekmiyor: taramaya göre herkese açık sayfalar ${scan.rendering.verdict} HTML olarak üretiliyor. Büyük değişikliklerden sonra JavaScript kapalı kelime sayısıyla tekrar kontrol edin.`));
|
|
147
|
+
for (const id of ["P3", "P4", "P5", "P9", "P10", "P11", "P12"])
|
|
148
|
+
tasks.push(promptTask(id));
|
|
149
|
+
tasks.push(toolTask("cdn", 2, undefined, L("CDN / firewall bot settings (ch. 6): legacy AI-bot block off, Search + Agent allowed, managed robots.txt / AI Labyrinth / Bot Fight Mode / e-mail obfuscation off", "CDN / güvenlik duvarı bot ayarları (06): eski AI bot engeli kapalı, Arama + Ajan izinli, yönetilen robots.txt / AI Labyrinth / Bot Fight Mode / e-posta gizleme kapalı"), L("Every AI bot gets 200 through the CDN (command 5); robots.txt and the CDN say the same thing.", "Her yapay zekâ botu CDN’den 200 alıyor (komut 5); robots.txt ile CDN aynı şeyi söylüyor."), fmtCallShort(calls.audit(c.root, siteUrl ?? "[FILL: site URL]"))));
|
|
150
|
+
tasks.push(toolTask("notify", 2, "katman_indexnow", L("Tell the search engines (ch. 13): Search Console (DNS), Bing Webmaster Tools, IndexNow key, Brave submit-url; open the Search Console Gen-AI and Bing AI Performance reports", "Arama motorlarına bildir (13): Search Console (DNS), Bing Webmaster Tools, IndexNow anahtarı, Brave submit-url; Search Console Gen-AI ve Bing AI Performance raporlarını aç"), L("Sitemap submitted to Google and Bing; the IndexNow key file is live; Brave submission done.", "Sitemap Google ve Bing’e gönderildi; IndexNow anahtar dosyası yayında; Brave gönderimi yapıldı."), `${fmtCallShort(calls.generate(c.root, "indexnow-key"))} · ${fmtCallShort(calls.indexnow(c.root))}`));
|
|
151
|
+
// Week 3
|
|
152
|
+
const sitsMissing = (r?.situations ?? []).filter((s) => s.status !== "live" && pathKnown(known, surl(s)) !== "live");
|
|
153
|
+
for (const s of sitsMissing.slice(0, 5)) {
|
|
154
|
+
const def = PROMPTS.find((p) => p.id === "P6");
|
|
155
|
+
tasks.push({
|
|
156
|
+
id: `P6-${s.id}`,
|
|
157
|
+
week: 3,
|
|
158
|
+
prompt: "P6",
|
|
159
|
+
title: `P6 · ${t(def.title, lang)}: “${s.sentence}” → ${surl(s)}`,
|
|
160
|
+
files: pageLocation(scan, surl(s), sd).files,
|
|
161
|
+
accept: t(def.accept, lang),
|
|
162
|
+
verifiedBy: shortVerify("P6", s),
|
|
163
|
+
done: false,
|
|
164
|
+
situationId: s.id,
|
|
165
|
+
source: "situation",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (sitsMissing.length > 5)
|
|
169
|
+
notes.push(L(`${sitsMissing.length - 5} more situation page(s) after these 5: keep to 5–10 new pages a week.`, `Bu 5’ten sonra ${sitsMissing.length - 5} durum sayfası daha: haftada 5–10 yeni sayfayı geçmeyin.`));
|
|
170
|
+
if (!r?.situations.length)
|
|
171
|
+
tasks.push(promptTask("P6", { id: "P6", title: L("P6 · Write situation pages (add situations to research first)", "P6 · Durum sayfaları yaz (önce araştırmaya durum ekleyin)"), source: "situation" }));
|
|
172
|
+
tasks.push(promptTask("P7"));
|
|
173
|
+
tasks.push(promptTask("P8"));
|
|
174
|
+
tasks.push(toolTask("landing", 3, undefined, L("Welcome the visitor (ch. 10): on the pages assistants send people to, one immediate action usable within 60 s on a phone; don't re-ask what they told ChatGPT", "Ziyaretçiyi karşıla (10): asistanların insan gönderdiği sayfalarda telefonda 60 saniyede kullanılabilen tek bir eylem; ChatGPT’ye anlattığını tekrar sorma"), L("On a phone, without signing up, a visitor sees within 60 seconds that the product works for their situation.", "Telefonda, kayıt olmadan, ziyaretçi 60 saniye içinde ürünün kendi durumunda işe yaradığını görüyor."), L("by hand on a phone", "telefonda elle")));
|
|
175
|
+
tasks.push(toolTask("notify-new", 3, "katman_indexnow", L("Announce every new page (ch. 13): in the sitemap, ≥1 internal link, P14 passed; IndexNow for that URL only; Search Console request indexing; Bing URL Submission; Brave submit-url", "Her yeni sayfayı bildir (13): sitemap’te, ≥1 iç link, P14 geçti; IndexNow’a yalnızca o adres; Search Console dizine ekleme isteği; Bing URL Submission; Brave submit-url"), L("Every new page is in the sitemap, linked, verified and submitted once.", "Her yeni sayfa sitemap’te, linkli, doğrulanmış ve bir kez gönderilmiş."), fmtCallShort(calls.indexnow(c.root))));
|
|
176
|
+
// Week 4
|
|
177
|
+
tasks.push(promptTask("P13"));
|
|
178
|
+
tasks.push(toolTask("offsite", 4, "katman_report", L("Traces outside the site (ch. 14): one YouTube video for a top situation, directory texts (name, category, 60-char tagline, 250/600-char descriptions), an outreach list from the domains assistants cite", "Site dışı izler (14): önde gelen bir durum için bir YouTube videosu, dizin metinleri (ad, kategori, 60 karakterlik slogan, 250/600 karakterlik açıklamalar), asistanların alıntıladığı alan adlarından bir ulaşma listesi"), L("One video live and embedded on its page; directory text ready (the one sentence word for word); outreach list written down. No bought links, fake reviews or self-ranked lists.", "Bir video yayında ve sayfasına gömülü; dizin metni hazır (tek cümle aynen); ulaşma listesi yazılı. Satın alınmış link, sahte yorum ya da kendini birinci yazan liste yok."), `${fmtCallShort(calls.report(c.root))} (${L("top cited domains", "en çok alıntılanan alan adları")})`));
|
|
179
|
+
tasks.push(toolTask("remeasure", 4, "katman_visibility_run", L("Day 14: ask the question set again — same questions, models and temperature; 3 samples for the 3 most important situation questions; confirm them in the real apps", "14. gün: soru setini tekrar sor — aynı sorular, modeller ve sıcaklık; en önemli 3 durum sorusu için 3 örnek; gerçek uygulamalarda doğrula"), L("A second run is saved next to the first; you read frequency (how often named / recommended / cited), not rank.", "İkinci ölçüm birincinin yanında kayıtlı; sırayı değil sıklığı okuyorsunuz (ne sıklıkla anıldı / önerildi / kaynak gösterildi)."), `${fmtCallShort(calls.visibilityDryRun(c.root))} (${L("full set", "tüm set")}) · ${fmtCallShort(calls.visibilityDryRun(c.root, { question_ids: keySituationQuestionIds(r).length ? keySituationQuestionIds(r) : ["q1", "q2", "q3"], samples: 3 }))} · ${fmtCallShort(calls.manualCheck(c.root))}`));
|
|
180
|
+
tasks.push(toolTask("gsc", 4, undefined, L("Search Console: why aren't pages indexed? “Discovered – not indexed” → internal links; “Crawled – not indexed” → stop that template (30% rule)", "Search Console: sayfalar neden dizinde değil? “Keşfedildi, dizine eklenmedi” → iç link; “Tarandı, dizine eklenmedi” → o şablonu durdur (%30 kuralı)"), L("Every non-indexed page has a known reason and an action.", "Dizinde olmayan her sayfanın bilinen bir sebebi ve bir aksiyonu var."), L("Search Console → Pages, by hand", "Search Console → Sayfalar, elle")));
|
|
181
|
+
tasks.push(promptTask("P14"));
|
|
182
|
+
// ------------------------------------------------------------ findings
|
|
183
|
+
const findings = new Map();
|
|
184
|
+
for (const f of scan?.findings ?? [])
|
|
185
|
+
findings.set(f.id, { ...f, from: "scan" });
|
|
186
|
+
for (const f of audit?.findings ?? [])
|
|
187
|
+
findings.set(f.id, { ...f, from: "audit" });
|
|
188
|
+
const otherFindings = [];
|
|
189
|
+
const byPrompt = (p) => tasks.find((x) => x.prompt === p);
|
|
190
|
+
for (const f of [...findings.values()].sort((a, b) => SEV_RANK[a.severity] - SEV_RANK[b.severity])) {
|
|
191
|
+
const important = f.severity === "critical" || f.severity === "high";
|
|
192
|
+
let task = f.prompt ? byPrompt(f.prompt) : undefined;
|
|
193
|
+
if (!task && f.prompt === "P2" && important) {
|
|
194
|
+
task = promptTask("P2", { week: 1 });
|
|
195
|
+
tasks.push(task);
|
|
196
|
+
}
|
|
197
|
+
if (task) {
|
|
198
|
+
(task.findings ??= []).push({ id: f.id, title: f.title, severity: f.severity });
|
|
199
|
+
if (!task.severity || SEV_RANK[f.severity] < SEV_RANK[task.severity])
|
|
200
|
+
task.severity = f.severity;
|
|
201
|
+
if (f.severity === "critical")
|
|
202
|
+
task.week = 1;
|
|
203
|
+
for (const file of f.files ?? [])
|
|
204
|
+
if (!task.files.includes(file))
|
|
205
|
+
task.files.push(file);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (important) {
|
|
209
|
+
tasks.push({
|
|
210
|
+
id: `F-${f.id}`,
|
|
211
|
+
week: f.severity === "critical" ? 1 : f.prompt ? weekOf(f.prompt) : 2,
|
|
212
|
+
...(f.prompt ? { prompt: f.prompt } : {}),
|
|
213
|
+
tool: f.from === "scan" ? "katman_scan_code" : "katman_audit_site",
|
|
214
|
+
title: `[${f.severity.toUpperCase()}] ${f.title}`,
|
|
215
|
+
files: f.files ?? [],
|
|
216
|
+
accept: f.accept ?? L("The finding no longer appears in the next scan/audit.", "Bulgu bir sonraki tarama/denetimde çıkmıyor."),
|
|
217
|
+
verifiedBy: f.from === "scan" ? fmtCallShort(calls.scan(c.root)) : fmtCallShort(calls.audit(c.root, siteUrl ?? "[FILL: site URL]")),
|
|
218
|
+
done: false,
|
|
219
|
+
severity: f.severity,
|
|
220
|
+
findings: [{ id: f.id, title: f.title, severity: f.severity }],
|
|
221
|
+
source: "finding",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
otherFindings.push({ id: f.id, title: f.title, severity: f.severity, gate: f.gate, ...(f.prompt ? { prompt: f.prompt } : {}) });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// ------------------------------------------------------------ done state
|
|
229
|
+
const prev = c.previousChecks ?? new Map();
|
|
230
|
+
const prevTasks = new Map((c.previousTasks ?? []).map((x) => [x.id, x]));
|
|
231
|
+
const runsSorted = [...c.runs];
|
|
232
|
+
const firstRun = runsSorted[0] ? runDate(runsSorted[0]) : undefined;
|
|
233
|
+
const lastRun = runsSorted.length ? runDate(runsSorted[runsSorted.length - 1]) : undefined;
|
|
234
|
+
const auto = {
|
|
235
|
+
research: completeness.percent >= 100 ? L("research is 100% complete", "araştırma %100 dolu") : undefined,
|
|
236
|
+
"measure-1": c.runs.length ? L(`${c.runs.length} run(s) recorded`, `${c.runs.length} ölçüm kaydedildi`) : undefined,
|
|
237
|
+
P1: scan && audit ? L("scan + audit exist", "tarama + denetim var") : undefined,
|
|
238
|
+
cdn: audit && audit.site.bots.length && !audit.site.bots.some((b) => b.blocked) ? L("every tested AI bot got 200", "test edilen her yapay zekâ botu 200 aldı") : undefined,
|
|
239
|
+
remeasure: firstRun && lastRun && c.runs.length >= 2 && lastRun.getTime() - firstRun.getTime() >= 13 * 86_400_000 ? L("a second run ≥13 days after the first", "ilkinden ≥13 gün sonra ikinci ölçüm") : undefined,
|
|
240
|
+
};
|
|
241
|
+
for (const task of tasks) {
|
|
242
|
+
const was = prev.has(task.id) ? prev.get(task.id) : prevTasks.get(task.id)?.done ?? false;
|
|
243
|
+
task.done = was;
|
|
244
|
+
const a = auto[task.id];
|
|
245
|
+
if (a) {
|
|
246
|
+
task.done = true;
|
|
247
|
+
task.auto = a;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// carry over tasks that were done before but are no longer generated (e.g. fixed findings)
|
|
251
|
+
for (const [id, old] of prevTasks) {
|
|
252
|
+
const done = prev.has(id) ? prev.get(id) : old.done;
|
|
253
|
+
if (done && !tasks.some((x) => x.id === id))
|
|
254
|
+
tasks.push({ ...old, done: true, source: "carried" });
|
|
255
|
+
}
|
|
256
|
+
// order: by week, then severity, then original order
|
|
257
|
+
const order = new Map(tasks.map((x, i) => [x, i]));
|
|
258
|
+
tasks.sort((a, b) => a.week - b.week || (SEV_RANK[a.severity ?? "info"] - SEV_RANK[b.severity ?? "info"]) || order.get(a) - order.get(b));
|
|
259
|
+
return {
|
|
260
|
+
version: 1,
|
|
261
|
+
generatedAt: now.toISOString(),
|
|
262
|
+
lang,
|
|
263
|
+
...(siteUrl ? { siteUrl } : {}),
|
|
264
|
+
...(isFilled(r?.brand.name) ? { brand: r.brand.name } : {}),
|
|
265
|
+
sources: { researchPct: completeness.percent, ...(scan?.ts ? { scanTs: scan.ts } : {}), ...(audit?.ts ? { auditTs: audit.ts } : {}), runs: c.runs.length },
|
|
266
|
+
pages,
|
|
267
|
+
noindex,
|
|
268
|
+
disallow,
|
|
269
|
+
conflicts: ap.conflicts,
|
|
270
|
+
notes,
|
|
271
|
+
tasks,
|
|
272
|
+
otherFindings,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
export function renderPlanMd(plan) {
|
|
276
|
+
const lang = plan.lang;
|
|
277
|
+
const L = (en, tr) => (lang === "tr" ? tr : en);
|
|
278
|
+
const out = [];
|
|
279
|
+
out.push(`# ${L("Katman plan", "Katman planı")}${plan.brand ? ` — ${plan.brand}` : ""}`);
|
|
280
|
+
out.push(L(`Generated ${isoDay(plan.generatedAt)} from research (${plan.sources.researchPct}%), scan (${plan.sources.scanTs ? isoDay(plan.sources.scanTs) : "none"}), audit (${plan.sources.auditTs ? isoDay(plan.sources.auditTs) : "none"}) and ${plan.sources.runs} visibility run(s).\nTick a task (\`- [x]\`) only when its acceptance check passes. \`katman_plan\` keeps your ticks when it regenerates this file; tasks marked _auto_ were verified by Katman.`, `${isoDay(plan.generatedAt)} tarihinde araştırma (%${plan.sources.researchPct}), tarama (${plan.sources.scanTs ? isoDay(plan.sources.scanTs) : "yok"}), denetim (${plan.sources.auditTs ? isoDay(plan.sources.auditTs) : "yok"}) ve ${plan.sources.runs} görünürlük ölçümünden üretildi.\nBir görevi (\`- [x]\`) yalnızca kabul kontrolü geçince işaretleyin. \`katman_plan\` dosyayı yeniden üretirken işaretlerinizi korur; _auto_ işaretliler Katman tarafından doğrulandı.`));
|
|
281
|
+
out.push(`## ${L("Page plan", "Sayfa planı")}`);
|
|
282
|
+
out.push(mdTable([L("Page", "Sayfa"), "URL", L("Status", "Durum"), "Schema", L("Notes", "Notlar")], plan.pages.map((p) => [p.name, p.url, p.status, p.schema.join(", ") || "–", p.note + (p.file ? ` · ${L("file", "dosya")}: ${p.file}` : "")])));
|
|
283
|
+
out.push(`## ${L("Keep out of the index", "Dizine girmemesi gerekenler")}`);
|
|
284
|
+
out.push([
|
|
285
|
+
`- ${L("`noindex,follow` and not in the sitemap", "`noindex,follow` ve sitemap’te yok")}: ${plan.noindex.length ? plan.noindex.join(", ") : L("login, sign-up, password reset, checkout, thank-you and site-search pages (none found by the scan)", "giriş, kayıt, şifre sıfırlama, ödeme, teşekkür ve site içi arama sayfaları (tarama bulmadı)")}`,
|
|
286
|
+
`- ${L("`Disallow` in robots.txt (app paths, trailing slash)", "robots.txt’de `Disallow` (uygulama yolları, sonda /)")}: ${plan.disallow.length ? plan.disallow.join(", ") : L("none", "yok")}`,
|
|
287
|
+
`- ${L("Never Disallow a page that carries noindex: Google then can't see the noindex.", "noindex taşıyan bir sayfayı asla Disallow etmeyin: Google o zaman noindex’i göremez.")}`,
|
|
288
|
+
].join("\n"));
|
|
289
|
+
if (plan.notes.length)
|
|
290
|
+
out.push(plan.notes.map((n) => `> ${n}`).join("\n"));
|
|
291
|
+
const done = plan.tasks.filter((x) => x.done).length;
|
|
292
|
+
out.push(`## ${L("Tasks — first 30 days", "Görevler — ilk 30 gün")} (${done}/${plan.tasks.length} ${L("done", "tamam")})`);
|
|
293
|
+
for (const w of [1, 2, 3, 4]) {
|
|
294
|
+
const wt = plan.tasks.filter((x) => x.week === w);
|
|
295
|
+
if (!wt.length)
|
|
296
|
+
continue;
|
|
297
|
+
const wp = FIRST_30_DAYS.find((x) => x.week === w);
|
|
298
|
+
out.push(`### ${L("Week", "Hafta")} ${w} — ${t(wp.title, lang)}`);
|
|
299
|
+
out.push(wt
|
|
300
|
+
.map((x) => {
|
|
301
|
+
const lines = [`- [${x.done ? "x" : " "}] **${x.title}**${x.severity && (x.severity === "critical" || x.severity === "high") ? ` · ${x.severity.toUpperCase()}` : ""}${x.auto ? ` _(auto: ${x.auto})_` : ""} <!-- id:${x.id} -->`];
|
|
302
|
+
lines.push(` - ${L("Accept", "Kabul")}: ${x.accept}`);
|
|
303
|
+
lines.push(` - ${L("Verify", "Doğrula")}: ${x.verifiedBy}`);
|
|
304
|
+
if (x.prompt)
|
|
305
|
+
lines.push(` - ${L("Prompt", "Prompt")}: \`katman_prompt {"id":"${x.prompt}"${x.situationId ? `,"situation_id":"${x.situationId}"` : ""}}\``);
|
|
306
|
+
if (x.files.length)
|
|
307
|
+
lines.push(` - ${L("Files", "Dosyalar")}: ${x.files.join(", ")}`);
|
|
308
|
+
for (const f of x.findings ?? [])
|
|
309
|
+
lines.push(` - ${L("Finding", "Bulgu")} [${f.severity.toUpperCase()}]: ${f.title}`);
|
|
310
|
+
return lines.join("\n");
|
|
311
|
+
})
|
|
312
|
+
.join("\n"));
|
|
313
|
+
}
|
|
314
|
+
if (plan.otherFindings.length) {
|
|
315
|
+
out.push(`## ${L("Other findings (medium / low)", "Diğer bulgular (orta / düşük)")}`);
|
|
316
|
+
out.push(plan.otherFindings.map((f) => `- [${f.severity.toUpperCase()}] ${f.title} (${f.gate}${f.prompt ? `, ${f.prompt}` : ""})`).join("\n"));
|
|
317
|
+
}
|
|
318
|
+
out.push(`## ${L("Measuring", "Ölçüm")}\n- ${t(MEASURE_FREQUENCY, lang)}\n- ${t(API_PROXY_NOTE, lang)}`);
|
|
319
|
+
out.push(`---\nGenerated with Katman — katman.pro`);
|
|
320
|
+
return out.join("\n\n") + "\n";
|
|
321
|
+
}
|
|
322
|
+
export function openTasks(plan) {
|
|
323
|
+
return plan.tasks.filter((x) => !x.done);
|
|
324
|
+
}
|
|
325
|
+
/** Task → the tool call that starts it. */
|
|
326
|
+
export function taskCall(root, task, siteUrl) {
|
|
327
|
+
if (task.prompt)
|
|
328
|
+
return calls.prompt(root, task.prompt, task.situationId);
|
|
329
|
+
switch (task.tool) {
|
|
330
|
+
case "katman_research": return calls.researchShow(root);
|
|
331
|
+
case "katman_visibility_run": return calls.visibilityDryRun(root);
|
|
332
|
+
case "katman_audit_site": return siteUrl ? calls.audit(root, siteUrl) : calls.scan(root);
|
|
333
|
+
case "katman_indexnow": return calls.indexnow(root);
|
|
334
|
+
case "katman_report": return calls.report(root);
|
|
335
|
+
case "katman_scan_code": return calls.scan(root);
|
|
336
|
+
default: return undefined;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
export async function loadPlanState(root) {
|
|
340
|
+
const md = await readText(root, "plan.md");
|
|
341
|
+
const plan = await readJson(root, "plan.json");
|
|
342
|
+
const checks = parsePlanChecks(md);
|
|
343
|
+
if (plan && Array.isArray(plan.tasks)) {
|
|
344
|
+
for (const task of plan.tasks)
|
|
345
|
+
if (checks.has(task.id))
|
|
346
|
+
task.done = checks.get(task.id);
|
|
347
|
+
return { plan, checks, md };
|
|
348
|
+
}
|
|
349
|
+
return { checks, md };
|
|
350
|
+
}
|
|
351
|
+
export async function runPlan(root, args = {}) {
|
|
352
|
+
const [research, config, scan, loaded, runs, prevState] = await Promise.all([
|
|
353
|
+
loadResearchSafe(root), loadConfigSafe(root), loadScan(root), loadLatestAudit(root), loadRuns(root), loadPlanState(root),
|
|
354
|
+
]);
|
|
355
|
+
const lang = args.lang ?? config.lang ?? "en";
|
|
356
|
+
const staticDir = staticDirFor(root, scan).dir;
|
|
357
|
+
const plan = buildPlanData({
|
|
358
|
+
root, lang, research, config, scan, audit: loaded?.audit, runs,
|
|
359
|
+
previousChecks: prevState.checks,
|
|
360
|
+
previousTasks: prevState.plan?.tasks,
|
|
361
|
+
staticDir,
|
|
362
|
+
});
|
|
363
|
+
await writeJson(root, "plan.json", plan);
|
|
364
|
+
await writeText(root, "plan.md", renderPlanMd(plan));
|
|
365
|
+
const open = openTasks(plan);
|
|
366
|
+
const done = plan.tasks.length - open.length;
|
|
367
|
+
await logEvent(root, `katman_plan: ${plan.tasks.length} tasks (${done} done), ${plan.pages.filter((p) => p.status === "missing").length} pages missing`);
|
|
368
|
+
const L = (en, tr) => (lang === "tr" ? tr : en);
|
|
369
|
+
const missingPages = plan.pages.filter((p) => p.status === "missing" || p.status === "soft 404");
|
|
370
|
+
const warn = [];
|
|
371
|
+
if (!research)
|
|
372
|
+
warn.push(L(`No research yet: the plan is generic. Start with ${fmtCallShort(calls.researchTemplate(root))}.`, `Henüz araştırma yok: plan genel. ${fmtCallShort(calls.researchTemplate(root))} ile başlayın.`));
|
|
373
|
+
if (!scan)
|
|
374
|
+
warn.push(L("No scan yet: file hints are generic (katman_scan_code).", "Henüz tarama yok: dosya ipuçları genel (katman_scan_code)."));
|
|
375
|
+
if (!loaded)
|
|
376
|
+
warn.push(L("No audit yet: live status of pages is unknown (katman_audit_site).", "Henüz denetim yok: sayfaların canlı durumu bilinmiyor (katman_audit_site)."));
|
|
377
|
+
const siteUrl = siteUrlOf(research, config);
|
|
378
|
+
const firstOpen = open.slice(0, 5).map((x, i) => {
|
|
379
|
+
const call = taskCall(root, x, siteUrl);
|
|
380
|
+
return `${i + 1}. **${x.title}** (${L("week", "hafta")} ${x.week}${x.severity ? `, ${x.severity}` : ""})\n ${L("Accept", "Kabul")}: ${x.accept}\n ${call ? `${L("Start", "Başla")}: ${fmtCallShort(call)}` : L("Manual task — tick it in plan.md when done.", "Elle yapılan görev — bitince plan.md’de işaretleyin.")}`;
|
|
381
|
+
});
|
|
382
|
+
const text = [
|
|
383
|
+
`${L("Wrote", "Yazıldı")} \`.katman/plan.md\` + \`.katman/plan.json\`: ${plan.tasks.length} ${L("tasks", "görev")} (${done} ${L("done", "tamam")}), ${plan.pages.length} ${L("pages in the page plan", "sayfa planında")} (${missingPages.length} ${L("missing", "eksik")}).`,
|
|
384
|
+
warn.join("\n"),
|
|
385
|
+
missingPages.length ? `**${L("Missing pages", "Eksik sayfalar")}:** ${missingPages.map((p) => `${p.name} ${p.url}`).join(" · ")}` : "",
|
|
386
|
+
`**${L("Keep out of the index", "Dizin dışı")}:** noindex → ${plan.noindex.join(", ") || L("(none found)", "(bulunamadı)")}; Disallow → ${plan.disallow.join(", ") || L("none", "yok")}`,
|
|
387
|
+
plan.notes.map((n) => `> ${n}`).join("\n"),
|
|
388
|
+
`**${L("First open tasks", "İlk açık görevler")}:**\n${firstOpen.join("\n") || L("All tasks are done: verify, notify and re-measure.", "Bütün görevler tamam: doğrula, bildir, yeniden ölç.")}`,
|
|
389
|
+
L("Show the plan to the user and get approval before changing code (P0).", "Kod değiştirmeden önce planı kullanıcıya gösterin ve onay alın (P0)."),
|
|
390
|
+
]
|
|
391
|
+
.filter(Boolean)
|
|
392
|
+
.join("\n\n");
|
|
393
|
+
return {
|
|
394
|
+
text,
|
|
395
|
+
plan,
|
|
396
|
+
structured: {
|
|
397
|
+
tasks: plan.tasks.length,
|
|
398
|
+
done,
|
|
399
|
+
open: open.slice(0, 5).map((x) => ({ id: x.id, week: x.week, title: x.title, prompt: x.prompt, accept: x.accept })),
|
|
400
|
+
missingPages: missingPages.map((p) => ({ name: p.name, url: p.url })),
|
|
401
|
+
noindex: plan.noindex,
|
|
402
|
+
disallow: plan.disallow,
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { domainOf } from "../../../util.js";
|
|
2
|
+
import { calls, fmtCall } from "../lib/calls.js";
|
|
3
|
+
import { frameworkNotes, fwLabel } from "../lib/framework.js";
|
|
4
|
+
import { t } from "../lib/l10n.js";
|
|
5
|
+
import { appPathsText, knownPublicPaths, pathKnown } from "../lib/routes.js";
|
|
6
|
+
import { loadConfigSafe, loadLatestAudit, loadResearchSafe, loadScan, staticDirFor } from "../lib/state.js";
|
|
7
|
+
import { brandSentence, codeBlock, fillTemplate, hasNotX, isFilled, slugify } from "../lib/text.js";
|
|
8
|
+
import { PLACEHOLDERS, PROMPTS, getPrompt, promptPlaceholders } from "../data/prompts.js";
|
|
9
|
+
import { detectLang } from "./questions.js";
|
|
10
|
+
export function siteUrlOf(r, cfg) {
|
|
11
|
+
if (r && isFilled(r.site.url) && /^https?:\/\//i.test(r.site.url))
|
|
12
|
+
return r.site.url.replace(/\/+$/, "");
|
|
13
|
+
if (cfg?.siteUrl)
|
|
14
|
+
return cfg.siteUrl.replace(/\/+$/, "");
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Planned or live URL of a situation page; a sentence in a secondary site language gets its /<lang>/ prefix. */
|
|
18
|
+
export function situationUrl(s, languages = []) {
|
|
19
|
+
if (s.url && s.url.startsWith("/"))
|
|
20
|
+
return s.url;
|
|
21
|
+
const slug = slugify(s.sentence);
|
|
22
|
+
if (languages.length > 1) {
|
|
23
|
+
const l = detectLang(s.sentence, languages);
|
|
24
|
+
if (l !== languages[0])
|
|
25
|
+
return `/${l.split("-")[0]}/${slug}`;
|
|
26
|
+
}
|
|
27
|
+
return `/${slug}`;
|
|
28
|
+
}
|
|
29
|
+
/** The situation to use: by id, else the first one without a live page, else the first one. */
|
|
30
|
+
export function pickSituation(r, id, scan, audit) {
|
|
31
|
+
if (!r || !r.situations.length)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (id)
|
|
34
|
+
return r.situations.find((s) => s.id === id || s.sentence.toLowerCase() === id.toLowerCase());
|
|
35
|
+
const known = knownPublicPaths(scan, audit);
|
|
36
|
+
return r.situations.find((s) => s.status !== "live" && pathKnown(known, situationUrl(s, r.site.languages)) !== "live") ?? r.situations[0];
|
|
37
|
+
}
|
|
38
|
+
export function fillValues(r, cfg, scan, situation) {
|
|
39
|
+
const siteUrl = siteUrlOf(r, cfg);
|
|
40
|
+
const langs = r?.site.languages.length ? r.site.languages : scan?.i18n.locales.length ? scan.i18n.locales : scan?.i18n.htmlLang ? [scan.i18n.htmlLang] : [];
|
|
41
|
+
const competitors = (r?.competitors ?? [])
|
|
42
|
+
.slice(0, 8)
|
|
43
|
+
.map((c) => {
|
|
44
|
+
const bits = [isFilled(c.url) ? c.url : "", isFilled(c.checkedAt) ? `checked ${c.checkedAt}` : ""].filter(Boolean);
|
|
45
|
+
return bits.length ? `${c.name} (${bits.join(", ")})` : c.name;
|
|
46
|
+
})
|
|
47
|
+
.join("; ");
|
|
48
|
+
return {
|
|
49
|
+
brand: r?.brand.name,
|
|
50
|
+
domain: r && isFilled(r.site.domain) ? r.site.domain : siteUrl ? domainOf(siteUrl) : undefined,
|
|
51
|
+
site_url: siteUrl,
|
|
52
|
+
category: r?.brand.category,
|
|
53
|
+
sentence: brandSentence(r),
|
|
54
|
+
not_x: hasNotX(r?.brand.notX) ? r.brand.notX : undefined,
|
|
55
|
+
situation: situation?.sentence,
|
|
56
|
+
situation_url: situation ? situationUrl(situation, r?.site.languages) : undefined,
|
|
57
|
+
competitors: competitors || undefined,
|
|
58
|
+
languages: langs.length ? langs.join(", ") : undefined,
|
|
59
|
+
app_paths: appPathsText(scan),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function fillPrompt(def, values, lang) {
|
|
63
|
+
return fillTemplate(t(def.body, lang), values, PLACEHOLDERS);
|
|
64
|
+
}
|
|
65
|
+
/** The exact Katman call(s) that check the acceptance criterion. */
|
|
66
|
+
export function verifyCalls(id, root, siteUrl, situation, staticDir = "", languages = []) {
|
|
67
|
+
const site = siteUrl ?? "[FILL: site URL]";
|
|
68
|
+
switch (id) {
|
|
69
|
+
case "P0": return [calls.researchShow(root)];
|
|
70
|
+
case "P1": return [calls.scan(root), calls.audit(root, site)];
|
|
71
|
+
case "P6": {
|
|
72
|
+
const url = situation ? situationUrl(situation, languages) : "/[FILL]";
|
|
73
|
+
return [
|
|
74
|
+
calls.lint(root, { file: "[FILL: the new page file]", page_type: "situation", ...(situation ? { situation_id: situation.id } : {}) }),
|
|
75
|
+
calls.auditUrls(root, [`${site}${url}`]),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
case "P7": return [calls.lint(root, { file: "[FILL: the guide page file]", page_type: "comparison" })];
|
|
79
|
+
case "P8": return [calls.generate(root, "llms.txt"), calls.lint(root, { file: `${staticDir ? staticDir + "/" : ""}llms.txt` })];
|
|
80
|
+
case "P12": return [];
|
|
81
|
+
case "P13": return [calls.scan(root)];
|
|
82
|
+
case "P14": return [calls.auditUrls(root, [`${site}/`])];
|
|
83
|
+
default: return [calls.audit(root, site)];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function renderPrompt(def, o) {
|
|
87
|
+
const values = fillValues(o.research, o.config, o.scan, o.situation);
|
|
88
|
+
const filled = fillPrompt(def, values, o.lang);
|
|
89
|
+
const used = promptPlaceholders(def);
|
|
90
|
+
const missing = used.filter((p) => !isFilled(values[p]));
|
|
91
|
+
const L = (en, tr) => (o.lang === "tr" ? tr : en);
|
|
92
|
+
const parts = [];
|
|
93
|
+
parts.push(`# ${def.id} · ${t(def.title, o.lang)}${def.tag ? ` — ${t(def.tag, o.lang)}` : ""}`);
|
|
94
|
+
parts.push(`**${L("When", "Ne zaman")}:** ${t(def.when, o.lang)}${def.changesCode ? "" : L(" This prompt changes no code.", " Bu prompt hiçbir kodu değiştirmez.")}`);
|
|
95
|
+
if (def.id === "P6" && o.situation)
|
|
96
|
+
parts.push(`**${L("Situation", "Durum")}:** \`${o.situation.id}\` "${o.situation.sentence}"`);
|
|
97
|
+
parts.push(`## ${L("Prompt (do this now)", "Prompt (şimdi bunu yap)")}\n${codeBlock(filled)}`);
|
|
98
|
+
if (missing.length) {
|
|
99
|
+
parts.push(L(`**[FILL] left in the prompt:** ${missing.map((m) => `{${m}}`).join(", ")}. Fill them in research (katman_research mode "update") or ask the user — don't guess.`, `**Prompt’ta [FILL] kaldı:** ${missing.map((m) => `{${m}}`).join(", ")}. Araştırmada doldurun (katman_research mode "update") ya da kullanıcıya sorun — tahmin etmeyin.`));
|
|
100
|
+
}
|
|
101
|
+
parts.push(`**${L("Accept", "Kabul")}:** ${t(def.accept, o.lang)}`);
|
|
102
|
+
parts.push(`**${L("Why", "Neden")}:** ${t(def.why, o.lang)}`);
|
|
103
|
+
if (def.katmanNote)
|
|
104
|
+
parts.push(`**${L("Evidence note", "Kanıt notu")}:** ${t(def.katmanNote, o.lang)}`);
|
|
105
|
+
const verify = def.verifiedBy.map((v) => `- ${v.tool === "manual" ? L("by hand", "elle") : `\`${v.tool}\``}: ${t(v.check, o.lang)}`);
|
|
106
|
+
const vc = verifyCalls(def.id, o.root, siteUrlOf(o.research, o.config), o.situation, o.staticDir, o.research?.site.languages);
|
|
107
|
+
parts.push(`## ${L("How Katman verifies this", "Katman bunu nasıl doğrular")}\n${verify.join("\n")}${vc.length ? `\n\n${L("Run", "Çalıştır")}: ${vc.map(fmtCall).join(" · ")}` : ""}`);
|
|
108
|
+
const notes = frameworkNotes(def.id, o.scan, o.lang, o.staticDir ?? "");
|
|
109
|
+
if (notes.length)
|
|
110
|
+
parts.push(`## ${L("Notes for this codebase", "Bu kod tabanı için notlar")} (${fwLabel(o.scan)})\n${notes.map((n) => `- ${n.replace(/\n/g, "\n ")}`).join("\n")}`);
|
|
111
|
+
else if (!o.scan && ["P2", "P3", "P4", "P6", "P11", "P13"].includes(def.id)) {
|
|
112
|
+
parts.push(L(`No scan yet: run ${fmtCall(calls.scan(o.root))} for framework-specific notes.`, `Henüz tarama yok: çatıya özel notlar için ${fmtCall(calls.scan(o.root))} çalıştırın.`));
|
|
113
|
+
}
|
|
114
|
+
parts.push(L(`**When you're done:** list the files you changed, run the check above, and tick this task in .katman/plan.md only when it passes.`, `**Bitirince:** değiştirdiğin dosyaları listele, yukarıdaki kontrolü çalıştır ve görevi .katman/plan.md’de yalnızca kontrol geçince işaretle.`));
|
|
115
|
+
return { markdown: parts.join("\n\n"), filled, missing };
|
|
116
|
+
}
|
|
117
|
+
export async function runPrompt(root, args) {
|
|
118
|
+
const def = getPrompt(args.id);
|
|
119
|
+
if (!def) {
|
|
120
|
+
return { text: `Unknown prompt "${args.id}". Valid ids: ${PROMPTS.map((p) => p.id).join(", ")}.`, isError: true };
|
|
121
|
+
}
|
|
122
|
+
const [research, config, scan, loaded] = await Promise.all([loadResearchSafe(root), loadConfigSafe(root), loadScan(root), loadLatestAudit(root)]);
|
|
123
|
+
const lang = args.lang ?? config.lang ?? "en";
|
|
124
|
+
let situation;
|
|
125
|
+
if (def.id === "P6") {
|
|
126
|
+
situation = pickSituation(research, args.situationId, scan, loaded?.audit);
|
|
127
|
+
if (args.situationId && !situation) {
|
|
128
|
+
const ids = (research?.situations ?? []).map((s) => `${s.id} ("${s.sentence}")`).join(", ");
|
|
129
|
+
return { text: `Situation "${args.situationId}" not found in research. Known situations: ${ids || "none — add them with katman_research mode \"update\""}.`, isError: true };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const staticDir = staticDirFor(root, scan).dir;
|
|
133
|
+
const out = renderPrompt(def, { root, lang, research, config, scan, audit: loaded?.audit, situation, staticDir });
|
|
134
|
+
const warn = [];
|
|
135
|
+
if (!research)
|
|
136
|
+
warn.push(`No research yet, so placeholders are [FILL]. Start with ${fmtCall(calls.researchTemplate(root))}.`);
|
|
137
|
+
return {
|
|
138
|
+
text: [warn.join("\n"), out.markdown].filter(Boolean).join("\n\n"),
|
|
139
|
+
structured: {
|
|
140
|
+
id: def.id,
|
|
141
|
+
lang,
|
|
142
|
+
prompt: out.filled,
|
|
143
|
+
accept: t(def.accept, lang),
|
|
144
|
+
why: t(def.why, lang),
|
|
145
|
+
verifiedBy: def.verifiedBy.map((v) => ({ tool: v.tool, check: t(v.check, lang) })),
|
|
146
|
+
missing: out.missing,
|
|
147
|
+
...(situation ? { situation } : {}),
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|