diff --git a/packages/cli/src/compiler.ts b/packages/cli/src/compiler.ts
index 6458764..c2366ea 100644
--- a/packages/cli/src/compiler.ts
+++ b/packages/cli/src/compiler.ts
@@ -180,14 +180,14 @@ export const AgentFacts: React.FC = () => {
margin: '24px 0',
}}
>
-
${config.entity.name} — Verified Specifications
+ {${JSON.stringify(`${config.entity.name} — Verified Specifications`)}}
- ${primaryOffer?.quotableSummary || ''}
+ {${JSON.stringify(primaryOffer?.quotableSummary || '')}}
Boundaries & Constraints:
- ${boundaries.map((b) => `- ${b}
`).join('\n ')}
+ ${boundaries.map((b) => `- {${JSON.stringify(b)}}
`).join('\n ')}
@@ -211,12 +211,12 @@ const jsonLd = ${JSON.stringify(jsonLdString)};
- ${config.entity.name} — Verified Specifications
- ${primaryOffer?.quotableSummary || ''}
+ {${JSON.stringify(`${config.entity.name} — Verified Specifications`)}}
+ {${JSON.stringify(primaryOffer?.quotableSummary || '')}}
Boundaries & Constraints:
- ${boundaries.map((b) => `- ${b}
`).join('\n ')}
+ ${boundaries.map((b) => `- {${JSON.stringify(b)}}
`).join('\n ')}
Verified as of: ${timestamp.split('T')[0]}
diff --git a/packages/cli/test/cli.test.ts b/packages/cli/test/cli.test.ts
index 0d8234e..88217de 100644
--- a/packages/cli/test/cli.test.ts
+++ b/packages/cli/test/cli.test.ts
@@ -155,6 +155,18 @@ describe('agentlens CLI — Compiler Engine', () => {
expect(llmsTxt).toContain('## Boundaries & Negative Constraints (ncs:v1)');
expect(llmsTxt).toContain('WATERPROOF');
+ // Verify generated React component wraps strings safely in JSX expressions
+ const reactComponentPath = path.join(tmpDir, 'components', 'AgentFacts.tsx');
+ expect(fs.existsSync(reactComponentPath)).toBe(true);
+ const reactContent = fs.readFileSync(reactComponentPath, 'utf8');
+ expect(reactContent).toContain('{"TestCorp — Verified Specifications"}');
+
+ // Verify generated Astro component wraps strings safely
+ const astroComponentPath = path.join(tmpDir, 'components', 'AgentFacts.astro');
+ expect(fs.existsSync(astroComponentPath)).toBe(true);
+ const astroContent = fs.readFileSync(astroComponentPath, 'utf8');
+ expect(astroContent).toContain('{"TestCorp — Verified Specifications"}');
+
// Clean up
fs.rmSync(tmpDir, { recursive: true, force: true });
});
diff --git a/packages/core/src/differ.ts b/packages/core/src/differ.ts
index fed16b2..cf7ebd9 100644
--- a/packages/core/src/differ.ts
+++ b/packages/core/src/differ.ts
@@ -171,12 +171,24 @@ function extractElements(html: string): ElementTagInfo[] {
return results;
}
-function buildCssSelector(el: ElementTagInfo): string {
- if (el.id) return `#${el.id}`;
+/**
+ * Robust CSS selector builder that produces valid CSS selectors for any
+ * class names (including Tailwind classes with slashes or brackets) and numeric IDs.
+ */
+export function buildCssSelector(el: { tag: string; id?: string; className?: string }): string {
+ if (el.id) {
+ if (/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(el.id)) {
+ return `#${el.id}`;
+ }
+ return `[id="${el.id.replace(/["\\]/g, '\\$&')}"]`;
+ }
if (el.className) {
const firstClass = el.className.trim().split(/\s+/)[0];
- if (firstClass && !firstClass.includes(':')) {
- return `${el.tag}.${firstClass}`;
+ if (firstClass) {
+ if (/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(firstClass)) {
+ return `${el.tag}.${firstClass}`;
+ }
+ return `${el.tag}[class~="${firstClass.replace(/["\\]/g, '\\$&')}"]`;
}
}
return el.tag;
diff --git a/packages/core/src/scorer.ts b/packages/core/src/scorer.ts
index 6840356..343f1d3 100644
--- a/packages/core/src/scorer.ts
+++ b/packages/core/src/scorer.ts
@@ -33,8 +33,14 @@ export interface AuditInput {
export function auditAndScore(input: AuditInput): AgentLensReport {
const startTime = Date.now();
- const urlObj = new URL(input.targetUrl);
- const pathname = urlObj.pathname || '/';
+ let pathname = '/';
+ try {
+ const urlObj = new URL(input.targetUrl);
+ // RFC 9309 Section 2.2.2: path comparison matches against path and query components
+ pathname = `${urlObj.pathname || '/'}${urlObj.search || ''}`;
+ } catch {
+ pathname = input.targetUrl.startsWith('/') ? input.targetUrl : `/${input.targetUrl}`;
+ }
const rawHtml = input.rawHtml || '';
const hydratedHtml = input.hydratedHtml || rawHtml;
diff --git a/packages/core/test/core.test.ts b/packages/core/test/core.test.ts
index b39195b..a422493 100644
--- a/packages/core/test/core.test.ts
+++ b/packages/core/test/core.test.ts
@@ -282,4 +282,40 @@ describe('Full 7-Pillar Deterministic Scorer', () => {
expect(report.pillars.renderParity.score).toBe(100);
expect(report.deductions.length).toBeGreaterThanOrEqual(0);
});
+
+ it('preserves query parameters in robots.txt matching per RFC 9309', () => {
+ const robotsTxt = `
+User-agent: *
+Disallow: /*?variant=
+Allow: /
+`;
+ // URL with query parameter should trigger Disallow
+ const reportBlocked = auditAndScore({
+ targetUrl: 'https://garuda.shoes/products?variant=123',
+ rawHtml: 'Products
',
+ robotsTxt,
+ });
+ expect(reportBlocked.pillars.robots.deductions.length).toBeGreaterThan(0);
+
+ // URL without query parameter should be Allowed
+ const reportAllowed = auditAndScore({
+ targetUrl: 'https://garuda.shoes/products',
+ rawHtml: 'Products
',
+ robotsTxt,
+ });
+ expect(reportAllowed.pillars.robots.deductions.length).toBe(0);
+ });
+});
+
+describe('CSS Selector Escaping (differ.ts)', () => {
+ it('generates valid CSS selectors for Tailwind classes and numeric IDs', async () => {
+ const { buildCssSelector } = await import('../src/index.js');
+
+ expect(buildCssSelector({ tag: 'span', className: 'w-1/2' })).toBe('span[class~="w-1/2"]');
+ expect(buildCssSelector({ tag: 'div', className: 'text-[14px]' })).toBe('div[class~="text-[14px]"]');
+ expect(buildCssSelector({ tag: 'button', className: 'p-2.5' })).toBe('button[class~="p-2.5"]');
+ expect(buildCssSelector({ tag: 'div', id: '123-product' })).toBe('[id="123-product"]');
+ expect(buildCssSelector({ tag: 'section', id: 'pricing' })).toBe('#pricing');
+ expect(buildCssSelector({ tag: 'p', className: 'description' })).toBe('p.description');
+ });
});