diff --git a/.agents/skills/validate-sas-release/SKILL.md b/.agents/skills/validate-sas-release/SKILL.md index f41d79e6..1b78c971 100644 --- a/.agents/skills/validate-sas-release/SKILL.md +++ b/.agents/skills/validate-sas-release/SKILL.md @@ -3,7 +3,7 @@ name: validate-sas-release description: Validate Super Agent Skill changes before publication, using the existing local checks without Lovable generation credits. --- -Run `npm ci` when locked dependencies changed. Run `npm run check:project` from the repository root. This batches tests, types, dependency audit, build, strict lint, content validation and both generated-artifact checks. It writes actual exit codes and logs to `artifacts/validation/`; failures block release. +Run `npm ci` when locked dependencies changed. Run `npm run check:project` from the repository root. Its `check:navigation` step audits the generated route hierarchy and rendered local wrappers (including `SitePage`) for duplicate global navigation in nested layouts; retain `artifacts/validation/navigation.json`. Global navigation belongs to the ancestor layout, while child content retains its controls and authorization. Verify affected pages visually in the authenticated browser after deployment; the static check does not replace visual judgment. This batches tests, types, dependency audit, build, strict lint, content validation and both generated-artifact checks. It writes actual exit codes and logs to `artifacts/validation/`; failures block release. Use `npm run check:project -- --security` with the existing Supabase publishable environment to include RLS and anonymous-access checks. Never expose credentials or weaken allowlists to pass. With a local dev server running, add `--ui` to test signup at mobile and desktop sizes with intercepted auth; review its screenshots personally. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b4fa16e..f18601fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ Component artifacts are versioned and released independently via git tags: - **`super-agent` CLI** — `cli-v*` tags publish cross-platform binaries. - **`@superagentskill/sdk`** — versioned in `packages/sdk-ts/package.json`. +## [0.6.8] — 2026-10-24 — *Genesis* (alpha) — Deduplicate admin page navigation + +### Changed + +- Remove duplicate global menus from catalog curation, judge calibration and funnel reports. +- Audit nested route navigation in the release validation batch. + ## [0.6.7] — 2026-10-24 — *Genesis* (alpha) — Fix duplicate admin navigation ### Changed diff --git a/package-lock.json b/package-lock.json index c70aa625..a63f3ad8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tanstack_start_ts", - "version": "0.6.7", + "version": "0.6.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tanstack_start_ts", - "version": "0.6.7", + "version": "0.6.8", "dependencies": { "@ai-sdk/openai-compatible": "^2.0.47", "@cloudflare/vite-plugin": "1.63.0", diff --git a/package.json b/package.json index 349d392c..3bf67744 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tanstack_start_ts", - "version": "0.6.7", + "version": "0.6.8", "private": true, "sideEffects": false, "type": "module", @@ -40,6 +40,7 @@ "test:ts": "node --experimental-strip-types --test tests/site-stats.test.ts tests/certify.test.ts tests/prompt-injection-guard.test.ts tests/audit-skills.test.ts tests/runtime.test.ts tests/integrations.test.ts tests/growth-revenue-split.test.ts tests/trust-badge.test.ts tests/bounties.test.ts tests/trust-scoring.test.ts tests/adversarial-holdout.test.ts tests/adversarial-judge.test.ts tests/paired-benchmark.test.ts tests/format-rubric.test.ts tests/lang-detect.test.ts tests/university.test.ts tests/package-signature.test.ts", "test:all": "node --experimental-strip-types --test tests/*.test.ts", "lint:strict": "eslint .", + "check:navigation": "node scripts/check-navigation.mjs", "check:project": "node scripts/check-project.mjs", "test:ui": "node scripts/check-signup-ui.mjs && node --experimental-strip-types scripts/check-business-ui.mjs", "check:types": "tsc --noEmit", diff --git a/plugin.json b/plugin.json index 42fae10e..592663a0 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "superagentskill", - "version": "0.6.7", + "version": "0.6.8", "description": "Agent skills and candidate workflows from Super Agent Skill. Published registry packages expose available trust and execution evidence; candidate workflows include offline fixtures.", "author": { "name": "Super Agent Skill", diff --git a/scripts/check-navigation.mjs b/scripts/check-navigation.mjs new file mode 100644 index 00000000..283186b4 --- /dev/null +++ b/scripts/check-navigation.mjs @@ -0,0 +1,108 @@ +import ts from "typescript"; +import { readdirSync, readFileSync, mkdirSync, writeFileSync } from "node:fs"; +import { resolve, dirname, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +// Follow rendered local components, including wrappers such as SitePage, rather +// than merely counting Nav imports (loading/error branches are alternatives). +export function auditNavigation(sources, tree) { + if (!Object.keys(sources).length || !tree.trim()) throw new Error("Empty navigation audit"); + const parsed = new Map(); + function info(file) { + if (parsed.has(file)) return parsed.get(file); + const source = sources[file]; + if (source === undefined) return null; + const ast = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); + const imports = new Map(); + const tags = new Set(); + function visit(node) { + if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { + const path = node.moduleSpecifier.text; + const bindings = node.importClause?.namedBindings; + if (bindings && ts.isNamedImports(bindings)) { + for (const item of bindings.elements) imports.set(item.name.text, path); + } + if (node.importClause?.name) imports.set(node.importClause.name.text, path); + } + if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) + tags.add(node.tagName.getText(ast)); + ts.forEachChild(node, visit); + } + visit(ast); + const result = { imports, tags }; + parsed.set(file, result); + return result; + } + function ownsNavigation(file, visited = new Set()) { + if (visited.has(file)) return false; + visited.add(file); + const data = info(file); + if (!data) return false; + if (file === "src/components/site/Nav.tsx") return true; + for (const tag of data.tags) { + const target = data.imports.get(tag); + if (!target || (!target.startsWith("@/") && !target.startsWith("."))) continue; + const base = target.startsWith("@/") + ? `src/${target.slice(2)}` + : relative(process.cwd(), resolve(dirname(file), target)); + const candidate = [`${base}.tsx`, `${base}/index.tsx`, base].find((path) => path in sources); + if (candidate && ownsNavigation(candidate, new Set(visited))) return true; + } + return false; + } + const imports = new Map( + [...tree.matchAll(/import \{ Route as (\w+) \} from ['"]\.\/(routes\/[^'"]+)['"]/g)].map( + (m) => [m[1], [`src/${m[2]}.tsx`, `src/${m[2]}.ts`].find((path) => path in sources)], + ), + ); + const routes = new Map( + [...tree.matchAll(/const (\w+)\s*=\s*(\w+)\.update\(\{([\s\S]*?)\}\s*as/g)].map((m) => [ + m[1], + { file: imports.get(m[2]), parent: m[3].match(/getParentRoute:\s*\(\)\s*=>\s*(\w+)/)?.[1] }, + ]), + ); + if (!routes.size || [...routes.values()].some((r) => !r.file || !sources[r.file])) + throw new Error("Incomplete generated route inventory"); + if (routes.size !== imports.size - 1) throw new Error("Incomplete generated route coverage"); + routes.set("rootRouteImport", { file: imports.get("rootRouteImport"), parent: undefined }); + const violations = []; + let nestedChecks = 0; + for (const route of routes.values()) { + let ancestor = routes.get(route.parent); + const seen = new Set(); + while (ancestor && !seen.has(ancestor.file)) { + seen.add(ancestor.file); + // A route without an Outlet does not render its child inside its chrome. + if (info(ancestor.file)?.tags.has("Outlet")) { + nestedChecks++; + if (ownsNavigation(route.file) && ownsNavigation(ancestor.file)) + violations.push({ child: route.file, layout: ancestor.file }); + } + ancestor = routes.get(ancestor.parent); + } + } + return { + routes: routes.size, + navigationOwners: [...routes.values()].filter((r) => ownsNavigation(r.file)).length, + nestedChecks, + violations, + }; +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + if (process.argv.length > 2) throw new Error("Usage: npm run check:navigation"); + const sources = {}; + function collect(dir) { + for (const item of readdirSync(dir, { withFileTypes: true })) { + const path = `${dir}/${item.name}`; + if (item.isDirectory()) collect(path); + else if (/\.tsx?$/.test(path)) sources[path] = readFileSync(path, "utf8"); + } + } + collect("src"); + const report = auditNavigation(sources, readFileSync("src/routeTree.gen.ts", "utf8")); + mkdirSync("artifacts/validation", { recursive: true }); + writeFileSync("artifacts/validation/navigation.json", JSON.stringify(report, null, 2) + "\n"); + console.log(JSON.stringify(report)); + process.exitCode = report.violations.length ? 1 : 0; +} diff --git a/scripts/check-project.mjs b/scripts/check-project.mjs index 25552b7c..7d986eef 100644 --- a/scripts/check-project.mjs +++ b/scripts/check-project.mjs @@ -12,6 +12,7 @@ const checks = [ "check:business-skills", "check:skills-mirror", "check:agent-plugin", + "check:navigation", ]; if (process.argv.includes("--security")) checks.push("check:security"); if (process.argv.includes("--ui")) checks.push("test:ui"); diff --git a/src/lib/version.ts b/src/lib/version.ts index 85756e08..cd58b397 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -12,7 +12,7 @@ * We are still pre-1.0: rapid evolution, no stability guarantees. */ -export const PLATFORM_VERSION = "0.6.7" as const; +export const PLATFORM_VERSION = "0.6.8" as const; export const PLATFORM_CODENAME = "Genesis" as const; export const PLATFORM_STAGE: "alpha" | "beta" | "ga" = "alpha"; export const PLATFORM_BUILD_DATE = "2026-10-24" as const; @@ -30,6 +30,16 @@ export type ChangelogEntry = { * Every deployed change appends a new entry (or amends the top one before shipping). */ export const CHANGELOG: ChangelogEntry[] = [ + { + version: "0.6.8", + date: "2026-10-24", + kind: "patch", + title: "Deduplicate admin page navigation", + highlights: [ + "Remove duplicate global menus from catalog curation, judge calibration and funnel reports.", + "Audit nested route navigation in the release validation batch.", + ], + }, { version: "0.6.7", date: "2026-10-24", diff --git a/src/routes/admin.calibration.tsx b/src/routes/admin.calibration.tsx index bc655c58..c700fe66 100644 --- a/src/routes/admin.calibration.tsx +++ b/src/routes/admin.calibration.tsx @@ -3,7 +3,7 @@ import { useQuery, useMutation } from "@tanstack/react-query"; import { useServerFn } from "@tanstack/react-start"; import { useState } from "react"; import { toast } from "sonner"; -import { SitePage } from "@/components/site/SitePage"; +import { Footer } from "@/components/site/Footer"; import { getJudgeCalibration, runAdversarialWithJudge, @@ -67,8 +67,8 @@ function AdminCalibrationPage() { const rows = (q.data?.rows ?? []) as JudgeCalibrationRow[]; return ( - - + <> + Admin Judge calibration @@ -188,8 +188,9 @@ function AdminCalibrationPage() { - - + + + > ); } diff --git a/src/routes/admin.curation.tsx b/src/routes/admin.curation.tsx index 24d1af7d..e20c0d91 100644 --- a/src/routes/admin.curation.tsx +++ b/src/routes/admin.curation.tsx @@ -1,7 +1,7 @@ import { createFileRoute, Link } from "@tanstack/react-router"; import { useServerFn } from "@tanstack/react-start"; import { useQuery } from "@tanstack/react-query"; -import { SitePage } from "@/components/site/SitePage"; +import { Footer } from "@/components/site/Footer"; import { getCatalogCuration } from "@/lib/admin/curation.functions"; export const Route = createFileRoute("/admin/curation")({ head: () => ({ meta: [{ title: "Catalog curation — Super Agent Skill" }] }), @@ -11,8 +11,8 @@ function Curation() { const get = useServerFn(getCatalogCuration); const q = useQuery({ queryKey: ["catalog-curation"], queryFn: () => get(), retry: false }); return ( - - + <> + Catalog curation Find potentially overlapping published packages. Inspect contracts, examples, behavior and @@ -69,7 +69,8 @@ function Curation() { ))} > )} - - + + + > ); } diff --git a/src/routes/admin.funnel.tsx b/src/routes/admin.funnel.tsx index 062a3f23..ad9234a4 100644 --- a/src/routes/admin.funnel.tsx +++ b/src/routes/admin.funnel.tsx @@ -2,7 +2,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { useQuery } from "@tanstack/react-query"; import { useServerFn } from "@tanstack/react-start"; import { useState } from "react"; -import { SitePage } from "@/components/site/SitePage"; +import { Footer } from "@/components/site/Footer"; import { getMcpFunnelSummary, getMcpFunnelTrafficBreakdown, @@ -107,8 +107,8 @@ function AdminFunnelPage() { const humanPct = totalAll ? ((totals.human / totalAll) * 100).toFixed(1) : "0.0"; return ( - - + <> + Admin Funnel & traffic @@ -269,8 +269,9 @@ function AdminFunnelPage() { - - + + + > ); } diff --git a/tests/navigation-layout.test.ts b/tests/navigation-layout.test.ts new file mode 100644 index 00000000..8954e941 --- /dev/null +++ b/tests/navigation-layout.test.ts @@ -0,0 +1,60 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { auditNavigation } from "../scripts/check-navigation.mjs"; + +const tree = ` +import { Route as rootRouteImport } from './routes/__root' +import { Route as AdminImport } from './routes/admin' +import { Route as ChildImport } from './routes/admin.child' +const Admin = AdminImport.update({ getParentRoute: () => rootRouteImport } as never) +const Child = ChildImport.update({ getParentRoute: () => Admin } as never) +`; +const sources = { + "src/routes/__root.tsx": `function Root() { return ; }`, + "src/routes/admin.tsx": `import { Nav } from '@/components/site/Nav'; function Layout() { return <>>; }`, + "src/routes/admin.child.tsx": `function Child() { return Review; }`, + "src/components/site/Nav.tsx": `export function Nav() { return Global; }`, + "src/components/site/SitePage.tsx": `import { Nav } from './Nav'; export function SitePage({children}) { return <>{children}>; }`, +}; + +test("navigation audit checks nested routes and accepts content owned by the parent layout", () => { + const report = auditNavigation(sources, tree); + assert.equal(report.routes, 3); + assert.equal(report.nestedChecks, 3); + assert.deepEqual(report.violations, []); +}); + +test("navigation audit rejects both direct and wrapper-based duplicate menus", () => { + for (const [name, path] of [ + ["Nav", "Nav"], + ["SitePage", "SitePage"], + ]) { + const report = auditNavigation( + { + ...sources, + "src/routes/admin.child.tsx": `import { ${name} as Chrome } from '@/components/site/${path}'; function Child() { return ; }`, + }, + tree, + ); + assert.deepEqual(report.violations, [ + { child: "src/routes/admin.child.tsx", layout: "src/routes/admin.tsx" }, + ]); + } +}); + +test("navigation audit does not treat alternative loading and ready branches as duplicate menus", () => { + const report = auditNavigation( + { + ...sources, + "src/routes/admin.tsx": `import { Nav } from '@/components/site/Nav'; function Layout() { if (loading) return ; return <>>; }`, + }, + tree, + ); + assert.deepEqual(report.violations, []); +}); + +test("navigation audit rejects empty and incomplete inventories", () => { + assert.throws(() => auditNavigation({}, tree), /Empty/); + assert.throws(() => auditNavigation(sources, ""), /Empty/); + assert.throws(() => auditNavigation({ "src/routes/admin.tsx": "" }, tree), /Incomplete/); +});
Admin
Find potentially overlapping published packages. Inspect contracts, examples, behavior and @@ -69,7 +69,8 @@ function Curation() { ))} > )} - - +
@@ -269,8 +269,9 @@ function AdminFunnelPage() {