page-foundry 2.9.1 → 3.2.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.
@@ -175,6 +175,50 @@ def scan_structural(copy):
175
175
  return warns
176
176
 
177
177
 
178
+ KICKER_PAIR = re.compile(
179
+ # A label element directly before a section heading. The body group refuses
180
+ # to cross its own closing tag or any block/heading boundary, so a long
181
+ # paragraph sitting before the real kicker cannot backtrack-swallow it.
182
+ r"<(p|span|div|small)\b([^>]*)>"
183
+ r"((?:[^<]|<(?!/\1\s*>|/?(?:p|div|small|h[1-6])\b)[^>]*>)*)"
184
+ r"</\1\s*>\s*<h([234])\b[^>]*>(.*?)</h\4\s*>",
185
+ re.S | re.I)
186
+
187
+
188
+ def scan_html_kickers(raw):
189
+ """Repeated section kickers, absorbed from impeccable's detector (rule
190
+ repeated-section-kickers) under the v3.0 capability-ownership split: a short
191
+ uppercase label element sitting directly before an h2/h3/h4, three or more
192
+ times on a page, is AI editorial scaffolding. Impeccable gates on computed
193
+ font-size and letter-spacing; a static scanner cannot, so this gates on the
194
+ source-level tells instead: literal ALL-CAPS text, an `uppercase` utility
195
+ class or inline transform, or an eyebrow/kicker class name. Pairs split by
196
+ an intermediate wrapper element are invisible to this regex; the design
197
+ detect gate (impeccable detect, when installed) sees those with real styles.
198
+ WARN-level, threshold 3 per page, matching the source rule's advisory
199
+ severity and its 'once is fine; the pattern is the tell' doctrine."""
200
+ candidates = []
201
+ for m in KICKER_PAIR.finditer(raw):
202
+ attrs = m.group(2).lower()
203
+ kicker = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", m.group(3))).strip()
204
+ heading = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", m.group(5))).strip()
205
+ if not (2 <= len(kicker) <= 34) or len(heading) < 3:
206
+ continue
207
+ if re.match(r"step\s*\d+", kicker, re.I) or re.fullmatch(r"\d{1,2}", kicker):
208
+ continue
209
+ allcaps = re.search(r"[A-Z]", kicker) and not re.search(r"[a-z]", kicker)
210
+ styled = "uppercase" in attrs or "eyebrow" in attrs or "kicker" in attrs
211
+ if not (allcaps or styled):
212
+ continue
213
+ line_no = raw.count("\n", 0, m.start()) + 1
214
+ candidates.append((line_no, kicker, "h" + m.group(4), heading))
215
+ if len(candidates) < 3:
216
+ return []
217
+ return [(ln, "repeated section kickers ('%s' before %s, %d on page)"
218
+ % (kick, tag, len(candidates)), head[:80])
219
+ for ln, kick, tag, head in candidates]
220
+
221
+
178
222
  def scan_file(path, banned, judgment, patterns, cfg):
179
223
  fails, warns = [], []
180
224
  raw = path.read_text(encoding="utf-8", errors="replace")
@@ -196,6 +240,8 @@ def scan_file(path, banned, judgment, patterns, cfg):
196
240
  if dash in line and cfg.get(key) != "off":
197
241
  (fails if cfg.get(key) == "fail" else warns).append((i, label, ctx))
198
242
  warns += scan_structural(copy)
243
+ if path.suffix.lower() in (".html", ".htm"):
244
+ warns += scan_html_kickers(raw)
199
245
  bangs = copy.count("!")
200
246
  if bangs > cfg.get("max_exclamations", 1):
201
247
  fails.append((0, f"{bangs} exclamation points (max {cfg.get('max_exclamations', 1)} per page)", ""))
@@ -0,0 +1,214 @@
1
+ #!/bin/bash
2
+ # Build calibration fixtures for run_audit.py. Idempotent.
3
+ set -e
4
+ ROOT="$1"; AUDIT="$2"
5
+ rm -rf "$ROOT"; mkdir -p "$ROOT"/.agents "$ROOT"/pages/demo
6
+ D="$ROOT/.agents/foundry/demo"
7
+ mkdir -p "$D"/copy "$D"/tokens "$D"/comp
8
+
9
+ cat > "$ROOT/.agents/product-marketing.md" <<'E'
10
+ # Product Marketing Context
11
+ **One-liner:** demo product for the solo builder shipping their own page.
12
+ ## Target Audience
13
+ solo indie founders who ship many pages
14
+ ## Problems & Pain Points
15
+ they ship a page and cannot tell why nobody signs up
16
+ ## Differentiation
17
+ the spec is written before the design
18
+ E
19
+
20
+ cat > "$D/preflight.md" <<'E'
21
+ ## Preflight — 2026-07-24
22
+ | companion | tier | state | resolved-via |
23
+ |---|---|---|---|
24
+ | product-marketing | core | present | npx skills list |
25
+ | customer-research | core | present | npx skills list |
26
+ | marketing-psychology | core | present | filesystem |
27
+ | cro | core | present | filesystem |
28
+ | copywriting | core | present | filesystem |
29
+ | frontend-design | core | present | npx skills list |
30
+ | humanizer | core | present | filesystem |
31
+ | impeccable | core | present | npx impeccable |
32
+ override: none
33
+ E
34
+
35
+ cat > "$D/voc.md" <<'E'
36
+ # voc.md
37
+ ## Verbatim
38
+ > I can't tell what this does.
39
+ — Hacker News, https://news.ycombinator.com/item?id=42901780 · vunderba · 2025-02-01 · comprehension
40
+ > after 10 days, I have zero signups. Not even one.
41
+ — r/SaaS, https://www.reddit.com/r/SaaS/comments/1mf2dgx/ · SetDependent1293 · 2025-08-01 · no-signups
42
+ > your landing page contains a lot of AI generated metaphors
43
+ — Hacker News, https://news.ycombinator.com/item?id=46836627 · flexagoon · 2026-01-31 · slop
44
+ ## Themes
45
+ 1. comprehension failure (High, 3 sources)
46
+ 2. zero signups, no diagnosis (High)
47
+ 3. looks AI-made (Medium)
48
+ E
49
+
50
+ cat > "$D/persuasion-map.md" <<'E'
51
+ # Persuasion map
52
+ Levers chosen for a skeptical solo builder who reads before trusting.
53
+ ## Curse of Knowledge (primary lever)
54
+ Lands on: the lead claim and the whole register. The buyer cannot read their own page cold.
55
+ ## Pratfall effect
56
+ Lands on: the objection answers. Admit newness and slowness plainly.
57
+ ## Jobs to be Done
58
+ Lands on: every capability line, framed as the outcome not the artifact.
59
+ ## Levers not chosen
60
+ Social proof: out of scope, adoption is thin and stays TK.
61
+ E
62
+
63
+ cat > "$D/message-architecture.md" <<'E'
64
+ # Message architecture
65
+ Positioning: for the solo builder, demo is the thing that writes the spec, unlike a design tool that only renders what you hand it.
66
+ ## Hierarchy
67
+ 1. primary claim: the page argues a case
68
+ 2. secondary claim: the argument traces to a real quote
69
+ 3. tertiary claim: nothing is fabricated
70
+ ## Objection map
71
+ O1: another AI builder. Answer: output is yours, no template.
72
+ O2: I can just prompt an agent. Answer: the gates are the repeatability.
73
+ O3: is it safe. Answer: one stdlib script, read it.
74
+ ## Proof inventory
75
+ - real: the voice scanner, the run artifacts on disk
76
+ - TK: adoption count
77
+ E
78
+
79
+ cat > "$D/page-spec.md" <<'E'
80
+ # Page spec (oss-project)
81
+ archetype: oss-project
82
+ 1. hero — primary claim — proof: the demo itself
83
+ 2. problem — the comprehension failure — no proof needed
84
+ 3. how it works — secondary claim — proof: the artifact chain
85
+ 4. objections — O1 O2 O3 — proof beside each
86
+ 5. install — CTA — the one command
87
+ Answer block: the head title and meta description answer the query intent.
88
+ Measurement plan: install count via the CLI, no third-party analytics.
89
+ E
90
+
91
+ cat > "$D/copy/hero-candidates.md" <<'E'
92
+ # Hero candidates
93
+ 1. One brief in, a page that argues its case out. — score 8, message match high
94
+ 2. You already know what an AI page looks like. — score 9, specificity high
95
+ 3. The spec your page never got. — score 7, budget ok
96
+ Pick: candidate 2.
97
+ E
98
+
99
+ cat > "$D/copy/approved-draft.md" <<'E'
100
+ **`<title>`:** demo: the spec your page never got
101
+ **meta description:** demo writes the marketing spec before anything gets designed.
102
+ # Copy (page order)
103
+ Hero: You already know what an AI page looks like.
104
+ Problem: nobody can tell what it does.
105
+ How: it writes the spec first, then builds from it.
106
+ E
107
+
108
+ cat > "$D/copy/copy-approved.md" <<'E'
109
+ **`<title>`:** demo: the spec your page never got
110
+ **meta description:** demo writes the marketing spec before anything gets designed.
111
+ # Copy (frozen snapshot, page order)
112
+ Hero: You already know what an AI page looks like.
113
+ Problem: nobody can tell what it does.
114
+ How: it writes the spec first, then builds from it.
115
+ Install: one command.
116
+ E
117
+
118
+ cat > "$D/imagery-plan.md" <<'E'
119
+ # Imagery plan
120
+ Consistency rules: one grade (cool, high-contrast), 16:9 crops, hairline frames.
121
+ - hero: real product screenshot, browser chrome cropped out, 16:9, cool grade.
122
+ - proof: no image by choice; the gate report card carries the visual weight.
123
+ - how-it-works: a diagram of the real component flow, on-token line weights.
124
+ Every image traces to a slot here; no product screenshot is a mockup.
125
+ E
126
+
127
+ cat > "$D/states-motion.md" <<'E'
128
+ # States and motion
129
+ - form: error state = red hairline + aria-live message under the field; success =
130
+ inline confirmation, no redirect. loading = disabled button with a spinner glyph.
131
+ - copy button: copied state swaps label to "Copied" for 1.4s.
132
+ - focus: 2px ring, brand color, offset 2px, :focus-visible only.
133
+ - hover: 120ms ease on transform/opacity only; nothing hover-only.
134
+ - motion identity: one scroll-reveal on section entry, 200ms ease-out;
135
+ prefers-reduced-motion disables it and shows the static end state.
136
+ E
137
+
138
+ cat > "$D/tokens/plan-a.md" <<'E'
139
+ # Plan A — instrument dark
140
+ brand #ff3b1d on #0a0a0a, mono display, tight grid
141
+ E
142
+ cat > "$D/tokens/plan-b.md" <<'E'
143
+ # Plan B — paper light
144
+ brand #0a0a0a on #ffffff, serif display, wide grid
145
+ E
146
+ cat > "$D/tokens/selection.md" <<'E'
147
+ Winner: plan-a. Why: the buyer reads at night in a terminal; instrument dark matches the register.
148
+ E
149
+
150
+ cat > "$D/comp/index.html" <<'E'
151
+ <!doctype html><html><head><title>comp</title></head>
152
+ <body><h1>You already know what an AI page looks like.</h1>
153
+ <p>nobody can tell what it does.</p></body></html>
154
+ E
155
+ cat > "$D/comp/rounds.md" <<'E'
156
+ Round 1: hero ran too tall at 1440, subhead lost. Fixed the scale.
157
+ Round 2: 390 wrap held, density confirmed. Approved.
158
+ E
159
+
160
+ cat > "$ROOT/pages/demo/index.html" <<'E'
161
+ <!doctype html><html><head><title>demo: the spec your page never got</title>
162
+ <meta name="description" content="demo writes the marketing spec before anything gets designed."></head>
163
+ <body><h1>You already know what an AI page looks like.</h1>
164
+ <p>nobody can tell what it does. demo writes the spec first, then builds from it.</p></body></html>
165
+ E
166
+ cat > "$ROOT/pages/demo/theme.css" <<'E'
167
+ :root{--brand:#ff3b1d;--bg:#0a0a0a}
168
+ E
169
+ cat > "$ROOT/pages/demo/llms.txt" <<'E'
170
+ # demo
171
+ The spec your page never got. Open source, MIT.
172
+ E
173
+
174
+ cat > "$ROOT/DESIGN.md" <<'E'
175
+ ---
176
+ name: demo
177
+ version: alpha
178
+ ---
179
+ ## Brand
180
+ brand #ff3b1d on #0a0a0a; the signature is a hairline rule under the eyebrow.
181
+ ## Type
182
+ display: mono, tight tracking; body: system sans, 1.6 line height.
183
+ ## Color
184
+ --brand #ff3b1d; --bg #0a0a0a; --fg #f4f4f4; --muted #8a8a8a.
185
+ ## Layout
186
+ container max 72ch; single column mobile, 12-col at 1024; 24px gutter.
187
+ ## Motion
188
+ one scroll-reveal, 200ms ease-out; reduced-motion disables it.
189
+ E
190
+
191
+ cat > "$D/conversion-audit.md" <<'E'
192
+ # Gate 1 conversion audit
193
+ The heuristic scored twice: the builder self-score from full context, then the
194
+ score of record from a fresh agent given only the page and the brief.
195
+ Self score: 31. Independent score: 27. Divergence: 4 points, concentrated on the
196
+ install CTA which is the strongest lever and currently thinnest.
197
+ E
198
+
199
+ cat > "$D/foundry-log.md" <<'E'
200
+ # foundry-log.md: demo
201
+ Per-property memory. Phase 6 appends after every run.
202
+
203
+ ### 2026-07-24 · build · oss-project
204
+ - headline: You already know what an AI page looks like.
205
+ - skeleton: hero, problem, how, objections, install · direct/static-hero/proof-adjacent/compressed
206
+ - meclabs: C=27 (M4 V3 I2 F3 A2)
207
+ - gates: all pass
208
+ - companions: product-marketing, customer-research, marketing-psychology, cro, copywriting, frontend-design
209
+ - degraded: none
210
+ - open items: adoption count TK
211
+ - conversion data: none exists yet
212
+ - learnings: the cold re-score caught the CTA thinness
213
+ E
214
+ echo "fixture built at $ROOT"
@@ -0,0 +1,92 @@
1
+ #!/bin/bash
2
+ # Calibration regression for run_audit.py (issue #38).
3
+ # Builds fixtures and asserts exit codes. No network, no deps.
4
+ # Run from anywhere: bash skills/page-foundry/tests/run_audit_test.sh
5
+ set -u
6
+ HERE="$(cd "$(dirname "$0")" && pwd)"
7
+ AUDIT="$HERE/../scripts/run_audit.py"
8
+ BUILD="$HERE/build_fixture.sh"
9
+ TMP="$(mktemp -d)"
10
+ trap 'rm -rf "$TMP"' EXIT
11
+ fails=0
12
+
13
+ assert_exit() { # name expected dir extra...
14
+ local name="$1" expected="$2" dir="$3"; shift 3
15
+ python3 "$AUDIT" --mode build --archetype oss-project "$@" "$dir" >/dev/null 2>&1
16
+ local got=$?
17
+ if [ "$got" -eq "$expected" ]; then
18
+ echo " ok $name (exit $got)"
19
+ else
20
+ echo " FAIL $name (got $got, want $expected)"; fails=$((fails+1))
21
+ fi
22
+ }
23
+
24
+ bash "$BUILD" "$TMP/clean" "$AUDIT" >/dev/null
25
+ assert_exit "A: complete valid run passes" 0 "$TMP/clean/.agents/foundry/demo"
26
+
27
+ cp -r "$TMP/clean" "$TMP/missing"
28
+ rm "$TMP/missing/.agents/foundry/demo/message-architecture.md"
29
+ assert_exit "B: missing required artifact fails" 1 "$TMP/missing/.agents/foundry/demo"
30
+
31
+ cp -r "$TMP/clean" "$TMP/badlog"
32
+ printf '# log\n\n## Run 1 (started 2026-07-24)\n- headline: x\n' > "$TMP/badlog/.agents/foundry/demo/foundry-log.md"
33
+ assert_exit "C: foundry-log off documented format fails" 1 "$TMP/badlog/.agents/foundry/demo"
34
+
35
+ cp -r "$TMP/clean" "$TMP/tk"
36
+ sed -i.bak 's/one command\./[TK: the install command]/' "$TMP/tk/.agents/foundry/demo/copy/copy-approved.md" && rm -f "$TMP/tk/.agents/foundry/demo/copy/copy-approved.md.bak"
37
+ assert_exit "D: unresolved [TK in copy fails" 1 "$TMP/tk/.agents/foundry/demo"
38
+
39
+ cp -r "$TMP/clean" "$TMP/stale"; sleep 1; touch "$TMP/stale/pages/demo/index.html"
40
+ assert_exit "E: mtime inversion is advisory (WARN), still passes" 0 "$TMP/stale/.agents/foundry/demo"
41
+
42
+ # F: a core companion overridden at preflight may skip its artifact (honest PARTIAL).
43
+ cp -r "$TMP/clean" "$TMP/override"
44
+ D="$TMP/override/.agents/foundry/demo"
45
+ rm "$D/persuasion-map.md"
46
+ python3 - "$D/preflight.md" <<'PY'
47
+ import sys
48
+ p = sys.argv[1]; t = open(p).read()
49
+ t = t.replace("| marketing-psychology | core | present | filesystem |",
50
+ "| marketing-psychology | core | missing | — |")
51
+ t = t.replace("override: none", "override: marketing-psychology")
52
+ open(p, "w").write(t)
53
+ PY
54
+ assert_exit "F: overridden companion may skip its artifact" 0 "$D"
55
+
56
+ # G: a degraded claim naming a PRESENT, un-overridden companion fails.
57
+ cp -r "$TMP/clean" "$TMP/lie"
58
+ D="$TMP/lie/.agents/foundry/demo"
59
+ python3 - "$D/foundry-log.md" <<'PY'
60
+ import sys
61
+ p = sys.argv[1]; t = open(p).read()
62
+ open(p, "w").write(t.replace("- degraded: none",
63
+ "- degraded: cro missing, ran on the reference file"))
64
+ PY
65
+ assert_exit "G: false degraded claim (companion was present) fails" 1 "$D"
66
+
67
+ # H: a missing Phase 4 design output (imagery-plan/states-motion) fails.
68
+ cp -r "$TMP/clean" "$TMP/noimg"
69
+ rm "$TMP/noimg/.agents/foundry/demo/imagery-plan.md"
70
+ assert_exit "H: missing imagery-plan.md fails" 1 "$TMP/noimg/.agents/foundry/demo"
71
+
72
+ # I: impeccable is core — a missing DESIGN.md fails.
73
+ cp -r "$TMP/clean" "$TMP/nodesign"
74
+ rm "$TMP/nodesign/DESIGN.md"
75
+ assert_exit "I: missing DESIGN.md (impeccable core) fails" 1 "$TMP/nodesign/.agents/foundry/demo"
76
+
77
+ # J: impeccable overridden at preflight -> DESIGN.md may be absent (honest PARTIAL).
78
+ cp -r "$TMP/clean" "$TMP/impoverride"
79
+ rm "$TMP/impoverride/DESIGN.md"
80
+ python3 - "$TMP/impoverride/.agents/foundry/demo/preflight.md" <<'PY'
81
+ import sys
82
+ p = sys.argv[1]; t = open(p).read()
83
+ t = t.replace("| impeccable | core | present | npx impeccable |",
84
+ "| impeccable | core | missing | — |")
85
+ t = t.replace("override: none", "override: impeccable")
86
+ open(p, "w").write(t)
87
+ PY
88
+ assert_exit "J: impeccable overridden -> DESIGN.md absence is advisory" 0 "$TMP/impoverride/.agents/foundry/demo"
89
+
90
+ echo ""
91
+ if [ "$fails" -eq 0 ]; then echo "run_audit calibration: all fixtures pass"; else echo "run_audit calibration: $fails FAILED"; fi
92
+ exit $fails