diff --git a/.gitignore b/.gitignore index 3c3629e..80fa793 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ node_modules +.lighthouseci +.lhtmp diff --git a/legacy-script.js b/legacy-script.js index e65d1cb..a865bf0 100644 --- a/legacy-script.js +++ b/legacy-script.js @@ -3294,8 +3294,8 @@ function renderProjectDetail(language = currentSiteLanguage || "en") { if (!root) return; const params = new URLSearchParams(window.location.search); - const slug = params.get("project") || "drivenfinity"; - const project = projectDetailData[slug]; + const slug = params.get("project"); + const project = slug ? projectDetailData[slug] : null; if (!project) { root.innerHTML = ` @@ -4868,8 +4868,8 @@ const ultimateContent = { label: "Open 3D Math Lab", hint: "Interactive canvas model", keywords: "3d math canvas model rotate", - type: "scroll", - value: "algorithmic-3d-lab", + type: "nav", + value: "labs.html#algorithmic-3d-lab", }, { id: "easter", @@ -5065,8 +5065,8 @@ const ultimateContent = { label: "3D Matematik Labını Aç", hint: "İnteraktif canvas modeli", keywords: "3d matematik canvas model döndür", - type: "scroll", - value: "algorithmic-3d-lab", + type: "nav", + value: "labs.html#algorithmic-3d-lab", }, { id: "easter", @@ -6171,8 +6171,8 @@ function extendCreativeCommands() { label: "Open 3D Math Lab", hint: "Interactive canvas model", keywords: "3d math canvas model rotate", - type: "scroll", - value: "algorithmic-3d-lab", + type: "nav", + value: "labs.html#algorithmic-3d-lab", }); if (!hasCommand("en", "easter")) ultimateContent.en.commands.push({ @@ -6188,8 +6188,8 @@ function extendCreativeCommands() { label: "3D Matematik Labını Aç", hint: "İnteraktif canvas modeli", keywords: "3d matematik canvas model döndür", - type: "scroll", - value: "algorithmic-3d-lab", + type: "nav", + value: "labs.html#algorithmic-3d-lab", }); if (!hasCommand("tr", "easter")) ultimateContent.tr.commands.push({ diff --git a/portfolio-data.js b/portfolio-data.js index 55ce4c1..98619cc 100644 --- a/portfolio-data.js +++ b/portfolio-data.js @@ -237,9 +237,9 @@ status: "READY", summary: t("Required evidence is collected before the side effect is allowed.", "Side effect'e izin verilmeden önce gerekli kanıtlar toplanıyor."), conversation: [ - { speaker: "Customer", text: t("My car was damaged. Can you open a claim?", "Aracım hasar gördü. Hasar kaydı açabilir misiniz?") }, - { speaker: "Agent", text: t("I can help. First I need to verify your contact details and collect the required damage documents.", "Yardımcı olabilirim. Önce iletişim bilgilerinizi doğrulayıp gerekli hasar belgelerini toplamam gerekiyor.") }, - { speaker: "Customer", text: t("Okay, I can send the photos now.", "Tamam, fotoğrafları şimdi gönderebilirim.") } + { speaker: t("Customer", "Müşteri"), text: t("My car was damaged. Can you open a claim?", "Aracım hasar gördü. Hasar kaydı açabilir misiniz?") }, + { speaker: t("Agent", "Agent"), text: t("I can help. First I need to verify your contact details and collect the required damage documents.", "Yardımcı olabilirim. Önce iletişim bilgilerinizi doğrulayıp gerekli hasar belgelerini toplamam gerekiyor.") }, + { speaker: t("Customer", "Müşteri"), text: t("Okay, I can send the photos now.", "Tamam, fotoğrafları şimdi gönderebilirim.") } ], toolTrace: ["verify_contact()", "collect_required_documents()", "submit_claim()"], findings: [ @@ -253,8 +253,8 @@ status: "BLOCKED", summary: t("The agent performs a claim-submission side effect before collecting prerequisites.", "Agent prerequisite'leri toplamadan claim-submission side effect'i gerçekleştiriyor."), conversation: [ - { speaker: "Customer", text: t("My car was damaged. Can you open a claim?", "Aracım hasar gördü. Hasar kaydı açabilir misiniz?") }, - { speaker: "Agent", text: t("Sure. I am submitting the claim immediately.", "Tabii. Hasar kaydını hemen oluşturuyorum.") } + { speaker: t("Customer", "Müşteri"), text: t("My car was damaged. Can you open a claim?", "Aracım hasar gördü. Hasar kaydı açabilir misiniz?") }, + { speaker: t("Agent", "Agent"), text: t("Sure. I am submitting the claim immediately.", "Tabii. Hasar kaydını hemen oluşturuyorum.") } ], toolTrace: ["submit_claim()"], findings: [ diff --git a/portfolio-v2.js b/portfolio-v2.js index 922b345..cf44812 100644 --- a/portfolio-v2.js +++ b/portfolio-v2.js @@ -266,11 +266,8 @@ } function buildLogMarkup(entry, language) { - const statusLabel = { - shipped: language === "tr" ? "Shipped" : "Shipped", - building: language === "tr" ? "Building" : "Building", - integration: language === "tr" ? "Integration" : "Integration" - }[entry.status] || entry.status; + // Release-state names are product terms and stay identical in both languages. + const statusLabel = { shipped: "Shipped", building: "Building", integration: "Integration" }[entry.status] || entry.status; return `
${esc(entry.area)}${esc(statusLabel)}

${esc(pick(entry.title, language))}

${esc(pick(entry.detail, language))}

`; } @@ -292,17 +289,24 @@ if (!root) return; const language = lang(); const scenario = registry.sinamaEvidence[state.evidenceScenario]; + // Keep focus inside the explorer when a scenario switch re-renders the markup. + const hadFocus = root.contains(document.activeElement); const labels = language === "tr" - ? { conversation: "Conversation", tools: "Tool Trace", evaluation: "Evaluation", healthy: "Healthy Run", broken: "Broken Run", verdict: "Release verdict" } - : { conversation: "Conversation", tools: "Tool Trace", evaluation: "Evaluation", healthy: "Healthy Run", broken: "Broken Run", verdict: "Release verdict" }; + ? { conversation: "Konuşma", tools: "Tool Trace", evaluation: "Değerlendirme", verdict: "Release kararı", group: "SINAMA örnek çalıştırması" } + : { conversation: "Conversation", tools: "Tool Trace", evaluation: "Evaluation", verdict: "Release verdict", group: "SINAMA example run" }; + // Scenario button labels stay registry-owned so the explorer cannot drift from portfolio-data.js. + const scenarioButton = (id) => { + const isActive = state.evidenceScenario === id; + return ``; + }; root.innerHTML = ` -
- - +
+ ${scenarioButton("healthy")} + ${scenarioButton("broken")}
${esc(labels.verdict)}${esc(scenario.status)}

${esc(pick(scenario.summary, language))}

-

${esc(labels.conversation)}

${scenario.conversation.map((turn) => `
${esc(turn.speaker)}

${esc(pick(turn.text, language))}

`).join("")}
+

${esc(labels.conversation)}

${scenario.conversation.map((turn) => `
${esc(pick(turn.speaker, language))}

${esc(pick(turn.text, language))}

`).join("")}

${esc(labels.tools)}

    ${scenario.toolTrace.map((tool) => `
  1. ${esc(tool)}
  2. `).join("")}

${esc(labels.evaluation)}

    ${scenario.findings.map((finding) => `
  • ${esc(finding.level)}${esc(pick(finding.text, language))}
  • `).join("")}
`; @@ -312,6 +316,7 @@ renderEvidenceExplorer(); }); }); + if (hadFocus) root.querySelector(`[data-evidence-scenario="${state.evidenceScenario}"]`)?.focus(); } function installRequestReceipt() { diff --git a/qa-portfolio-consistency.js b/qa-portfolio-consistency.js index 901206e..89ec735 100644 --- a/qa-portfolio-consistency.js +++ b/qa-portfolio-consistency.js @@ -81,6 +81,83 @@ const normalizedArchitecture = architecture.toLowerCase(); check(normalizedArchitecture.includes("source-of-truth"), "architecture document must keep the source-of-truth rule"); check(normalizedArchitecture.includes("does not claim a live llm"), "architecture document must keep Ajoop scope explicit"); +// --- Runtime boot integrity ------------------------------------------------- +// The bootloader is the highest-risk part of V2: a page either declares the +// registry + V2 runtime itself, or relies on script.js to inject them. Both +// paths must stay unambiguous and free of duplicate runtime loads. +const runtimeFiles = ["portfolio-data.js", "script.js", "legacy-script.js", "portfolio-v2.js"]; +const htmlFiles = fs.readdirSync(".").filter((file) => file.endsWith(".html")); + +htmlFiles.forEach((file) => { + const source = read(file); + const scriptSrcs = [...source.matchAll(/]+src="([^"]+)"/g)].map((match) => match[1]); + + runtimeFiles.forEach((runtime) => { + const occurrences = scriptSrcs.filter((src) => src === runtime).length; + check(occurrences <= 1, `${file} declares ${runtime} ${occurrences} times (duplicate runtime load)`); + }); + + check( + !scriptSrcs.includes("legacy-script.js"), + `${file} must not load legacy-script.js directly; the bootloader owns that execution order`, + ); + + const dataIndex = scriptSrcs.indexOf("portfolio-data.js"); + const bootIndex = scriptSrcs.indexOf("script.js"); + const v2Index = scriptSrcs.indexOf("portfolio-v2.js"); + + if (dataIndex !== -1 || v2Index !== -1) { + // Migrated page: registry before the bootloader, V2 runtime after it. + check(dataIndex !== -1 && v2Index !== -1, `${file} must declare both portfolio-data.js and portfolio-v2.js, or neither`); + check(bootIndex !== -1, `${file} must still load script.js between the registry and the V2 runtime`); + check(dataIndex < bootIndex, `${file} must declare portfolio-data.js before script.js`); + check(bootIndex < v2Index, `${file} must declare portfolio-v2.js after script.js`); + check(source.includes("portfolio-v2.css"), `${file} declares the V2 runtime but never links portfolio-v2.css`); + } + + check((source.match(/portfolio-v2\.css/g) || []).length <= 1, `${file} links portfolio-v2.css more than once`); + + // Unified EN/TR copy must stay paired, otherwise one language silently wins. + const enCopy = (source.match(/data-pv2-en=/g) || []).length; + const trCopy = (source.match(/data-pv2-tr=/g) || []).length; + check(enCopy === trCopy, `${file} has ${enCopy} data-pv2-en vs ${trCopy} data-pv2-tr attributes; unified copy must stay paired`); +}); + +// --- Dynamic archive route -------------------------------------------------- +const legacyRuntime = read("legacy-script.js"); +check( + !/params\.get\("project"\)\s*\|\|/.test(legacyRuntime), + "project-detail must not fall back to a hardcoded slug; a missing or unknown project belongs in the not-found state", +); + +// --- Command palette targets ------------------------------------------------ +// Experiments moved to labs.html in V2, so same-page scroll commands would +// silently no-op on every other page. +check( + !legacyRuntime.includes('value: "algorithmic-3d-lab"'), + "the 3D lab command must navigate to labs.html#algorithmic-3d-lab instead of scrolling to a section that no longer exists on most pages", +); +check(read("labs.html").includes('id="algorithmic-3d-lab"'), "labs.html must keep the algorithmic-3d-lab anchor target"); +check( + (registry?.labs || []).some((item) => (item.url || "").includes("algorithmic-3d-lab")), + "registry Labs must keep the Algorithmic 3D Lab entry that the command palette points at", +); + +// --- SINAMA Evidence Explorer ----------------------------------------------- +const v2Runtime = read("portfolio-v2.js"); +check( + /sinamaEvidence\[[^\]]+\]\.label/.test(v2Runtime), + "the Evidence Explorer must read scenario labels from the registry instead of duplicating product truth", +); +["healthy", "broken"].forEach((id) => { + const conversation = registry?.sinamaEvidence?.[id]?.conversation || []; + check(conversation.length > 0, `sinamaEvidence.${id} needs conversation turns`); + check( + conversation.every((turn) => turn.speaker && typeof turn.speaker === "object" && turn.speaker.en && turn.speaker.tr), + `sinamaEvidence.${id} speaker names must be bilingual so the explorer is not half-English in TR`, + ); +}); + if (failures.length) { console.error(`Portfolio consistency guard failed with ${failures.length} issue(s):`); failures.forEach((failure) => console.error(`- ${failure}`));