feat(plugins): add source-control extension contracts (#2117)

This commit is contained in:
José Almeida
2026-08-13 08:29:29 +01:00
committed by GitHub
parent 064b852884
commit f218880ecb
530 changed files with 39054 additions and 7518 deletions
+33 -1
View File
@@ -318,6 +318,35 @@ export function findAbsenceClaims(content) {
);
}
/**
* Split prose into sentence-sized claim scopes. API names elsewhere in the
* same wrapped Markdown paragraph must not inherit an unrelated absence claim.
* Dots inside identifiers such as `host.storage` are preserved because a
* boundary requires following whitespace or the end of the paragraph.
*
* @param {string} text Paragraph text.
* @returns {string[]} Sentence-sized fragments.
*/
function splitClaimScopes(text) {
return text.split(/[.!?](?:\s+|$)/).filter(Boolean);
}
/**
* API claims in harness Markdown name contracts as inline code. Restricting
* matching to those spans prevents common host words such as `context` from
* being attributed to unrelated phrases like "thread context is unavailable."
*
* @param {string} scope Sentence-sized absence-claim scope.
* @param {RegExp} boundary Escaped live-name matcher.
* @returns {boolean} Whether an inline-code span names the live API.
*/
function scopeMentionsLiveAPI(scope, boundary) {
for (const match of scope.matchAll(/`([^`]+)`/g)) {
if (boundary.test(match[1])) return true;
}
return false;
}
/**
* Resolve the fixed set of harness files this guard scans, de-duplicated by
* physical (symlink-resolved) path.
@@ -423,11 +452,14 @@ export async function validatePluginApiClaims({
const relFile = path.relative(root, file);
for (const claim of claims) {
const claimScopes = splitClaimScopes(claim.text).filter((scope) =>
CLAIM_PATTERNS.some((pattern) => pattern.test(scope)),
);
for (const name of liveNames) {
const boundary = new RegExp(
`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
);
if (boundary.test(claim.text)) {
if (claimScopes.some((scope) => scopeMentionsLiveAPI(scope, boundary))) {
findings.push({ file: relFile, line: claim.line, api: name });
}
}
@@ -174,6 +174,43 @@ for the full contract.
assert.deepEqual(findings, []);
});
test("validatePluginApiClaims keeps unrelated absence prose sentence-scoped", async () => {
const dir = await createFixture({
"types.ts": FIXTURE_TYPES_TS,
"AGENTS.md": `# Frontend
The Host can be unavailable during construction, so resolve it when handling
work. Honor context cancellation and use host.storage for user settings.
`,
});
const { findings } = await validatePluginApiClaims({
repoRoot: dir,
typesPath: path.join(dir, "types.ts"),
scanFiles: [path.join(dir, "AGENTS.md")],
});
assert.deepEqual(findings, []);
});
test("validatePluginApiClaims ignores ordinary words that share an API name", async () => {
const dir = await createFixture({
"types.ts": FIXTURE_TYPES_TS,
"AGENTS.md": `# Reviews
Expand the review before deciding whether its thread context is unavailable.
`,
});
const { findings } = await validatePluginApiClaims({
repoRoot: dir,
typesPath: path.join(dir, "types.ts"),
scanFiles: [path.join(dir, "AGENTS.md")],
});
assert.deepEqual(findings, []);
});
test("validatePluginApiClaims exits clean against the real kandev tree", async () => {
const repoRoot = path.resolve(import.meta.dirname, "..");
const { findings } = await validatePluginApiClaims({ repoRoot });
+11 -14
View File
@@ -298,14 +298,16 @@ function assertFeatureClipShape(clip) {
"filenames",
Boolean(
clip.filenames &&
typeof clip.filenames === "object" &&
!Array.isArray(clip.filenames),
typeof clip.filenames === "object" &&
!Array.isArray(clip.filenames),
),
],
[
"files",
Boolean(
clip.files && typeof clip.files === "object" && !Array.isArray(clip.files),
clip.files &&
typeof clip.files === "object" &&
!Array.isArray(clip.files),
),
],
];
@@ -335,10 +337,7 @@ function hasCompleteFeatureMediaEmbed(markdown, filenames) {
...match[0].matchAll(
/\b(webm|mp4|poster)\s*=\s*(?:"([^"]*)"|'([^']*)')/g,
),
].map((attribute) => [
attribute[1],
attribute[2] ?? attribute[3],
]),
].map((attribute) => [attribute[1], attribute[2] ?? attribute[3]]),
);
return Object.entries(filenames).every(
@@ -738,7 +737,9 @@ async function collectFilesWithExtension(dir, extension) {
function assertCompleteSurfaceCoverage(shipped, covered, excluded, label) {
for (const value of covered) {
if (excluded.has(value)) {
throw new Error(`coverage.json both covers and excludes ${label}: ${value}`);
throw new Error(
`coverage.json both covers and excludes ${label}: ${value}`,
);
}
}
for (const value of shipped) {
@@ -871,9 +872,7 @@ function assertExperimentalCalloutPlacement(file, markdown) {
const heading = headings[headingIndex];
if (
!heading ||
source
.slice(heading.index + heading[0].length, callout.index)
.trim()
source.slice(heading.index + heading[0].length, callout.index).trim()
) {
throw new Error(
`${file} experimental callouts must immediately follow a descriptive heading`,
@@ -888,9 +887,7 @@ function assertExperimentalCalloutPlacement(file, markdown) {
const sectionEnd = nextHeading?.index ?? source.length;
const calloutBlock = source
.slice(callout.index)
.match(
/^>\s*\[!EXPERIMENTAL\]\s*\r?\n(?:^>.*(?:\r?\n|$))*/im,
)?.[0];
.match(/^>\s*\[!EXPERIMENTAL\]\s*\r?\n(?:^>.*(?:\r?\n|$))*/im)?.[0];
const sectionContent = source
.slice(
callout.index + (calloutBlock?.length ?? callout[0].length),
+31 -1
View File
@@ -54,7 +54,9 @@ async function createCoverageRepo({
"docs/public/index.md": validPage,
"apps/web/src/settings-routes.tsx": `const SETTINGS_ROUTES = {\n${settingsRoutes
.map((route) => ` "${route}": () => null,`)
.join("\n")}\n};\n\nexport const SETTINGS_ROUTE_PATHS = new Set(Object.keys(SETTINGS_ROUTES));\n\nexport function SettingsRoutes() {}`,
.join(
"\n",
)}\n};\n\nexport const SETTINGS_ROUTE_PATHS = new Set(Object.keys(SETTINGS_ROUTES));\n\nexport function SettingsRoutes() {}`,
"apps/web/src/settings-routes.test.ts": "// settings route coverage",
"apps/backend/internal/mcp/server/server.go": mcpTools
.map((tool) => `mcp.NewTool("${tool}")`)
@@ -214,6 +216,34 @@ test("accepts explicitly ordered pages with required frontmatter", async () => {
await assert.doesNotReject(validatePublicDocs(dir));
});
test("plugin authoring reference documents authenticated actions and live host writes", async () => {
const [authoring, manifest, integrations] = await Promise.all([
fs.readFile(
path.join(process.cwd(), "docs/public/plugins-authoring.md"),
"utf8",
),
fs.readFile(
path.join(process.cwd(), "docs/public/plugins-manifest.md"),
"utf8",
),
fs.readFile(
path.join(process.cwd(), "docs/public/integrations.md"),
"utf8",
),
]);
assert.match(authoring, /Authenticated declared actions/);
assert.match(authoring, /workspaceId/);
assert.match(authoring, /AbortSignal/);
assert.match(authoring, /CreateTask/);
assert.match(manifest, /\bscope\b/);
assert.match(manifest, /repository_providers/);
assert.match(manifest, /reference_sources/);
assert.match(manifest, /webhooks\[\]\.access/);
assert.match(manifest, /webhooks\[\]\.max_body_bytes/);
assert.match(integrations, /Bitbucket/);
});
test("rejects public pages that contain an em dash", async () => {
const dir = await createDocs(
{ "index.md": validPage.replace("Page body.", "Public — copy.") },