Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/custom-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ Pick from `.github/ISSUE_TEMPLATE/01-*.md` to `.github/ISSUE_TEMPLATE/26-*.md`.

Current parity note:

- Canonical issue types = 29 (from `.github/issue-types.yml`).
- Canonical issue types = 35 (from `.github/issue-types.yml`).
- Numbered templates = 26.
- Types currently without dedicated templates: `type:chore`, `type:question`, `type:support`.
- Several canonical types intentionally share broader templates.

For these three missing-template types, use the nearest template and explicitly state the target type in the opening section.
When the selected template is broader than the request, use the nearest numbered template and explicitly state the target canonical type in the opening section.

### 3. Fill structured sections completely

Expand Down
6 changes: 3 additions & 3 deletions docs/AUTOMATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ file_type: "documentation"
title: "Automation & Workflows"
description: "Strategy, governance, and workflow documentation for GitHub automation in LightSpeed repositories."
version: "v1.0.2"
last_updated: "2026-06-08"
last_updated: "2026-06-09"
owners: ["LightSpeedWP Team"]
tags: ["automation", "workflows", "governance", "agents"]
status: "active"
Expand Down Expand Up @@ -74,7 +74,7 @@ If your project allows hotfixes directly to `main`, ensure validation workflows
| **changelog-validate.yml** | develop | Enforce changelog requirements and PR labelling standards | changelog validation |
| **planner.yml** | develop | Post merge-readiness checklists and exit criteria to PRs | planner.agent.js |
| **reviewer.yml** | develop | Automated PR review and quality feedback | reviewer.agent.js |
| **project-meta-sync.yml** | develop | Sync project board with PR/issue status | project-meta-sync.agent.js |
| **project-meta-sync.yml** | develop | Sync project board with PR/issue labels, status, priority, type, and supported project fields | project-meta-sync.agent.js |
| **checklist-finalisation.yml** | issues.closed / pull_request_target.closed | Final checklist sync for completed issues and merged PRs | workflow backstop |
| **release.yml** | main | Versioning, changelog generation, tagging, and release notes | release.agent.js |
| **reporting.yml** | develop | Generate metrics and activity reports | reporting.agent.js |
Expand Down Expand Up @@ -147,7 +147,7 @@ The labelling agent enforces canonical label usage:

Issue types are defined once in `.github/issue-types.yml` and used by both:

- **Issue templates:** Pre-populate the `type` field (maps to `type:*` labels)
- **Issue templates:** Provide the canonical template selection and body guidance for the intended `type:*`
- **Labelling agent:** Auto-applies `type:*` labels based on issue type field and content heuristics

**Enforcement:** One type per issue (one-hot principle); issue type field mirrors `type:*` label for consistency.
Expand Down
10 changes: 5 additions & 5 deletions docs/ISSUE_CREATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
title: GitHub Issue Creation Guide
description: How to create well-formed issues, select templates, and trigger automation
file_type: documentation
version: "1.0.1"
version: "1.0.2"
created_date: "2026-05-31"
last_updated: '2026-06-03'
last_updated: '2026-06-09'
author: Claude Code
maintainer: Ash Shaw
owners:
Expand Down Expand Up @@ -56,10 +56,10 @@ This guide helps contributors, team members, and AI agents create high-quality G
### Current Template Parity Note

- Numbered issue templates available: 26 (`01`-`26`)
- Canonical issue types available: 29
- Types currently without dedicated templates: `type:chore`, `type:question`, `type:support`
- Canonical issue types available: 35
- Numbered templates intentionally cover multiple canonical types in a few places.

For these three, use the nearest template and state the intended canonical type in the issue body.
Where a template is broader than the request, use the nearest numbered template and state the intended canonical type in the issue body.

---

Expand Down
19 changes: 19 additions & 0 deletions scripts/agents/includes/__tests__/check-template-labels.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Smoke test for the issue template label validator.
*/
const path = require("path");
const { execFileSync } = require("child_process");

describe("check-template-labels.js", () => {
it("validates the current issue template frontmatter and mappings", () => {
const scriptPath = path.join(__dirname, "../check-template-labels.js");
const output = execFileSync(process.execPath, [scriptPath], {
cwd: path.join(__dirname, "../../../.."),
encoding: "utf8",
});

expect(output).toContain(
"All template frontmatter, labels, and type mappings are valid.",
);
});
});
237 changes: 232 additions & 5 deletions scripts/agents/includes/check-template-labels.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
#!/usr/bin/env node
/**
* check-template-labels.js
* Validates that all labels referenced in issue/PR templates exist in labels.yml
* Validates issue-template frontmatter, canonical labels, and template-to-type mappings.
*/
// TODO: Align this helper with the latest automation spec updates.

import fs from "fs";
import yaml from "js-yaml";
Expand All @@ -27,10 +26,114 @@ const ISSUE_TEMPLATE_DIR = resolveFromRoot(
".github/ISSUE_TEMPLATE",
);

const TEMPLATE_TYPE_MAP = {
"01-task.md": {
primaryType: "type:task",
},
"02-bug.md": {
primaryType: "type:bug",
},
"03-feature.md": {
primaryType: "type:feature",
secondaryTypes: ["type:enhancement"],
},
"04-design.md": {
primaryType: "type:design",
secondaryTypes: ["type:ui", "type:a11y"],
},
"05-epic.md": {
primaryType: "type:epic",
},
"06-story.md": {
primaryType: "type:story",
},
"07-improvement.md": {
primaryType: "type:improve",
secondaryTypes: ["type:enhancement"],
},
"08-user-experience-feedback.md": {
primaryType: "type:ux-feedback",
},
"09-code-refactor.md": {
primaryType: "type:refactor",
secondaryTypes: ["type:maintenance", "type:chore"],
},
"10-build-ci.md": {
primaryType: "type:build",
secondaryTypes: ["type:ci"],
},
"11-automation.md": {
primaryType: "type:automation",
},
"12-testing-coverage.md": {
primaryType: "type:test",
secondaryTypes: ["type:qa"],
},
"13-performance.md": {
primaryType: "type:performance",
},
"14-a11y.md": {
primaryType: "type:a11y",
},
"15-security.md": {
primaryType: "type:security",
},
"16-compatibility.md": {
primaryType: "type:compatibility",
},
"17-integration-issue.md": {
primaryType: "type:integration",
secondaryTypes: ["type:dependency"],
},
"18-release.md": {
primaryType: "type:release",
},
"19-maintenance.md": {
primaryType: "type:maintenance",
secondaryTypes: ["type:chore"],
},
"20-documentation.md": {
primaryType: "type:documentation",
},
"21-research.md": {
primaryType: "type:research",
secondaryTypes: ["type:investigation"],
},
"22-audit.md": {
primaryType: "type:audit",
},
"23-code-review.md": {
primaryType: "type:review",
},
"24-ai-ops.md": {
primaryType: "type:ai-ops",
},
"25-content-modelling.md": {
primaryType: "type:content-modelling",
},
"26-help.md": {
primaryType: "type:help",
secondaryTypes: ["type:question", "type:support"],
},
};

function loadYaml(file) {
return yaml.load(fs.readFileSync(file, "utf8"));
}

function extractFrontmatter(content) {
const match = content.match(/^---\n([\s\S]*?)\n---\n?/);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The regular expression used to extract frontmatter expects Unix-style line endings (\n). If the repository is checked out on Windows with core.autocrlf enabled, the files will have CRLF (\r\n) line endings, causing this match to fail and return null. This will lead to false validation failures. Updating the regex to support optional carriage returns (\r?) ensures cross-platform compatibility.

Suggested change
const match = content.match(/^---\n([\s\S]*?)\n---\n?/);
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);

if (!match) {
return null;
}

try {
return yaml.load(match[1]) || {};
} catch (error) {
return { __error: error.message };
}
}

function getCanonicalLabels() {
const labels = loadYaml(LABELS_FILE);
return new Set(
Expand Down Expand Up @@ -62,7 +165,7 @@ function getIssueTypeLabels() {
function getTemplateLabels() {
const files = fs
.readdirSync(ISSUE_TEMPLATE_DIR)
.filter((f) => f.endsWith(".md"));
.filter((f) => /^\d{2}-.+\.md$/u.test(f));
const labelRegex = /labels?:\s*\[([^\]]+)\]|labels?:\s*([^\n]+)/gi;
const labels = new Set();
for (const file of files) {
Expand All @@ -86,19 +189,143 @@ function getTemplateLabels() {
return labels;
}

function normaliseTemplateLabels(labelsValue) {
if (!labelsValue) {
return [];
}

if (Array.isArray(labelsValue)) {
return labelsValue.flatMap((label) =>
typeof label === "string" ? [label.trim()] : [],
);
}

if (typeof labelsValue === "string") {
return labelsValue
.split(",")
.map((label) => label.replace(/['"\][]]/gu, "").trim())
.filter(Boolean);
}

return [];
}

function validateTemplateFrontmatter(file, frontmatter, canonicalLabels, issueTypeLabels) {
const requiredKeys = [
"file_type",
"name",
"description",
"version",
"last_updated",
"category",
];

if (!frontmatter) {
throw new Error(`Missing YAML frontmatter in ${file}`);
}

if (frontmatter.__error) {
throw new Error(`Invalid YAML frontmatter in ${file}: ${frontmatter.__error}`);
}

for (const key of requiredKeys) {
if (!frontmatter[key]) {
throw new Error(`Missing required frontmatter key "${key}" in ${file}`);
}
}

if (frontmatter.file_type !== "issue-template") {
throw new Error(
`Unexpected file_type in ${file}: expected "issue-template", found "${frontmatter.file_type}"`,
);
}

const declaredLabels = normaliseTemplateLabels(frontmatter.labels);
const declaredTypes = normaliseTemplateLabels(frontmatter.type).filter((label) =>
label.startsWith("type:"),
);

for (const label of declaredLabels) {
if (label && !canonicalLabels.has(label)) {
throw new Error(`Unknown label "${label}" referenced in ${file}`);
}
}

for (const typeLabel of declaredTypes) {
if (!issueTypeLabels.has(typeLabel)) {
throw new Error(`Unknown issue type "${typeLabel}" referenced in ${file}`);
}
}
}

function main() {
const canonical = getCanonicalLabels();
const issueTypeLabels = getIssueTypeLabels();
const templateLabels = getTemplateLabels();
const templateFiles = fs
.readdirSync(ISSUE_TEMPLATE_DIR)
.filter((f) => /^\d{2}-.+\.md$/u.test(f));
const all = new Set([...issueTypeLabels, ...templateLabels]);
const unknown = [...all].filter((l) => l && !canonical.has(l));
if (unknown.length) {
console.error("Unknown labels found in templates or issue-types.yml:");
for (const l of unknown) console.error(` - ${l}`);
process.exit(1);
} else {
console.log("All template and type labels are valid.");
}

const templateMapEntries = Object.entries(TEMPLATE_TYPE_MAP);
const missingMappedFiles = templateFiles.filter(
(file) => !TEMPLATE_TYPE_MAP[file],
);
if (missingMappedFiles.length > 0) {
console.error("Missing template-to-type mapping entries:");
for (const file of missingMappedFiles) console.error(` - ${file}`);
process.exit(1);
}
Comment on lines +277 to +284

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current validation only checks if all existing template files are defined in TEMPLATE_TYPE_MAP. However, it does not verify the reverse: if TEMPLATE_TYPE_MAP contains a file that does not exist in the filesystem, fs.readFileSync on line 293 will throw an unhandled ENOENT error and crash the script. Implementing bidirectional validation ensures the mapping is kept perfectly in sync and prevents unhandled crashes.

  const templateFilesSet = new Set(templateFiles);
  const missingMappedFiles = templateFiles.filter(
    (file) => !TEMPLATE_TYPE_MAP[file],
  );
  const extraMappedFiles = templateMapEntries
    .map(([file]) => file)
    .filter((file) => !templateFilesSet.has(file));

  if (missingMappedFiles.length > 0 || extraMappedFiles.length > 0) {
    if (missingMappedFiles.length > 0) {
      console.error("Missing template-to-type mapping entries:");
      for (const file of missingMappedFiles) console.error(\`  - \${file}\`);
    }
    if (extraMappedFiles.length > 0) {
      console.error("Template-to-type mapping contains non-existent files:");
      for (const file of extraMappedFiles) console.error(\`  - \${file}\`);
    }
    process.exit(1);
  }


const mappedTypes = new Set();
for (const [file, mapping] of templateMapEntries) {
mappedTypes.add(mapping.primaryType);
for (const secondaryType of mapping.secondaryTypes || []) {
mappedTypes.add(secondaryType);
}

const content = fs.readFileSync(path.join(ISSUE_TEMPLATE_DIR, file), "utf8");
const frontmatter = extractFrontmatter(content);
validateTemplateFrontmatter(file, frontmatter, canonical, issueTypeLabels);

const declaredTypes = [
...normaliseTemplateLabels(frontmatter?.type).filter((label) =>
label.startsWith("type:"),
),
...normaliseTemplateLabels(frontmatter?.labels).filter((label) =>
label.startsWith("type:"),
),
];

if (declaredTypes.length > 0) {
const allowedTypes = new Set([
mapping.primaryType,
...(mapping.secondaryTypes || []),
]);

const invalidTypes = declaredTypes.filter((typeLabel) => !allowedTypes.has(typeLabel));
if (invalidTypes.length > 0) {
console.error(`Unexpected template-to-type mapping in ${file}:`);
for (const typeLabel of invalidTypes) console.error(` - ${typeLabel}`);
process.exit(1);
}
}
}

const unknownMappedTypes = [...mappedTypes].filter((typeLabel) => !issueTypeLabels.has(typeLabel));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add a reverse coverage check for canonical types

When a new canonical issue type is added to .github/issue-types.yml without updating TEMPLATE_TYPE_MAP, this validator still passes because it only verifies that mapped types exist in issueTypeLabels; it never checks for issueTypeLabels that are absent from mappedTypes. That leaves the new template-to-type mapping contract unable to catch exactly the drift it is meant to guard against, so future canonical types can be introduced without any broader-template assignment.

Useful? React with 👍 / 👎.

if (unknownMappedTypes.length > 0) {
console.error("Template map references unknown issue types:");
for (const typeLabel of unknownMappedTypes) console.error(` - ${typeLabel}`);
process.exit(1);
}

console.log("All template frontmatter, labels, and type mappings are valid.");
}

main();
Loading