Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/validate-sas-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tanstack_start_ts",
"version": "0.6.7",
"version": "0.6.8",
"private": true,
"sideEffects": false,
"type": "module",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
108 changes: 108 additions & 0 deletions scripts/check-navigation.mjs
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions scripts/check-project.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
12 changes: 11 additions & 1 deletion src/lib/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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",
Expand Down
11 changes: 6 additions & 5 deletions src/routes/admin.calibration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -67,8 +67,8 @@ function AdminCalibrationPage() {
const rows = (q.data?.rows ?? []) as JudgeCalibrationRow[];

return (
<SitePage>
<main className="mx-auto max-w-4xl px-4 py-10 md:py-14">
<>
<div className="mx-auto max-w-4xl px-4 py-10 md:py-14">
<p className="font-mono text-[11px] uppercase tracking-[0.22em] text-primary">Admin</p>
<h1 className="mt-2 text-3xl font-semibold tracking-tight md:text-4xl">
Judge calibration
Expand Down Expand Up @@ -188,8 +188,9 @@ function AdminCalibrationPage() {
</tbody>
</table>
</div>
</main>
</SitePage>
</div>
<Footer />
</>
);
}

Expand Down
11 changes: 6 additions & 5 deletions src/routes/admin.curation.tsx
Original file line number Diff line number Diff line change
@@ -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" }] }),
Expand All @@ -11,8 +11,8 @@ function Curation() {
const get = useServerFn(getCatalogCuration);
const q = useQuery({ queryKey: ["catalog-curation"], queryFn: () => get(), retry: false });
return (
<SitePage>
<main className="mx-auto max-w-5xl space-y-6 px-4 py-12">
<>
<div className="mx-auto max-w-5xl space-y-6 px-4 py-12">
<h1 className="text-3xl font-semibold">Catalog curation</h1>
<p>
Find potentially overlapping published packages. Inspect contracts, examples, behavior and
Expand Down Expand Up @@ -69,7 +69,8 @@ function Curation() {
))}
</>
)}
</main>
</SitePage>
</div>
<Footer />
</>
);
}
11 changes: 6 additions & 5 deletions src/routes/admin.funnel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -107,8 +107,8 @@ function AdminFunnelPage() {
const humanPct = totalAll ? ((totals.human / totalAll) * 100).toFixed(1) : "0.0";

return (
<SitePage>
<main className="mx-auto max-w-4xl px-4 py-10 md:py-14">
<>
<div className="mx-auto max-w-4xl px-4 py-10 md:py-14">
<p className="font-mono text-[11px] uppercase tracking-[0.22em] text-primary">Admin</p>
<h1 className="mt-2 text-3xl font-semibold tracking-tight md:text-4xl">Funnel & traffic</h1>
<p className="mt-2 text-sm text-muted-foreground">
Expand Down Expand Up @@ -269,8 +269,9 @@ function AdminFunnelPage() {
</tbody>
</table>
</div>
</main>
</SitePage>
</div>
<Footer />
</>
);
}

Expand Down
60 changes: 60 additions & 0 deletions tests/navigation-layout.test.ts
Original file line number Diff line number Diff line change
@@ -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 <Outlet />; }`,
"src/routes/admin.tsx": `import { Nav } from '@/components/site/Nav'; function Layout() { return <><Nav /><Outlet /></>; }`,
"src/routes/admin.child.tsx": `function Child() { return <section>Review</section>; }`,
"src/components/site/Nav.tsx": `export function Nav() { return <nav>Global</nav>; }`,
"src/components/site/SitePage.tsx": `import { Nav } from './Nav'; export function SitePage({children}) { return <><Nav />{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 <Chrome />; }`,
},
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 <Nav />; return <><Nav /><Outlet /></>; }`,
},
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/);
});
Loading