Files
antigravity-skills-reference/tools/lib/workflow-contract.js
sickn33 9e1e9c97a1 fix(ci): Track canonical plugin drift
Treat generated plugin mirrors and marketplace outputs as managed
canonical artifacts so the main-branch sync bot can stage and commit
them instead of failing on unmanaged drift.

Ignore web-app coverage output during maintainer runs and update the
mirrored Office unpack scripts so plugin copies stay aligned with the
hardened source implementations.
2026-03-29 09:50:20 +02:00

198 lines
5.6 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const { findProjectRoot } = require("./project-root");
const DOC_PREFIXES = ["docs/"];
const DOC_FILES = new Set(["README.md", "CONTRIBUTING.md", "CHANGELOG.md", "walkthrough.md"]);
const INFRA_PREFIXES = [".github/", "tools/", "apps/"];
const INFRA_FILES = new Set(["package.json", "package-lock.json"]);
const REFERENCES_PREFIXES = ["docs/", ".github/", "tools/", "apps/", "data/"];
const REFERENCES_FILES = new Set([
"README.md",
"CONTRIBUTING.md",
"CHANGELOG.md",
"walkthrough.md",
"package.json",
"package-lock.json",
]);
function normalizeRepoPath(filePath) {
return String(filePath || "").replace(/\\/g, "/").replace(/^\.\//, "");
}
function matchesContractEntry(filePath, entry) {
const normalizedPath = normalizeRepoPath(filePath);
const normalizedEntry = normalizeRepoPath(entry);
if (!normalizedEntry) {
return false;
}
if (normalizedEntry.endsWith("/")) {
return normalizedPath.startsWith(normalizedEntry);
}
return normalizedPath === normalizedEntry;
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function loadWorkflowContract(startDir = __dirname) {
const projectRoot = findProjectRoot(startDir);
const configPath = path.join(projectRoot, "tools", "config", "generated-files.json");
const rawConfig = fs.readFileSync(configPath, "utf8");
const config = JSON.parse(rawConfig);
return {
projectRoot,
configPath,
derivedFiles: config.derivedFiles.map(normalizeRepoPath),
mixedFiles: config.mixedFiles.map(normalizeRepoPath),
releaseManagedFiles: config.releaseManagedFiles.map(normalizeRepoPath),
};
}
function getManagedFiles(contract, options = {}) {
const includeMixed = Boolean(options.includeMixed);
const includeReleaseManaged = Boolean(options.includeReleaseManaged);
const managedFiles = [...contract.derivedFiles];
if (includeMixed) {
managedFiles.push(...contract.mixedFiles);
}
if (includeReleaseManaged) {
managedFiles.push(...contract.releaseManagedFiles);
}
return [...new Set(managedFiles.map(normalizeRepoPath))];
}
function isDerivedFile(filePath, contract) {
return contract.derivedFiles.some((entry) => matchesContractEntry(filePath, entry));
}
function isMixedFile(filePath, contract) {
return contract.mixedFiles.some((entry) => matchesContractEntry(filePath, entry));
}
function isDocLikeFile(filePath) {
const normalized = normalizeRepoPath(filePath);
return normalized.endsWith(".md") || DOC_FILES.has(normalized) || DOC_PREFIXES.some((prefix) => normalized.startsWith(prefix));
}
function isInfraLikeFile(filePath) {
const normalized = normalizeRepoPath(filePath);
return (
INFRA_FILES.has(normalized) ||
INFRA_PREFIXES.some((prefix) => normalized.startsWith(prefix))
);
}
function classifyChangedFiles(changedFiles, contract) {
const categories = new Set();
const normalizedFiles = changedFiles.map(normalizeRepoPath).filter(Boolean);
for (const filePath of normalizedFiles) {
if (isDerivedFile(filePath, contract)) {
continue;
}
const isSkillPath = filePath.startsWith("skills/");
if (isSkillPath) {
categories.add("skill");
}
if (!isSkillPath && (isDocLikeFile(filePath) || isMixedFile(filePath, contract))) {
categories.add("docs");
}
if (isInfraLikeFile(filePath)) {
categories.add("infra");
}
}
const orderedCategories = ["skill", "docs", "infra"].filter((category) => categories.has(category));
let primaryCategory = "none";
if (orderedCategories.includes("infra")) {
primaryCategory = "infra";
} else if (orderedCategories.includes("skill")) {
primaryCategory = "skill";
} else if (orderedCategories.includes("docs")) {
primaryCategory = "docs";
}
return {
categories: orderedCategories,
primaryCategory,
};
}
function getDirectDerivedChanges(changedFiles, contract) {
return changedFiles
.map(normalizeRepoPath)
.filter(Boolean)
.filter((filePath) => isDerivedFile(filePath, contract));
}
function requiresReferencesValidation(changedFiles, contract) {
return changedFiles
.map(normalizeRepoPath)
.filter(Boolean)
.some((filePath) => {
if (isDerivedFile(filePath, contract) || isMixedFile(filePath, contract)) {
return true;
}
return (
REFERENCES_FILES.has(filePath) ||
REFERENCES_PREFIXES.some((prefix) => filePath.startsWith(prefix))
);
});
}
function extractChangelogSection(content, version) {
const headingExpression = new RegExp(`^## \\[${escapeRegExp(version)}\\].*$`, "m");
const headingMatch = headingExpression.exec(content);
if (!headingMatch) {
throw new Error(`CHANGELOG.md does not contain a section for version ${version}.`);
}
const startIndex = headingMatch.index;
const remainder = content.slice(startIndex + headingMatch[0].length);
const nextSectionRelativeIndex = remainder.search(/^## \[/m);
const endIndex =
nextSectionRelativeIndex === -1
? content.length
: startIndex + headingMatch[0].length + nextSectionRelativeIndex;
return `${content.slice(startIndex, endIndex).trim()}\n`;
}
function hasQualityChecklist(body) {
return /quality bar checklist/i.test(String(body || ""));
}
function hasIssueLink(body) {
return /(?:closes|fixes)\s+#\d+/i.test(String(body || ""));
}
module.exports = {
classifyChangedFiles,
extractChangelogSection,
getDirectDerivedChanges,
getManagedFiles,
hasIssueLink,
hasQualityChecklist,
isDerivedFile,
isMixedFile,
loadWorkflowContract,
normalizeRepoPath,
matchesContractEntry,
requiresReferencesValidation,
};