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
1 change: 0 additions & 1 deletion .cursor/rules/110-java-maven-best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ Before applying Maven best practices recommendations, ensure the project is in a

- **MANDATORY**: Run `./mvnw validate` or `mvn validate` before applying any Maven best practices recommendations
- **VERIFY**: Ensure all validation errors are resolved before proceeding with POM modifications
- **PREREQUISITE**: Project must compile and pass basic validation checks before optimization
- **SAFETY**: If validation fails, not continue and ask the user to fix the issues before continuing
- **MULTI-MODULE DISCOVERY**: After reading the root `pom.xml`, check whether it contains a `<modules>` section. If it does, read every child module's `pom.xml` before making any recommendations — analysis scope is the full module tree, not only the root
- **CROSS-MODULE SCOPE**: When child modules exist, check each one for: hardcoded dependency versions that duplicate `<dependencyManagement>` in the parent, plugin configurations that duplicate `<pluginManagement>`, properties that should be centralized in the parent, and version drift (same artifact declared at different versions across sibling modules)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package info.jab.pml;

import java.io.InputStream;
import java.io.StringWriter;
import java.util.Optional;
import java.util.stream.Stream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
Expand Down Expand Up @@ -36,7 +43,7 @@ public SkillsGenerator() {
*/
public Stream<SkillOutput> generateAllSkills() {
return SkillsInventory.skillDescriptors()
.map(d -> generateSkill(d.skillId(), d.requiresSystemPrompt()));
.map(d -> generateSkill(d.skillId(), d.requiresSystemPrompt(), d.useXml()));
}

/**
Expand All @@ -47,7 +54,7 @@ public Stream<SkillOutput> generateAllSkills() {
* @throws RuntimeException if resources cannot be loaded or generation fails
*/
public SkillOutput generateSkill(String skillId) {
return generateSkill(skillId, true);
return generateSkill(skillId, true, false);
}

/**
Expand All @@ -58,10 +65,24 @@ public SkillOutput generateSkill(String skillId) {
* @return the generated skill output
*/
public SkillOutput generateSkill(String skillId, boolean requiresSystemPrompt) {
return generateSkill(skillId, requiresSystemPrompt, false);
}

/**
* Generates SKILL.md and reference content for a given skill.
*
* @param skillId the skill identifier (e.g. 110-java-maven-best-practices)
* @param requiresSystemPrompt when false, skips system-prompt XML and reference generation
* @param useXml when true, loads skill from skills/{numericId}-skill.xml, validates against schema, transforms via XSLT
* @return the generated skill output
*/
public SkillOutput generateSkill(String skillId, boolean requiresSystemPrompt, boolean useXml) {
String referenceContent = requiresSystemPrompt
? generateReferenceContent(skillId, parseMetadata(skillId))
: "";
String skillMdContent = loadSkillSummary(skillId);
String skillMdContent = useXml
? loadSkillSummaryFromXml(skillId)
: loadSkillSummary(skillId);
return new SkillOutput(skillId, skillMdContent, referenceContent);
}

Expand Down Expand Up @@ -146,6 +167,49 @@ private String loadSkillSummary(String skillId) {
}
}

private String loadSkillSummaryFromXml(String skillId) {
String numericId = extractNumericId(skillId);
String xmlResource = "skills/" + numericId + "-skill.xml";
String xsltResource = "schemas/skill-to-markdown.xslt";
String schemaResource = "schemas/skill.xsd";
try (
InputStream xmlStream = getResource(xmlResource);
InputStream xsltStream = getResource(xsltResource);
InputStream schemaStream = getResource(schemaResource)
) {
if (xmlStream == null) {
throw new RuntimeException("Skill XML not found: " + xmlResource);
}
if (xsltStream == null) {
throw new RuntimeException("XSLT not found: " + xsltResource);
}
if (schemaStream == null) {
throw new RuntimeException("Schema not found: " + schemaResource);
}
SchemaFactory factory = SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schema = factory.newSchema(new StreamSource(schemaStream));
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
docFactory.setSchema(schema);
docFactory.setNamespaceAware(true);
DocumentBuilder builder = docFactory.newDocumentBuilder();
builder.parse(xmlStream);

TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer(new StreamSource(xsltStream));
StringWriter writer = new StringWriter();
try (InputStream xmlStreamForTransform = getResource(xmlResource)) {
if (xmlStreamForTransform == null) {
throw new RuntimeException("Skill XML not found: " + xmlResource);
}
transformer.transform(new StreamSource(xmlStreamForTransform), new StreamResult(writer));
}
String content = writer.toString();
return appendProjectTagToDescription(content);
} catch (Exception e) {
throw new RuntimeException("Failed to load and transform skill XML: " + xmlResource, e);
}
}

private String appendProjectTagToDescription(String content) {
boolean hasLicense = content.contains("license:");
return content.lines()
Expand Down
43 changes: 32 additions & 11 deletions skills-generator/src/main/java/info/jab/pml/SkillsInventory.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static Stream<String> skillIds() {

for (InventoryEntry entry : entries) {
String numericId = entry.numericId();
validateSkillSummaryExists(numericId);
validateSkillSummaryExists(numericId, entry.useXml());
String skillId = entry.requiresSystemPrompt()
? resolveSkillIdFromPrefix(Integer.parseInt(numericId))
: entry.skillId();
Expand All @@ -57,26 +57,26 @@ public static Stream<String> skillIds() {
}

/**
* Returns skill descriptors (skillId + requiresSystemPrompt) for generator use.
* Returns skill descriptors (skillId + requiresSystemPrompt + useXml) for generator use.
*/
public static Stream<SkillDescriptor> skillDescriptors() {
List<InventoryEntry> entries = loadInventory();
List<SkillDescriptor> descriptors = new ArrayList<>();
for (InventoryEntry entry : entries) {
String numericId = entry.numericId();
validateSkillSummaryExists(numericId);
validateSkillSummaryExists(numericId, entry.useXml());
String skillId = entry.requiresSystemPrompt()
? resolveSkillIdFromPrefix(Integer.parseInt(numericId))
: entry.skillId();
descriptors.add(new SkillDescriptor(skillId, entry.requiresSystemPrompt()));
descriptors.add(new SkillDescriptor(skillId, entry.requiresSystemPrompt(), entry.useXml()));
}
return descriptors.stream();
}

/**
* Skill ID and whether it requires a system prompt for reference generation.
* Skill ID, whether it requires a system prompt for reference generation, and whether to use XML source.
*/
public record SkillDescriptor(String skillId, boolean requiresSystemPrompt) {}
public record SkillDescriptor(String skillId, boolean requiresSystemPrompt, boolean useXml) {}

/**
* Resolves skillId by finding the system-prompt XML that starts with {@code {id}-}.
Expand Down Expand Up @@ -186,20 +186,39 @@ private static List<InventoryEntry> parseInventory(String json) {
throw new RuntimeException("Entry with id " + numericId
+ " has requiresSystemPrompt=false but no skillId specified.");
}
entries.add(new InventoryEntry(numericId, requiresSystemPrompt, skillId));
boolean useXml = parseXmlFlag(node);
entries.add(new InventoryEntry(numericId, requiresSystemPrompt, skillId, useXml));
}
return entries;
} catch (Exception e) {
throw new RuntimeException("Failed to parse skill inventory", e);
}
}

private static void validateSkillSummaryExists(String numericId) {
String resourceName = "skills/" + numericId + "-skill.md";
private static boolean parseXmlFlag(JsonNode node) {
if (!node.has("xml")) {
return false;
}
JsonNode xmlNode = node.get("xml");
if (xmlNode.isBoolean()) {
return xmlNode.asBoolean();
}
if (xmlNode.isTextual()) {
String s = xmlNode.asText().toLowerCase();
return "true".equals(s) || "yes".equals(s) || "1".equals(s);
}
return false;
}

private static void validateSkillSummaryExists(String numericId, boolean useXml) {
String resourceName = useXml
? "skills/" + numericId + "-skill.xml"
: "skills/" + numericId + "-skill.md";
try (InputStream stream = getResource(resourceName)) {
if (stream == null) {
throw new RuntimeException("Skill summary not found: " + resourceName
+ ". Add skills/" + numericId + "-skill.md for each skill in the inventory.");
+ ". Add skills/" + numericId + (useXml ? "-skill.xml" : "-skill.md")
+ " for each skill in the inventory.");
}
} catch (Exception e) {
if (e instanceof RuntimeException re) {
Expand All @@ -225,6 +244,8 @@ private static InputStream getResource(String name) {
* Single entry from skill-inventory.json. When requiresSystemPrompt is true,
* skillId is derived by matching system-prompts with prefix {@code {numericId}-}.
* When false, skillId must be provided and no system-prompt is required.
* When useXml is true, skill summary is loaded from skills/{numericId}-skill.xml
* and transformed via schema validation and XSLT; otherwise from skills/{numericId}-skill.md.
*/
public record InventoryEntry(String numericId, boolean requiresSystemPrompt, String skillId) {}
public record InventoryEntry(String numericId, boolean requiresSystemPrompt, String skillId, boolean useXml) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs">

<xsl:output method="text" encoding="UTF-8" indent="no"/>

<xsl:template match="/skill">
<xsl:text>---
name: </xsl:text>
<xsl:value-of select="@id"/>
<xsl:text>
description: </xsl:text>
<xsl:value-of select="normalize-space(description)"/>
<xsl:text>
license: </xsl:text>
<xsl:value-of select="metadata/license"/>
<xsl:text>
metadata:
author: </xsl:text>
<xsl:value-of select="metadata/author"/>
<xsl:text>
version: </xsl:text>
<xsl:value-of select="metadata/version"/>
<xsl:text>
---
# </xsl:text>
<xsl:value-of select="normalize-space(title)"/>
<xsl:text>

</xsl:text>
<xsl:apply-templates select="goal"/>
<xsl:apply-templates select="constraints"/>
<xsl:apply-templates select="reference"/>
</xsl:template>

<!-- Preserve line breaks: do not use normalize-space() which collapses newlines -->
<xsl:template match="goal">
<xsl:value-of select="."/>
<xsl:text>

</xsl:text>
</xsl:template>

<!-- Constraints: matches .cursor/rules rendering (## Constraints, list items with - ) -->
<xsl:template match="constraints">
<xsl:text>## Constraints

</xsl:text>
<xsl:if test="constraints-description">
<xsl:value-of select="normalize-space(constraints-description)"/>
<xsl:text>

</xsl:text>
</xsl:if>
<xsl:for-each select="constraint-list/constraint">
<xsl:text>- </xsl:text>
<xsl:value-of select="normalize-space(.)"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>

<xsl:template match="reference">
<xsl:if test="@path">
<xsl:text>## Reference

For detailed guidance, examples, and constraints, see [</xsl:text>
<xsl:value-of select="@path"/>
<xsl:text>](</xsl:text>
<xsl:value-of select="@path"/>
<xsl:text>).
</xsl:text>
</xsl:if>
</xsl:template>

</xsl:stylesheet>
89 changes: 89 additions & 0 deletions skills-generator/src/main/resources/schemas/skill.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
attributeFormDefault="unqualified">

<!-- Root: skill definition (aligns with SKILL.md frontmatter and system prompt structure) -->
<xs:element name="skill">
<xs:complexType>
<xs:sequence>
<xs:element ref="metadata"/>
<xs:element ref="title"/>
<xs:element ref="description" minOccurs="0"/>
<xs:element ref="goal"/>
<xs:element ref="constraints" minOccurs="0"/>
<xs:element ref="scope" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="reference" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="interactive" type="xs:boolean" use="optional" default="false"/>
</xs:complexType>
</xs:element>

<!-- Metadata (maps to frontmatter) -->
<xs:element name="metadata">
<xs:complexType>
<xs:sequence>
<xs:element name="author" type="xs:string" minOccurs="0"/>
<xs:element name="version" type="xs:string" minOccurs="0"/>
<xs:element name="license" type="xs:string" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
</xs:element>

<!-- Title: H1 heading -->
<xs:element name="title" type="xs:string"/>

<!-- Description: "Use when..." trigger text for AI skill matching -->
<xs:element name="description" type="xs:string"/>

<!-- Goal: short description of what the skill achieves -->
<xs:element name="goal" type="xs:string"/>

<!-- Constraints (aligns with system prompt constraints structure) -->
<xs:element name="constraints">
<xs:complexType>
<xs:sequence>
<xs:element name="constraints-description" type="xs:string" minOccurs="0"/>
<xs:element name="constraint-list">
<xs:complexType>
<xs:sequence>
<xs:element name="constraint" type="xs:string" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>

<!-- Scope block(s) -->
<xs:element name="scope">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="type" use="optional">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="multi-step"/>
<xs:enumeration value="reference-based"/>
<xs:enumeration value="before-asking"/>
<xs:enumeration value="before-applying"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>

<!-- Reference: link to detailed reference doc -->
<xs:element name="reference">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="path" type="xs:string" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:schema>
2 changes: 1 addition & 1 deletion skills-generator/src/main/resources/skill-inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{"id": "021", "requiresSystemPrompt": false, "skillId": "021-architecture-functional-requirements-rest"},
{"id": "030", "requiresSystemPrompt": false, "skillId": "030-architecture-non-functional-requirements"},
{"id": "040", "requiresSystemPrompt": false, "skillId": "040-planning-enhance-ai-plan-mode"},
{"id": 110},
{"id": 110, "xml": true},
{"id": 111},
{"id": 112},
{"id": 113},
Expand Down
Loading
Loading