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.
@@ -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.5.2",
4
- "description": "CLI toolkit that transforms SEO-optimized websites into AI-search-ready content",
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
- "aeo": "./dist/cli/index.js",
9
- "aeo-cli": "./dist/cli/index.js"
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 test && npm run build"
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/dexuwang627-cloud/aeoptimize",
54
+ "homepage": "https://github.com/cucuwang/aeoptimize",
44
55
  "repository": {
45
56
  "type": "git",
46
- "url": "https://github.com/dexuwang627-cloud/aeoptimize.git"
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": ">=18.0.0"
64
+ "node": ">=22.12.0"
51
65
  },
52
66
  "dependencies": {
53
67
  "chalk": "^5.3.0",
54
- "puppeteer-core": "^24.0.0",
55
- "cheerio": "^1.0.0",
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": "^20.0.0",
74
+ "@types/node": "^24.0.0",
61
75
  "typescript": "^5.4.0",
62
- "vitest": "^2.0.0"
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 creating llms.txt, JSON-LD structured data, or robots.txt AI crawler configuration for a website or project build output
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 — AI Infrastructure Files
6
+ # AEO Generate — Optional Discovery Artifacts
7
7
 
8
- Generate AI infrastructure files from existing website content to make it discoverable by AI search engines.
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. **Identify build output.** Ask the user for the directory containing their built site (e.g., `dist/`, `out/`, `build/`). Check for common framework patterns:
22
- - Next.js: `.next/` or `out/`
23
- - Vite/Astro: `dist/`
24
- - Hugo/Jekyll: `public/`
12
+ 1. Identify an authorized static build directory.
13
+ 2. Preview without writing:
25
14
 
26
- 2. **Preview first.** Run:
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. **Confirm and generate.** On approval:
33
- ```
34
- npx aeoptimize generate <dir>
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
- 5. **Integration guidance.** Explain how to deploy:
43
- - Place `llms.txt` at site root (alongside `robots.txt`)
44
- - Add `<link rel="llms-txt" href="/llms.txt">` to HTML `<head>`
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
- ## Important
29
+ ## Boundaries
49
30
 
50
- - Always preview with `--dry-run` before writing
51
- - Never overwrite existing files without user confirmation
52
- - Suggest running `/aeo-scan` first to understand current state
53
- - The robots.txt suggestions are printed only — never auto-modify robots.txt
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.