mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-04 23:25:51 +00:00
Address CodeRabbit review on #3771: - Treat immediately empty (`---\n---`) and whitespace-only frontmatter blocks as empty metadata instead of throwing or leaking the delimiters into the installed body. - Reject an empty `paths: []` list before serialization so an installed instruction never receives an empty `applyTo:` glob. - Rewrite relative Markdown links that resolve inside the installed rule set to their renamed `.instructions.md` targets; links escaping rules/, external URLs, absolute paths, anchors, and README.md stay unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
126 lines
4.4 KiB
JavaScript
126 lines
4.4 KiB
JavaScript
'use strict';
|
|
|
|
// GitHub Copilot loads path-specific instructions from
|
|
// `.github/instructions/**/*.instructions.md`, where the `applyTo` frontmatter
|
|
// key carries a comma-separated glob list. ECC rules express the same scoping
|
|
// with the Claude Code convention, a YAML `paths:` list, and rules without
|
|
// frontmatter apply to every prompt. The transform maps one model onto the
|
|
// other: `paths:` becomes `applyTo:`, and a rule without `paths` becomes
|
|
// `applyTo: "**"` so it keeps applying globally.
|
|
//
|
|
// Relative Markdown links between rules are renamed to their installed
|
|
// `.instructions.md` names; links that leave the rule set (docs, README,
|
|
// external URLs) are kept as-is.
|
|
//
|
|
// The transform is an allowlist: only `paths` is read, so frontmatter keys
|
|
// added to ECC rules later cannot silently leak into the Copilot copy.
|
|
|
|
const path = require('path');
|
|
|
|
const GLOBAL_APPLY_TO = '**';
|
|
|
|
function splitFrontmatter(source, label) {
|
|
const text = String(source || '');
|
|
|
|
// An immediately empty block (`---\n---\n`) never reaches the YAML loader:
|
|
// the delimiters are stripped and the rule is treated as global.
|
|
const emptyBlock = text.match(/^---\r?\n---\r?\n/);
|
|
if (emptyBlock) {
|
|
return { frontmatter: {}, body: text.slice(emptyBlock[0].length) };
|
|
}
|
|
|
|
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
// A whitespace-only block is empty metadata, not a YAML document.
|
|
if (match[1].trim() === '') {
|
|
return { frontmatter: {}, body: text.slice(match[0].length) };
|
|
}
|
|
|
|
// Keep YAML loading behind the transform boundary. Public help commands load
|
|
// the installer graph without executing a transform, including in hermetic
|
|
// packed-artifact checks where runtime dependencies are intentionally absent.
|
|
const frontmatter = require('js-yaml').load(match[1]);
|
|
if (!frontmatter || typeof frontmatter !== 'object' || Array.isArray(frontmatter)) {
|
|
throw new Error(`Cannot adapt Copilot rule ${label}: frontmatter must be an object`);
|
|
}
|
|
|
|
return {
|
|
frontmatter,
|
|
body: text.slice(match[0].length),
|
|
};
|
|
}
|
|
|
|
function serializeApplyTo(globs) {
|
|
return require('js-yaml')
|
|
.dump({ applyTo: globs }, { lineWidth: -1, noRefs: true })
|
|
.trimEnd();
|
|
}
|
|
|
|
// Markdown links with a compact target (no title), e.g. [text](../a/b.md#x).
|
|
const MARKDOWN_LINK_TARGET = /(\]\()([^)\s]+)(\))/g;
|
|
|
|
// Rewrite relative links that resolve inside the installed rule set to their
|
|
// `.instructions.md` names. Everything else — links escaping `rules/`,
|
|
// absolute or external targets, anchors, and `README.md` (not installed) —
|
|
// is left unchanged.
|
|
function rewriteRuleLinks(body, label) {
|
|
const normalizedLabel = String(label || '').replace(/\\/g, '/');
|
|
if (!/\.md$/i.test(normalizedLabel)) {
|
|
return body;
|
|
}
|
|
const sourceDir = path.posix.dirname(normalizedLabel);
|
|
|
|
return body.replace(MARKDOWN_LINK_TARGET, (link, open, target, close) => {
|
|
const anchorIndex = target.indexOf('#');
|
|
const targetPath = anchorIndex === -1 ? target : target.slice(0, anchorIndex);
|
|
const anchor = anchorIndex === -1 ? '' : target.slice(anchorIndex);
|
|
|
|
if (!/\.md$/i.test(targetPath)) {
|
|
return link;
|
|
}
|
|
if (/^[a-z][a-z0-9+.-]*:/i.test(targetPath) || targetPath.startsWith('/')) {
|
|
return link;
|
|
}
|
|
if (/^readme\.md$/i.test(path.posix.basename(targetPath))) {
|
|
return link;
|
|
}
|
|
|
|
const resolved = path.posix.normalize(path.posix.join(sourceDir, targetPath));
|
|
if (!resolved.startsWith('rules/')) {
|
|
return link;
|
|
}
|
|
|
|
return `${open}${targetPath.replace(/\.md$/i, '.instructions.md')}${anchor}${close}`;
|
|
});
|
|
}
|
|
|
|
function adaptCopilotRule(source, label = '<unknown>') {
|
|
const parsed = splitFrontmatter(source, label);
|
|
|
|
if (!parsed) {
|
|
return `---\napplyTo: "${GLOBAL_APPLY_TO}"\n---\n${rewriteRuleLinks(String(source || ''), label)}`;
|
|
}
|
|
|
|
const { frontmatter } = parsed;
|
|
const body = rewriteRuleLinks(parsed.body, label);
|
|
const paths = frontmatter.paths;
|
|
|
|
if (paths === undefined) {
|
|
return `---\napplyTo: "${GLOBAL_APPLY_TO}"\n---\n${body}`;
|
|
}
|
|
|
|
if (!Array.isArray(paths) || paths.length === 0 || paths.some(glob => typeof glob !== 'string' || glob.trim() === '')) {
|
|
throw new Error(`Cannot adapt Copilot rule ${label}: "paths" must be a non-empty list of glob strings`);
|
|
}
|
|
|
|
return `---\n${serializeApplyTo(paths.map(glob => glob.trim()).join(','))}\n---\n${body}`;
|
|
}
|
|
|
|
module.exports = {
|
|
adaptCopilotRule,
|
|
GLOBAL_APPLY_TO,
|
|
};
|