aeoptimize 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +5 -5
- package/.claude-plugin/plugin.json +4 -4
- package/CHANGELOG.md +27 -0
- package/CONTRIBUTING.md +37 -0
- package/README.md +88 -119
- package/ROADMAP.md +31 -0
- package/SECURITY.md +15 -0
- package/agents/aeo-ai-scorer.md +10 -39
- package/agents/aeo-analyzer.md +9 -55
- package/dist/cli/index.js +19 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/core/ai-prompt.js +8 -6
- package/dist/core/ai-prompt.js.map +1 -1
- package/dist/core/generator.js +17 -47
- package/dist/core/generator.js.map +1 -1
- package/dist/core/merger.js +2 -2
- package/dist/core/merger.js.map +1 -1
- package/dist/core/rules.js +92 -154
- package/dist/core/rules.js.map +1 -1
- package/dist/core/scanner.js +2 -20
- package/dist/core/scanner.js.map +1 -1
- package/dist/plugins/next.js +2 -2
- package/dist/plugins/next.js.map +1 -1
- package/dist/plugins/vite.js +2 -2
- package/dist/plugins/vite.js.map +1 -1
- package/docs/methodology.md +78 -0
- package/docs/release-v0.6.md +72 -0
- package/examples/github-action-sample/.github/workflows/aeoptimize.yml +18 -0
- package/examples/github-action-sample/README.md +9 -0
- package/examples/github-action-sample/site/index.html +38 -0
- package/fixtures/v0.6/rule-corpus.ts +314 -0
- package/package.json +26 -12
- package/scripts/verify-release-v0.6.sh +172 -0
- package/skills/aeo-generate/SKILL.md +21 -39
- package/skills/aeo-scan/SKILL.md +17 -64
- package/skills/aeo-transform/SKILL.md +20 -72
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
export type RuleFixtureKind = 'positive' | 'negative' | 'boundary';
|
|
2
|
+
|
|
3
|
+
export interface FixtureDocument {
|
|
4
|
+
url: string;
|
|
5
|
+
title: string;
|
|
6
|
+
html: string;
|
|
7
|
+
markdown: string;
|
|
8
|
+
headings: Array<{ level: number; text: string }>;
|
|
9
|
+
paragraphs: string[];
|
|
10
|
+
jsonLd: Array<{ '@type'?: string; '@context'?: string; [key: string]: unknown }>;
|
|
11
|
+
metaTags: Record<string, string>;
|
|
12
|
+
links: Array<{ href: string; text: string; rel?: string }>;
|
|
13
|
+
rawText: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RuleFixtureCase {
|
|
17
|
+
purpose: string;
|
|
18
|
+
document: Partial<FixtureDocument>;
|
|
19
|
+
expected: {
|
|
20
|
+
score: number;
|
|
21
|
+
issues: number;
|
|
22
|
+
suggestions: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RuleFixtureSet {
|
|
27
|
+
positive: RuleFixtureCase;
|
|
28
|
+
negative: RuleFixtureCase;
|
|
29
|
+
boundary: RuleFixtureCase;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const words = (count: number, prefix = 'word') =>
|
|
33
|
+
Array.from({ length: count }, (_, index) => `${prefix}${index}`).join(' ');
|
|
34
|
+
|
|
35
|
+
const repeatedWords = (count: number, word = 'word') =>
|
|
36
|
+
Array.from({ length: count }, () => word).join(' ');
|
|
37
|
+
|
|
38
|
+
const stuffedText = (
|
|
39
|
+
'Buy cheap widgets now. Cheap widgets are the best widgets. ' +
|
|
40
|
+
'Our widgets are cheap widgets for sale. Get cheap widgets today. ' +
|
|
41
|
+
'Cheap widgets online cheap widgets store cheap widgets deals. ' +
|
|
42
|
+
'Best cheap widgets cheap widgets review cheap widgets comparison. ' +
|
|
43
|
+
'Order cheap widgets cheap widgets shipping cheap widgets discount. '
|
|
44
|
+
).repeat(3);
|
|
45
|
+
|
|
46
|
+
export const ruleFixtureCorpusVersion = '0.6.0';
|
|
47
|
+
|
|
48
|
+
export const ruleFixtureCorpus: Record<string, RuleFixtureSet> = {
|
|
49
|
+
'heading-hierarchy': {
|
|
50
|
+
positive: {
|
|
51
|
+
purpose: 'A descriptive H1 followed by nested sections receives the full structure score.',
|
|
52
|
+
document: {
|
|
53
|
+
headings: [
|
|
54
|
+
{ level: 1, text: 'Release guide' },
|
|
55
|
+
{ level: 2, text: 'Verification' },
|
|
56
|
+
{ level: 3, text: 'CLI checks' },
|
|
57
|
+
],
|
|
58
|
+
rawText: 'A short release guide with a clear outline.',
|
|
59
|
+
},
|
|
60
|
+
expected: { score: 10, issues: 0, suggestions: 0 },
|
|
61
|
+
},
|
|
62
|
+
negative: {
|
|
63
|
+
purpose: 'A document with no headings triggers the deterministic missing-outline finding.',
|
|
64
|
+
document: { headings: [], rawText: 'Unstructured content.' },
|
|
65
|
+
expected: { score: 0, issues: 1, suggestions: 0 },
|
|
66
|
+
},
|
|
67
|
+
boundary: {
|
|
68
|
+
purpose: 'Multiple H1 elements are not treated as an automatic error when the outline does not skip levels.',
|
|
69
|
+
document: {
|
|
70
|
+
headings: [
|
|
71
|
+
{ level: 1, text: 'Primary title' },
|
|
72
|
+
{ level: 1, text: 'Secondary region title' },
|
|
73
|
+
{ level: 2, text: 'Details' },
|
|
74
|
+
],
|
|
75
|
+
rawText: 'Readable content.',
|
|
76
|
+
},
|
|
77
|
+
expected: { score: 10, issues: 0, suggestions: 0 },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
'paragraph-length': {
|
|
81
|
+
positive: {
|
|
82
|
+
purpose: 'Short focused paragraphs remain below the configured readability heuristic.',
|
|
83
|
+
document: { paragraphs: ['A concise paragraph.', 'Another concise paragraph.'] },
|
|
84
|
+
expected: { score: 8, issues: 0, suggestions: 0 },
|
|
85
|
+
},
|
|
86
|
+
negative: {
|
|
87
|
+
purpose: 'A majority of paragraphs above 150 words triggers the configured long-paragraph finding.',
|
|
88
|
+
document: { paragraphs: [repeatedWords(151), repeatedWords(151), 'A short paragraph.'] },
|
|
89
|
+
expected: { score: 3, issues: 1, suggestions: 1 },
|
|
90
|
+
},
|
|
91
|
+
boundary: {
|
|
92
|
+
purpose: 'Exactly 150 words is the non-penalized threshold boundary.',
|
|
93
|
+
document: { paragraphs: [repeatedWords(150)] },
|
|
94
|
+
expected: { score: 8, issues: 0, suggestions: 0 },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
'list-usage': {
|
|
98
|
+
positive: {
|
|
99
|
+
purpose: 'Long content containing a genuine list receives the full scannability score.',
|
|
100
|
+
document: { html: `<ul><li>First</li><li>Second</li></ul>${words(301)}`, rawText: words(301) },
|
|
101
|
+
expected: { score: 7, issues: 0, suggestions: 0 },
|
|
102
|
+
},
|
|
103
|
+
negative: {
|
|
104
|
+
purpose: 'Long content with no list receives a low-impact review suggestion.',
|
|
105
|
+
document: { html: `<p>${words(301)}</p>`, rawText: words(301) },
|
|
106
|
+
expected: { score: 4, issues: 0, suggestions: 1 },
|
|
107
|
+
},
|
|
108
|
+
boundary: {
|
|
109
|
+
purpose: 'Content at exactly 300 words is not forced into a list merely to satisfy the heuristic.',
|
|
110
|
+
document: { html: `<p>${words(300)}</p>`, rawText: words(300) },
|
|
111
|
+
expected: { score: 7, issues: 0, suggestions: 0 },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
'self-contained-statements': {
|
|
115
|
+
positive: {
|
|
116
|
+
purpose: 'Paragraphs that name their subject remain independently understandable.',
|
|
117
|
+
document: {
|
|
118
|
+
paragraphs: [
|
|
119
|
+
'Aeoptimize reports deterministic content-readiness findings.',
|
|
120
|
+
'The GitHub Action is advisory by default.',
|
|
121
|
+
'Project owners choose whether a threshold should block CI.',
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
expected: { score: 8, issues: 0, suggestions: 0 },
|
|
125
|
+
},
|
|
126
|
+
negative: {
|
|
127
|
+
purpose: 'A majority of dangling pronoun or transition openings triggers a review finding.',
|
|
128
|
+
document: {
|
|
129
|
+
paragraphs: ['This is important.', 'They require context.', 'However, it varies.', 'The release is versioned.', 'The report is public.'],
|
|
130
|
+
},
|
|
131
|
+
expected: { score: 3, issues: 1, suggestions: 1 },
|
|
132
|
+
},
|
|
133
|
+
boundary: {
|
|
134
|
+
purpose: 'Exactly twenty percent dangling openings is the non-penalized ratio boundary.',
|
|
135
|
+
document: {
|
|
136
|
+
paragraphs: ['This needs context.', 'The package is versioned.', 'The Action is advisory.', 'The report is stable.', 'The fixture is public.'],
|
|
137
|
+
},
|
|
138
|
+
expected: { score: 8, issues: 0, suggestions: 1 },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
'data-stats-presence': {
|
|
142
|
+
positive: {
|
|
143
|
+
purpose: 'A quantitative claim with explicit source language is not flagged as unsourced.',
|
|
144
|
+
document: { rawText: 'According to the linked release report, 20 users completed the test.' },
|
|
145
|
+
expected: { score: 7, issues: 0, suggestions: 0 },
|
|
146
|
+
},
|
|
147
|
+
negative: {
|
|
148
|
+
purpose: 'A quantitative claim without a detectable source receives an evidence warning.',
|
|
149
|
+
document: { rawText: 'The package serves 20 users.' },
|
|
150
|
+
expected: { score: 3, issues: 1, suggestions: 1 },
|
|
151
|
+
},
|
|
152
|
+
boundary: {
|
|
153
|
+
purpose: 'Content without quantitative claims is not penalized or encouraged to invent numbers.',
|
|
154
|
+
document: { rawText: 'The package exposes a deterministic local lint.' },
|
|
155
|
+
expected: { score: 7, issues: 0, suggestions: 0 },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
'clear-definitions': {
|
|
159
|
+
positive: {
|
|
160
|
+
purpose: 'Several explicit definitions receive the full clarity score.',
|
|
161
|
+
document: { rawText: 'A lint is a repeatable check. A fixture means a controlled input. A release refers to a published version.' },
|
|
162
|
+
expected: { score: 5, issues: 0, suggestions: 0 },
|
|
163
|
+
},
|
|
164
|
+
negative: {
|
|
165
|
+
purpose: 'Content without definitions receives a clarity suggestion.',
|
|
166
|
+
document: { rawText: 'Install the package and run the command.' },
|
|
167
|
+
expected: { score: 1, issues: 0, suggestions: 1 },
|
|
168
|
+
},
|
|
169
|
+
boundary: {
|
|
170
|
+
purpose: 'A semantic definition list is accepted without requiring a prose pattern.',
|
|
171
|
+
document: { html: '<dl><dt>Fixture</dt><dd>A controlled input.</dd></dl>', rawText: 'Fixture: a controlled input.' },
|
|
172
|
+
expected: { score: 5, issues: 0, suggestions: 0 },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
attribution: {
|
|
176
|
+
positive: {
|
|
177
|
+
purpose: 'Accurate author, date, and source language receive the full attribution score.',
|
|
178
|
+
document: {
|
|
179
|
+
metaTags: { author: 'Fixture Author', date: '2026-08-22' },
|
|
180
|
+
rawText: 'According to the release evidence, the focused checks passed.',
|
|
181
|
+
},
|
|
182
|
+
expected: { score: 5, issues: 0, suggestions: 0 },
|
|
183
|
+
},
|
|
184
|
+
negative: {
|
|
185
|
+
purpose: 'Authored or time-sensitive content with no attribution signals receives a suggestion.',
|
|
186
|
+
document: { metaTags: {}, rawText: 'A time-sensitive release note.' },
|
|
187
|
+
expected: { score: 0, issues: 0, suggestions: 1 },
|
|
188
|
+
},
|
|
189
|
+
boundary: {
|
|
190
|
+
purpose: 'Author plus source language reaches the no-suggestion threshold without inventing a date.',
|
|
191
|
+
document: { metaTags: { author: 'Fixture Author' }, rawText: 'Source: local release verification.' },
|
|
192
|
+
expected: { score: 3, issues: 0, suggestions: 0 },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
'json-ld-presence': {
|
|
196
|
+
positive: {
|
|
197
|
+
purpose: 'Present JSON-LD is detected without awarding extra points for schema count.',
|
|
198
|
+
document: { jsonLd: [{ '@context': 'https://schema.org', '@type': 'SoftwareApplication' }] },
|
|
199
|
+
expected: { score: 8, issues: 0, suggestions: 0 },
|
|
200
|
+
},
|
|
201
|
+
negative: {
|
|
202
|
+
purpose: 'Missing JSON-LD produces an informational finding but no score penalty because schema is optional.',
|
|
203
|
+
document: { jsonLd: [] },
|
|
204
|
+
expected: { score: 8, issues: 1, suggestions: 0 },
|
|
205
|
+
},
|
|
206
|
+
boundary: {
|
|
207
|
+
purpose: 'Presence and completeness are separate rules, preventing a duplicate penalty in the presence rule.',
|
|
208
|
+
document: { jsonLd: [{}] },
|
|
209
|
+
expected: { score: 8, issues: 0, suggestions: 0 },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
'json-ld-completeness': {
|
|
213
|
+
positive: {
|
|
214
|
+
purpose: 'JSON-LD with context and type receives the full completeness score.',
|
|
215
|
+
document: { jsonLd: [{ '@context': 'https://schema.org', '@type': 'SoftwareApplication' }] },
|
|
216
|
+
expected: { score: 12, issues: 0, suggestions: 0 },
|
|
217
|
+
},
|
|
218
|
+
negative: {
|
|
219
|
+
purpose: 'A JSON-LD object missing both required fields receives the deterministic completeness warning.',
|
|
220
|
+
document: { jsonLd: [{}] },
|
|
221
|
+
expected: { score: 0, issues: 1, suggestions: 0 },
|
|
222
|
+
},
|
|
223
|
+
boundary: {
|
|
224
|
+
purpose: 'No structured data receives no completeness penalty because optional absence belongs to the presence rule.',
|
|
225
|
+
document: { jsonLd: [] },
|
|
226
|
+
expected: { score: 12, issues: 0, suggestions: 0 },
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
'robots-txt-ai-config': {
|
|
230
|
+
positive: {
|
|
231
|
+
purpose: 'An indexable page receives the full page-level crawler score.',
|
|
232
|
+
document: { metaTags: { robots: 'index,follow' } },
|
|
233
|
+
expected: { score: 8, issues: 0, suggestions: 1 },
|
|
234
|
+
},
|
|
235
|
+
negative: {
|
|
236
|
+
purpose: 'A noindex directive triggers the deterministic critical finding.',
|
|
237
|
+
document: { metaTags: { robots: 'noindex,nofollow' } },
|
|
238
|
+
expected: { score: 0, issues: 1, suggestions: 0 },
|
|
239
|
+
},
|
|
240
|
+
boundary: {
|
|
241
|
+
purpose: 'A nofollow-only directive is not confused with noindex; site-level crawler access remains a separate check.',
|
|
242
|
+
document: { metaTags: { robots: 'nofollow,noarchive' } },
|
|
243
|
+
expected: { score: 8, issues: 0, suggestions: 1 },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
'meta-description-quality': {
|
|
247
|
+
positive: {
|
|
248
|
+
purpose: 'A page-specific readable summary receives the full metadata score.',
|
|
249
|
+
document: { metaTags: { description: 'A deterministic release guide covering package, Action, and rollback verification.' } },
|
|
250
|
+
expected: { score: 7, issues: 0, suggestions: 0 },
|
|
251
|
+
},
|
|
252
|
+
negative: {
|
|
253
|
+
purpose: 'A missing description receives a warning without claiming a ranking outcome.',
|
|
254
|
+
document: { metaTags: {} },
|
|
255
|
+
expected: { score: 0, issues: 1, suggestions: 0 },
|
|
256
|
+
},
|
|
257
|
+
boundary: {
|
|
258
|
+
purpose: 'A long but page-specific description is not penalized by a fabricated fixed-length limit.',
|
|
259
|
+
document: { metaTags: { description: `A page-specific release explanation ${repeatedWords(180, 'context')}.` } },
|
|
260
|
+
expected: { score: 7, issues: 0, suggestions: 0 },
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
'content-boilerplate-ratio': {
|
|
264
|
+
positive: {
|
|
265
|
+
purpose: 'Paragraph content at sixty percent of extracted text receives the full heuristic score.',
|
|
266
|
+
document: { paragraphs: [words(60, 'content')], rawText: `${words(60, 'content')} ${words(40, 'navigation')}` },
|
|
267
|
+
expected: { score: 5, issues: 0, suggestions: 0 },
|
|
268
|
+
},
|
|
269
|
+
negative: {
|
|
270
|
+
purpose: 'Very little paragraph content relative to total text receives a review suggestion.',
|
|
271
|
+
document: { paragraphs: [words(20, 'content')], rawText: `${words(20, 'content')} ${words(80, 'navigation')}` },
|
|
272
|
+
expected: { score: 1, issues: 0, suggestions: 1 },
|
|
273
|
+
},
|
|
274
|
+
boundary: {
|
|
275
|
+
purpose: 'Exactly forty percent paragraph content stays at the documented middle threshold instead of the low band.',
|
|
276
|
+
document: { paragraphs: [words(40, 'content')], rawText: `${words(40, 'content')} ${words(60, 'navigation')}` },
|
|
277
|
+
expected: { score: 3, issues: 0, suggestions: 0 },
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
'keyword-stuffing-detection': {
|
|
281
|
+
positive: {
|
|
282
|
+
purpose: 'Long content with diverse vocabulary receives the full repetition score.',
|
|
283
|
+
document: { rawText: words(80, 'term') },
|
|
284
|
+
expected: { score: 5, issues: 0, suggestions: 0 },
|
|
285
|
+
},
|
|
286
|
+
negative: {
|
|
287
|
+
purpose: 'Low-diversity language repeated across sentences triggers the stuffing heuristic.',
|
|
288
|
+
document: { rawText: stuffedText },
|
|
289
|
+
expected: { score: 0, issues: 1, suggestions: 1 },
|
|
290
|
+
},
|
|
291
|
+
boundary: {
|
|
292
|
+
purpose: 'A short sample below fifty words is not classified from insufficient repetition evidence.',
|
|
293
|
+
document: { rawText: repeatedWords(49, 'widget') },
|
|
294
|
+
expected: { score: 5, issues: 0, suggestions: 0 },
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
'content-uniqueness-signals': {
|
|
298
|
+
positive: {
|
|
299
|
+
purpose: 'A verifiable original measurement plus a code example receives the full heuristic score.',
|
|
300
|
+
document: { rawText: 'Our research measured the documented fixture under controlled inputs.', html: '<pre><code>npm run check</code></pre>' },
|
|
301
|
+
expected: { score: 5, issues: 0, suggestions: 0 },
|
|
302
|
+
},
|
|
303
|
+
negative: {
|
|
304
|
+
purpose: 'Generic prose with no original evidence or example remains at the base score.',
|
|
305
|
+
document: { rawText: 'The package checks content.', html: '<p>The package checks content.</p>' },
|
|
306
|
+
expected: { score: 2, issues: 0, suggestions: 1 },
|
|
307
|
+
},
|
|
308
|
+
boundary: {
|
|
309
|
+
purpose: 'A code sample alone adds one point but cannot masquerade as original research.',
|
|
310
|
+
document: { rawText: 'Run the documented command.', html: '<pre><code>aeoptimize --version</code></pre>' },
|
|
311
|
+
expected: { score: 3, issues: 0, suggestions: 1 },
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aeoptimize",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Deterministic content-readiness lint for static websites and documentation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/core/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"
|
|
9
|
-
"aeo
|
|
8
|
+
"aeoptimize": "dist/cli/index.js",
|
|
9
|
+
"aeo": "dist/cli/index.js",
|
|
10
|
+
"aeo-cli": "dist/cli/index.js"
|
|
10
11
|
},
|
|
11
12
|
"exports": {
|
|
12
13
|
".": "./dist/core/index.js",
|
|
@@ -17,16 +18,26 @@
|
|
|
17
18
|
"dist/",
|
|
18
19
|
"skills/",
|
|
19
20
|
"agents/",
|
|
21
|
+
"fixtures/",
|
|
22
|
+
"examples/github-action-sample/",
|
|
23
|
+
"scripts/verify-release-v0.6.sh",
|
|
20
24
|
".claude-plugin/",
|
|
25
|
+
"docs/methodology.md",
|
|
26
|
+
"docs/release-v0.6.md",
|
|
27
|
+
"CHANGELOG.md",
|
|
28
|
+
"CONTRIBUTING.md",
|
|
29
|
+
"ROADMAP.md",
|
|
30
|
+
"SECURITY.md",
|
|
21
31
|
"README.md",
|
|
22
32
|
"LICENSE"
|
|
23
33
|
],
|
|
24
34
|
"scripts": {
|
|
25
35
|
"build": "tsc",
|
|
36
|
+
"check": "npm test && npm run build",
|
|
26
37
|
"dev": "tsc --watch",
|
|
27
38
|
"test": "vitest run",
|
|
28
39
|
"test:watch": "vitest",
|
|
29
|
-
"prepublishOnly": "npm
|
|
40
|
+
"prepublishOnly": "npm run check"
|
|
30
41
|
},
|
|
31
42
|
"keywords": [
|
|
32
43
|
"aeo",
|
|
@@ -40,25 +51,28 @@
|
|
|
40
51
|
"vite-plugin",
|
|
41
52
|
"nextjs-plugin"
|
|
42
53
|
],
|
|
43
|
-
"homepage": "https://github.com/
|
|
54
|
+
"homepage": "https://github.com/cucuwang/aeoptimize",
|
|
44
55
|
"repository": {
|
|
45
56
|
"type": "git",
|
|
46
|
-
"url": "https://github.com/
|
|
57
|
+
"url": "git+https://github.com/cucuwang/aeoptimize.git"
|
|
58
|
+
},
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/cucuwang/aeoptimize/issues"
|
|
47
61
|
},
|
|
48
62
|
"license": "MIT",
|
|
49
63
|
"engines": {
|
|
50
|
-
"node": ">=
|
|
64
|
+
"node": ">=22.12.0"
|
|
51
65
|
},
|
|
52
66
|
"dependencies": {
|
|
53
67
|
"chalk": "^5.3.0",
|
|
54
|
-
"puppeteer-core": "^
|
|
55
|
-
"cheerio": "^1.
|
|
68
|
+
"puppeteer-core": "^25.7.0",
|
|
69
|
+
"cheerio": "^1.2.0",
|
|
56
70
|
"commander": "^12.0.0",
|
|
57
71
|
"gray-matter": "^4.0.3"
|
|
58
72
|
},
|
|
59
73
|
"devDependencies": {
|
|
60
|
-
"@types/node": "^
|
|
74
|
+
"@types/node": "^24.0.0",
|
|
61
75
|
"typescript": "^5.4.0",
|
|
62
|
-
"vitest": "^
|
|
76
|
+
"vitest": "^4.1.10"
|
|
63
77
|
}
|
|
64
78
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -u
|
|
3
|
+
|
|
4
|
+
PACKAGE_NAME=aeoptimize
|
|
5
|
+
EXPECTED_VERSION=0.6.0
|
|
6
|
+
EXPECTED_TAG=v0.6.0
|
|
7
|
+
REPOSITORY=cucuwang/aeoptimize
|
|
8
|
+
EXPECTED_COMMIT=${1:-}
|
|
9
|
+
EXPECTED_PACKAGE_SHA256=${2:-}
|
|
10
|
+
EXPECTED_REPOSITORY_URL=git+https://github.com/cucuwang/aeoptimize.git
|
|
11
|
+
EXPECTED_HOMEPAGE=https://github.com/cucuwang/aeoptimize
|
|
12
|
+
EXPECTED_BUGS_URL=https://github.com/cucuwang/aeoptimize/issues
|
|
13
|
+
|
|
14
|
+
if [ -z "$EXPECTED_COMMIT" ] || [ -z "$EXPECTED_PACKAGE_SHA256" ]; then
|
|
15
|
+
echo "usage: $0 <expected-release-commit> <expected-package-sha256>" >&2
|
|
16
|
+
exit 2
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
if ! [[ "$EXPECTED_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
|
|
20
|
+
echo "expected-release-commit must be a lowercase 40-character Git SHA" >&2
|
|
21
|
+
exit 2
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
if ! [[ "$EXPECTED_PACKAGE_SHA256" =~ ^[0-9a-f]{64}$ ]]; then
|
|
25
|
+
echo "expected-package-sha256 must be a lowercase 64-character SHA-256" >&2
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
for command_name in awk curl jq npm git mktemp node; do
|
|
30
|
+
if ! command -v "$command_name" >/dev/null 2>&1; then
|
|
31
|
+
echo "missing required command: $command_name" >&2
|
|
32
|
+
exit 2
|
|
33
|
+
fi
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
VERIFY_BASE=${TMPDIR:-/tmp}
|
|
37
|
+
VERIFY_BASE=${VERIFY_BASE%/}
|
|
38
|
+
VERIFY_ROOT=$(mktemp -d "$VERIFY_BASE/aeoptimize-release-verify.XXXXXX")
|
|
39
|
+
REGISTRY_JSON="$VERIFY_ROOT/registry.json"
|
|
40
|
+
RELEASE_JSON="$VERIFY_ROOT/release.json"
|
|
41
|
+
PACKAGE_TARBALL="$VERIFY_ROOT/$PACKAGE_NAME-$EXPECTED_VERSION.tgz"
|
|
42
|
+
CONSUMER_ROOT="$VERIFY_ROOT/consumer"
|
|
43
|
+
FAILURES=0
|
|
44
|
+
|
|
45
|
+
cleanup() {
|
|
46
|
+
if [ "${KEEP_VERIFY_ROOT:-0}" = "1" ]; then
|
|
47
|
+
echo "Verification workspace preserved: $VERIFY_ROOT"
|
|
48
|
+
return
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
case "$VERIFY_ROOT" in
|
|
52
|
+
"$VERIFY_BASE"/aeoptimize-release-verify.*)
|
|
53
|
+
rm -rf -- "$VERIFY_ROOT"
|
|
54
|
+
;;
|
|
55
|
+
*)
|
|
56
|
+
echo "Refusing to remove unexpected verification path: $VERIFY_ROOT" >&2
|
|
57
|
+
;;
|
|
58
|
+
esac
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
trap cleanup EXIT
|
|
62
|
+
|
|
63
|
+
pass() {
|
|
64
|
+
echo "PASS: $1"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
note() {
|
|
68
|
+
echo "INFO: $1"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fail() {
|
|
72
|
+
echo "FAIL: $1" >&2
|
|
73
|
+
FAILURES=$((FAILURES + 1))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if curl -fsS "https://registry.npmjs.org/$PACKAGE_NAME" > "$REGISTRY_JSON"; then
|
|
77
|
+
latest=$(jq -r '."dist-tags".latest // empty' "$REGISTRY_JSON")
|
|
78
|
+
if [ "$latest" = "$EXPECTED_VERSION" ]; then
|
|
79
|
+
pass "npm latest is $EXPECTED_VERSION"
|
|
80
|
+
else
|
|
81
|
+
fail "npm latest is ${latest:-missing}; expected $EXPECTED_VERSION"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
if jq -e --arg version "$EXPECTED_VERSION" '.versions[$version] != null' "$REGISTRY_JSON" >/dev/null; then
|
|
85
|
+
pass "npm contains exact version $EXPECTED_VERSION"
|
|
86
|
+
|
|
87
|
+
published_git_head=$(jq -r --arg version "$EXPECTED_VERSION" '.versions[$version].gitHead // empty' "$REGISTRY_JSON")
|
|
88
|
+
if [ -z "$published_git_head" ]; then
|
|
89
|
+
note "npm does not expose gitHead; tarball SHA-256 remains the artifact identity gate"
|
|
90
|
+
elif [ "$published_git_head" = "$EXPECTED_COMMIT" ]; then
|
|
91
|
+
pass "npm gitHead matches $EXPECTED_COMMIT"
|
|
92
|
+
else
|
|
93
|
+
fail "npm gitHead is $published_git_head; expected $EXPECTED_COMMIT"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
published_repository=$(jq -r --arg version "$EXPECTED_VERSION" '(.versions[$version].repository | if type == "object" then .url else . end) // empty' "$REGISTRY_JSON")
|
|
97
|
+
published_homepage=$(jq -r --arg version "$EXPECTED_VERSION" '.versions[$version].homepage // empty' "$REGISTRY_JSON")
|
|
98
|
+
published_bugs=$(jq -r --arg version "$EXPECTED_VERSION" '.versions[$version].bugs.url // empty' "$REGISTRY_JSON")
|
|
99
|
+
|
|
100
|
+
if [ "$published_repository" = "$EXPECTED_REPOSITORY_URL" ] && \
|
|
101
|
+
[ "$published_homepage" = "$EXPECTED_HOMEPAGE" ] && \
|
|
102
|
+
[ "$published_bugs" = "$EXPECTED_BUGS_URL" ]; then
|
|
103
|
+
pass "npm repository identity matches $REPOSITORY"
|
|
104
|
+
else
|
|
105
|
+
fail "npm repository identity does not match $REPOSITORY"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
tarball_url=$(jq -r --arg version "$EXPECTED_VERSION" '.versions[$version].dist.tarball // empty' "$REGISTRY_JSON")
|
|
109
|
+
if [ -n "$tarball_url" ] && curl -fLsS "$tarball_url" -o "$PACKAGE_TARBALL"; then
|
|
110
|
+
package_sha256=$(node -e "const crypto=require('node:crypto');const fs=require('node:fs');const path=process.argv[1];console.log(crypto.createHash('sha256').update(fs.readFileSync(path)).digest('hex'))" "$PACKAGE_TARBALL")
|
|
111
|
+
if [ "$package_sha256" = "$EXPECTED_PACKAGE_SHA256" ]; then
|
|
112
|
+
pass "npm tarball SHA-256 matches the verified candidate"
|
|
113
|
+
else
|
|
114
|
+
fail "npm tarball SHA-256 is ${package_sha256:-missing}; expected $EXPECTED_PACKAGE_SHA256"
|
|
115
|
+
fi
|
|
116
|
+
else
|
|
117
|
+
fail "npm tarball could not be downloaded for SHA-256 verification"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
if npm --cache "$VERIFY_ROOT/npm-cache" install \
|
|
121
|
+
--ignore-scripts --no-audit --no-fund \
|
|
122
|
+
--prefix "$CONSUMER_ROOT" "$PACKAGE_NAME@$EXPECTED_VERSION" >/dev/null; then
|
|
123
|
+
for binary in aeoptimize aeo aeo-cli; do
|
|
124
|
+
binary_version=$("$CONSUMER_ROOT/node_modules/.bin/$binary" --version 2>/dev/null || true)
|
|
125
|
+
if [ "$binary_version" = "$EXPECTED_VERSION" ]; then
|
|
126
|
+
pass "$binary resolves to $EXPECTED_VERSION from the public package"
|
|
127
|
+
else
|
|
128
|
+
fail "$binary returned ${binary_version:-no version}; expected $EXPECTED_VERSION"
|
|
129
|
+
fi
|
|
130
|
+
done
|
|
131
|
+
else
|
|
132
|
+
fail "clean consumer installation failed for $PACKAGE_NAME@$EXPECTED_VERSION"
|
|
133
|
+
fi
|
|
134
|
+
else
|
|
135
|
+
fail "npm does not contain exact version $EXPECTED_VERSION"
|
|
136
|
+
fi
|
|
137
|
+
else
|
|
138
|
+
fail "npm registry metadata could not be fetched"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
tag_lines=$(git ls-remote --tags "https://github.com/$REPOSITORY.git" \
|
|
142
|
+
"refs/tags/$EXPECTED_TAG" "refs/tags/$EXPECTED_TAG^{}" 2>/dev/null || true)
|
|
143
|
+
tag_commit=$(printf '%s\n' "$tag_lines" | awk -v peeled="refs/tags/$EXPECTED_TAG^{}" '$2 == peeled { print $1 }')
|
|
144
|
+
if [ -z "$tag_commit" ]; then
|
|
145
|
+
tag_commit=$(printf '%s\n' "$tag_lines" | awk -v direct="refs/tags/$EXPECTED_TAG" '$2 == direct { print $1 }')
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
if [ "$tag_commit" = "$EXPECTED_COMMIT" ]; then
|
|
149
|
+
pass "$EXPECTED_TAG points to $EXPECTED_COMMIT"
|
|
150
|
+
else
|
|
151
|
+
fail "$EXPECTED_TAG points to ${tag_commit:-missing}; expected $EXPECTED_COMMIT"
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
release_status=$(curl -sS -o "$RELEASE_JSON" -w '%{http_code}' \
|
|
155
|
+
"https://api.github.com/repos/$REPOSITORY/releases/tags/$EXPECTED_TAG" || true)
|
|
156
|
+
if [ "$release_status" = "200" ]; then
|
|
157
|
+
release_state=$(jq -r '[.tag_name, (.draft | tostring), (.prerelease | tostring)] | @tsv' "$RELEASE_JSON")
|
|
158
|
+
if [ "$release_state" = "$EXPECTED_TAG"$'\tfalse\tfalse' ]; then
|
|
159
|
+
pass "GitHub Release is published for $EXPECTED_TAG"
|
|
160
|
+
else
|
|
161
|
+
fail "GitHub Release is not a published non-prerelease for $EXPECTED_TAG"
|
|
162
|
+
fi
|
|
163
|
+
else
|
|
164
|
+
fail "GitHub Release lookup returned HTTP ${release_status:-error}"
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
if [ "$FAILURES" -ne 0 ]; then
|
|
168
|
+
echo "$FAILURES release verification check(s) failed." >&2
|
|
169
|
+
exit 1
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
echo "All public release checks passed."
|
|
@@ -1,53 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aeo-generate
|
|
3
|
-
description: Use when
|
|
3
|
+
description: Use when previewing optional llms.txt proposal files, candidate JSON-LD, or crawler-control suggestions for a website build
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# AEO Generate —
|
|
6
|
+
# AEO Generate — Optional Discovery Artifacts
|
|
7
7
|
|
|
8
|
-
Generate
|
|
9
|
-
|
|
10
|
-
## What Gets Generated
|
|
11
|
-
|
|
12
|
-
| File | Purpose |
|
|
13
|
-
|------|---------|
|
|
14
|
-
| `llms.txt` | Machine-readable site summary for LLMs (llmstxt.org standard) |
|
|
15
|
-
| `llms-full.txt` | Full content version for deep AI consumption |
|
|
16
|
-
| `_aeo/generated-schemas.json` | JSON-LD schemas (Article, FAQPage, BreadcrumbList) |
|
|
17
|
-
| robots.txt suggestions | AI crawler allow/deny rules (printed, not auto-applied) |
|
|
8
|
+
Generate reviewable candidate artifacts. These files do not guarantee crawling, indexing, search features, visibility, or citation.
|
|
18
9
|
|
|
19
10
|
## Workflow
|
|
20
11
|
|
|
21
|
-
1.
|
|
22
|
-
|
|
23
|
-
- Vite/Astro: `dist/`
|
|
24
|
-
- Hugo/Jekyll: `public/`
|
|
12
|
+
1. Identify an authorized static build directory.
|
|
13
|
+
2. Preview without writing:
|
|
25
14
|
|
|
26
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npx aeoptimize generate <directory> --dry-run
|
|
27
17
|
```
|
|
28
|
-
npx aeoptimize generate <dir> --dry-run
|
|
29
|
-
```
|
|
30
|
-
Show the user what will be generated and explain each file's purpose.
|
|
31
18
|
|
|
32
|
-
3.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
4. **Review generated files.** Read each generated file and suggest manual refinements:
|
|
38
|
-
- `llms.txt`: Verify site name, description, and page listing are accurate
|
|
39
|
-
- JSON-LD: Check that generated schemas match the actual content
|
|
40
|
-
- robots.txt: Explain each AI crawler and let user decide allow/deny
|
|
19
|
+
3. Review every output:
|
|
20
|
+
- `llms.txt` and `llms-full.txt` are experiments based on a proposal.
|
|
21
|
+
- Candidate `Article` or `BreadcrumbList` JSON-LD must match visible content.
|
|
22
|
+
- Crawler rules have service-specific meanings; an allow rule is not an outcome guarantee.
|
|
23
|
+
4. Write only after the user approves the exact directory:
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- Inject generated JSON-LD into page `<head>` sections
|
|
46
|
-
- Merge robots.txt suggestions with existing rules
|
|
25
|
+
```bash
|
|
26
|
+
npx aeoptimize generate <directory>
|
|
27
|
+
```
|
|
47
28
|
|
|
48
|
-
##
|
|
29
|
+
## Boundaries
|
|
49
30
|
|
|
50
|
-
-
|
|
51
|
-
- Never
|
|
52
|
-
-
|
|
53
|
-
-
|
|
31
|
+
- Never overwrite an existing artifact without confirmation and a recoverable copy.
|
|
32
|
+
- Never infer `FAQPage` from question headings.
|
|
33
|
+
- Never add `<link rel="llms-txt">` as if it were a standardized discovery mechanism.
|
|
34
|
+
- Never auto-apply `robots.txt` suggestions.
|
|
35
|
+
- Validate structured data against current primary documentation before deployment.
|