Publish curated Knowledge Hub source from 2ed833f290b439a7bc91e5fd19b042c269e3bee9
This commit is contained in:
1 parent
10f2edcd29
commit
59f15b2d08
218 files changed
+23995
-1849
No files matched your search
@@ -0,0 +1,23 @@
|
||||
/** Extract literal Minimal API routes and their receiver-specific MapGroup prefix. */
|
||||
export function extractRoutesFromSource(source, file) {
|
||||
const prefixes = new Map([["app", ""]]);
|
||||
const groupPattern = /\b(?:var|const|RouteGroupBuilder)\s+(\w+)\s*=\s*(\w+)\.MapGroup\(\s*"([^"]+)"\s*\)/g;
|
||||
for (const match of source.matchAll(groupPattern)) {
|
||||
const parent = prefixes.get(match[2]);
|
||||
if (parent === undefined) throw new Error(`Unknown MapGroup receiver ${match[2]} in ${file}`);
|
||||
prefixes.set(match[1], parent + match[3]);
|
||||
}
|
||||
const routes = [];
|
||||
const mapPattern = /\b(\w+)\.Map(Get|Post|Put|Delete|Patch)\(\s*"([^"]*)"/g;
|
||||
for (const match of source.matchAll(mapPattern)) {
|
||||
const prefix = prefixes.get(match[1]);
|
||||
if (prefix === undefined) throw new Error(`Unknown route receiver ${match[1]} in ${file}`);
|
||||
const path = (prefix + match[3]).replace(/\{([a-zA-Z0-9_]+)(:[a-zA-Z]+)?\}/g, "{$1}");
|
||||
if (!path.startsWith("/api/v1")) {
|
||||
if (!path.startsWith("/health")) throw new Error(`Unexpected non-API route ${path} in ${file}`);
|
||||
continue;
|
||||
}
|
||||
routes.push({ method: match[2].toUpperCase(), path: path.slice("/api/v1".length) || "/", file });
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
export function validateProductBacklog(backlog, plan) {
|
||||
const errors = [];
|
||||
if (backlog?.schemaVersion !== 2) errors.push("Product backlog schemaVersion must be 2.");
|
||||
if (!Array.isArray(backlog?.statusValues)) errors.push("Product backlog statusValues must be an array.");
|
||||
if (!Array.isArray(backlog?.items)) {
|
||||
errors.push("Product backlog items must be an array.");
|
||||
return errors;
|
||||
}
|
||||
const milestoneById = new Map((plan?.milestones ?? []).map((milestone) => [milestone.id, milestone]));
|
||||
const itemIds = new Set();
|
||||
const statuses = new Set(backlog.statusValues ?? []);
|
||||
for (const item of backlog.items) {
|
||||
if (!item || typeof item !== "object") {
|
||||
errors.push("Product backlog contains an invalid item.");
|
||||
continue;
|
||||
}
|
||||
if (typeof item.id !== "string" || !item.id || itemIds.has(item.id)) {
|
||||
errors.push(`Product backlog item has a missing or duplicate ID: ${item.id ?? "(missing)"}.`);
|
||||
}
|
||||
itemIds.add(item.id);
|
||||
if (!statuses.has(item.status)) errors.push(`${item.id} has an invalid status: ${item.status}.`);
|
||||
const milestone = milestoneById.get(item.milestone);
|
||||
if (!milestone) {
|
||||
errors.push(`${item.id} references an unknown milestone: ${item.milestone}.`);
|
||||
continue;
|
||||
}
|
||||
if (item.status === "complete" && (!Array.isArray(item.evidence) || item.evidence.length === 0)) {
|
||||
errors.push(`${item.id} is complete without evidence references.`);
|
||||
}
|
||||
if (item.status === "todo" && milestone.status === "complete" && item.postMilestone !== true) {
|
||||
errors.push(`${item.id} is todo in a completed milestone without an explicit postMilestone marker.`);
|
||||
}
|
||||
if (item.status === "partial" && (!item.notes || !Array.isArray(item.evidence) || item.evidence.length === 0)) {
|
||||
errors.push(`${item.id} is partial without notes and evidence references.`);
|
||||
}
|
||||
if (item.status === "blocked_external") {
|
||||
const acceptance = milestone.acceptance?.find((criterion) => criterion.id === item.acceptance);
|
||||
if (!item.externalOwner || !acceptance?.external || acceptance.met !== false) {
|
||||
errors.push(`${item.id} must reference an unmet external acceptance criterion and owner.`);
|
||||
}
|
||||
}
|
||||
if (item.status === "complete" && milestone.status !== "complete") {
|
||||
const acceptance = milestone.acceptance?.find((criterion) => criterion.id === item.acceptance);
|
||||
if (!acceptance || acceptance.met !== true) {
|
||||
errors.push(`${item.id} completes an unfinished milestone without a met acceptance criterion.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { commandExists } from "./exec.mjs";
|
||||
|
||||
export function requireReleaseTool(command, exists = commandExists) {
|
||||
if (!exists(command)) {
|
||||
throw new Error(`${command} is required for the release gate but was not found on PATH.`);
|
||||
}
|
||||
return command;
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
function skipQuoted(source, start) {
|
||||
const quote = source[start];
|
||||
let escaped = false;
|
||||
for (let index = start + 1; index < source.length; index += 1) {
|
||||
const character = source[index];
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (character === "\\") {
|
||||
escaped = true;
|
||||
} else if (character === quote) {
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
return source.length;
|
||||
}
|
||||
|
||||
function readRouteTag(source, start) {
|
||||
const closing = source[start + 1] === "/";
|
||||
const nameStart = start + (closing ? 2 : 1);
|
||||
const nameMatch = /^[A-Za-z_$][\w$.-]*/u.exec(source.slice(nameStart));
|
||||
if (!nameMatch || nameMatch[0] !== "Route") return null;
|
||||
|
||||
let curlyDepth = 0;
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
for (let index = nameStart + nameMatch[0].length; index < source.length; index += 1) {
|
||||
const character = source[index];
|
||||
if (quote !== null) {
|
||||
if (escaped) escaped = false;
|
||||
else if (character === "\\") escaped = true;
|
||||
else if (character === quote) quote = null;
|
||||
continue;
|
||||
}
|
||||
if (character === "\"" || character === "'" || character === "`") {
|
||||
quote = character;
|
||||
} else if (character === "{") {
|
||||
curlyDepth += 1;
|
||||
} else if (character === "}" && curlyDepth > 0) {
|
||||
curlyDepth -= 1;
|
||||
} else if (character === ">" && curlyDepth === 0) {
|
||||
const text = source.slice(nameStart + nameMatch[0].length, index);
|
||||
return {
|
||||
closing,
|
||||
selfClosing: !closing && /\/\s*$/u.test(text),
|
||||
attributes: closing ? "" : text.replace(/\/\s*$/u, ""),
|
||||
end: index + 1
|
||||
};
|
||||
}
|
||||
}
|
||||
return { error: "AuthenticatedRoutes.tsx contains an unterminated <Route> tag." };
|
||||
}
|
||||
|
||||
function findStaticPath(attributes) {
|
||||
const pathAttribute = /(?:^|\s)path\s*=\s*(?:"([^"]*)"|'([^']*)')/u.exec(attributes);
|
||||
if (pathAttribute) return { value: pathAttribute[1] ?? pathAttribute[2] };
|
||||
if (/(?:^|\s)path\s*=\s*\{/u.test(attributes)) return { error: "dynamic" };
|
||||
return null;
|
||||
}
|
||||
|
||||
function routePath(parentPath, childPath) {
|
||||
if (childPath === "*") return parentPath ? `${parentPath.replace(/\/$/u, "")}/*` : "*";
|
||||
if (childPath.startsWith("/")) return childPath.replace(/\/{2,}/gu, "/");
|
||||
const joined = `${parentPath || ""}/${childPath}`.replace(/\/{2,}/gu, "/");
|
||||
return joined || "/";
|
||||
}
|
||||
|
||||
function routeFeatures(attributes) {
|
||||
const features = new Set();
|
||||
for (const match of attributes.matchAll(/\bfeatures\s*\?\.\s*([A-Za-z_$][\w$]*)/gu)) {
|
||||
features.add(match[1]);
|
||||
}
|
||||
return [...features].sort();
|
||||
}
|
||||
|
||||
function extractRouteStructure(source) {
|
||||
const errors = [];
|
||||
const routeStack = [];
|
||||
const records = [];
|
||||
|
||||
for (let index = 0; index < source.length;) {
|
||||
if (source.startsWith("//", index)) {
|
||||
const newline = source.indexOf("\n", index + 2);
|
||||
index = newline < 0 ? source.length : newline + 1;
|
||||
continue;
|
||||
}
|
||||
if (source.startsWith("/*", index)) {
|
||||
const commentEnd = source.indexOf("*/", index + 2);
|
||||
index = commentEnd < 0 ? source.length : commentEnd + 2;
|
||||
continue;
|
||||
}
|
||||
if (source[index] === "\"" || source[index] === "'" || source[index] === "`") {
|
||||
index = skipQuoted(source, index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source[index] === "<" && (source[index + 1] === "/" || /[A-Za-z_$]/u.test(source[index + 1] ?? ""))) {
|
||||
const tag = readRouteTag(source, index);
|
||||
if (tag?.error) {
|
||||
errors.push(tag.error);
|
||||
break;
|
||||
}
|
||||
if (tag) {
|
||||
if (tag.closing) {
|
||||
if (routeStack.length === 0) errors.push("AuthenticatedRoutes.tsx closes a <Route> without a matching opening tag.");
|
||||
else routeStack.pop();
|
||||
} else {
|
||||
const parent = routeStack.at(-1);
|
||||
const parentPath = parent?.path ?? "";
|
||||
const pathAttribute = findStaticPath(tag.attributes);
|
||||
const hasIndex = /(?:^|\s)index(?:\s|=|$)/u.test(tag.attributes);
|
||||
const localFeatures = routeFeatures(tag.attributes);
|
||||
const features = [...new Set([...(parent?.features ?? []), ...localFeatures])].sort();
|
||||
const administrator = Boolean(parent?.administrator)
|
||||
|| /\bsession\.role\s*===?\s*["']Administrator["']/u.test(tag.attributes);
|
||||
|
||||
if (pathAttribute?.error === "dynamic") {
|
||||
errors.push("AuthenticatedRoutes.tsx uses a dynamic Route path; route-map validation requires a static path.");
|
||||
}
|
||||
if (hasIndex && pathAttribute) errors.push("AuthenticatedRoutes.tsx declares a Route with both index and path.");
|
||||
|
||||
let fullPath = null;
|
||||
if (hasIndex) fullPath = parentPath || "/";
|
||||
else if (pathAttribute && !pathAttribute.error) fullPath = routePath(parentPath, pathAttribute.value);
|
||||
|
||||
if (fullPath !== null) {
|
||||
records.push({
|
||||
path: fullPath,
|
||||
featureGates: features,
|
||||
auth: administrator ? "administrator" : "authenticated",
|
||||
depth: routeStack.length,
|
||||
parentPath
|
||||
});
|
||||
}
|
||||
|
||||
if (!tag.selfClosing) {
|
||||
routeStack.push({
|
||||
path: fullPath ?? parentPath,
|
||||
features,
|
||||
administrator
|
||||
});
|
||||
}
|
||||
}
|
||||
index = tag.end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if (routeStack.length > 0) errors.push("AuthenticatedRoutes.tsx contains an unclosed <Route> tag.");
|
||||
|
||||
const uniqueRecords = [];
|
||||
for (const record of records) {
|
||||
const priorIndex = uniqueRecords.findIndex((candidate) => candidate.path === record.path);
|
||||
if (priorIndex < 0) {
|
||||
uniqueRecords.push(record);
|
||||
continue;
|
||||
}
|
||||
|
||||
const prior = uniqueRecords[priorIndex];
|
||||
const isNestedSamePath = record.parentPath === record.path && record.depth > prior.depth;
|
||||
if (isNestedSamePath) {
|
||||
uniqueRecords[priorIndex] = record;
|
||||
} else {
|
||||
errors.push(`AuthenticatedRoutes.tsx declares duplicate route path ${record.path}.`);
|
||||
}
|
||||
}
|
||||
|
||||
return { records: uniqueRecords, errors };
|
||||
}
|
||||
|
||||
export function extractAuthenticatedRoutes(source) {
|
||||
return extractRouteStructure(source).records;
|
||||
}
|
||||
|
||||
export function extractAuthenticatedRoutePaths(source) {
|
||||
return extractAuthenticatedRoutes(source).map((route) => route.path);
|
||||
}
|
||||
|
||||
function mappedFeatureGates(route) {
|
||||
if (Array.isArray(route?.featureGates)) return [...route.featureGates].sort();
|
||||
return typeof route?.featureGate === "string" ? [route.featureGate] : [];
|
||||
}
|
||||
|
||||
export function validateRouteMap(routeMap, authenticatedSource, sessionSource) {
|
||||
const errors = [];
|
||||
const { records, errors: routeErrors } = extractRouteStructure(authenticatedSource);
|
||||
errors.push(...routeErrors);
|
||||
const actualPaths = records.map((route) => route.path);
|
||||
const mappedRoutes = Array.isArray(routeMap?.routes) ? routeMap.routes : [];
|
||||
const mappedPaths = mappedRoutes.map((route) => route?.path);
|
||||
|
||||
if (new Set(mappedPaths).size !== mappedPaths.length) {
|
||||
errors.push("product/route-map.json declares duplicate paths.");
|
||||
}
|
||||
const missing = actualPaths.filter((routePath) => !mappedPaths.includes(routePath));
|
||||
const stale = mappedPaths.filter((routePath) => !actualPaths.includes(routePath));
|
||||
if (missing.length > 0) errors.push(`Route map is missing: ${missing.join(", ")}.`);
|
||||
if (stale.length > 0) errors.push(`Route map has stale entries: ${stale.join(", ")}.`);
|
||||
|
||||
if (routeMap?.sourceOfTruth !== "src/frontend/src/router/AuthenticatedRoutes.tsx") {
|
||||
errors.push("Route map sourceOfTruth must identify AuthenticatedRoutes.tsx.");
|
||||
}
|
||||
const states = routeMap?.sessionStates;
|
||||
const expectedStates = ["loading", "error", "setupRequired", "unauthenticated"];
|
||||
if (!states || typeof states !== "object" || Array.isArray(states)) {
|
||||
errors.push("Route map must document the pre-route session states.");
|
||||
} else {
|
||||
const actualStates = Object.keys(states).sort();
|
||||
if (JSON.stringify(actualStates) !== JSON.stringify([...expectedStates].sort())) {
|
||||
errors.push(`Route map session states must be ${expectedStates.join(", ")}.`);
|
||||
}
|
||||
for (const [condition, screen] of Object.entries(states)) {
|
||||
if (typeof screen !== "string" || !sessionSource.includes(`<${screen}`)) {
|
||||
errors.push(`Session screen is not present in App.tsx: ${condition}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const actualRoute of records) {
|
||||
const mappedRoute = mappedRoutes.find((route) => route?.path === actualRoute.path);
|
||||
if (!mappedRoute) continue;
|
||||
if (mappedRoute.auth !== actualRoute.auth) {
|
||||
errors.push(`${actualRoute.path} must be marked ${actualRoute.auth}-only.`);
|
||||
}
|
||||
const expectedFeatures = actualRoute.featureGates;
|
||||
const mappedFeatures = mappedFeatureGates(mappedRoute);
|
||||
if (JSON.stringify(expectedFeatures) !== JSON.stringify(mappedFeatures)) {
|
||||
const expected = expectedFeatures.length > 0 ? expectedFeatures.join(", ") : "none";
|
||||
errors.push(`${actualRoute.path} feature-gate registry must be ${expected}.`);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
Reference in new issue
Block a user