Skip to content
Merged
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
2 changes: 1 addition & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ Dark (via prefers-color-scheme):
4. **Write the HTML** — single self-contained file, all CSS inline, all JS (vanilla, ~100-200 lines) inline, no external fonts or CDN assets by default; include the Open Graph / Twitter summary card in `<head>` (see Output)
5. **Pre-save checklist — hard fail, not a vibe check.** Run every item; if any answer is "no" or "I don't know", do not save yet. This is the load-bearing self-review that catches the "styled prose with a link button" failure mode before it ships.

- **Shape**: I picked one from the contract list (`dashboard` / `document` / `editorial` / `timeline` / `runbook` / `comparison` / `network-map` / `triage-board` / `developer` / `execution-log` / `deck-review` / `podcast`). I did not invent a shape.
- **Shape**: I picked one from the contract list (`dashboard` / `document` / `editorial` / `timeline` / `runbook` / `comparison` / `network-map` / `triage-board` / `developer` / `execution-log` / `deck-review` / `podcast`). I did not invent a shape. `<html>` must set `data-shape` to that `contracts/shapes.json` id.
- **HTML-native features ≥3**: I can name at least three, specifically, in this artifact. *Not* "it has nice CSS" — concrete interactions: search-with-nav, click-entity-to-highlight, copy-as-prompt, sortable headers, scroll-spy, drag-to-bucket, etc.
- **Information dimensions ≥4** out of the 8 listed in §The 8 information dimensions.
- **Flatten test (author self-check, NOT a shipped control)**: I mentally stripped all JS and SVG. What disappeared? If only a hover state changed, this is styled prose, not an artifact — redesign. Do NOT ship a "flatten test" toggle in the delivered artifact — it's a meta-gimmick, not a reader feature.
Expand Down
4 changes: 2 additions & 2 deletions contracts/shapes.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"id": "document",
"pickWhen": "Plans, specs, briefings, essays, brainstorms, meeting notes — prose-heavy where you'd read paragraph-to-paragraph",
"register": "reading",
"requiredFeatures": ["table", "copy_as_prompt"],
"requiredFeatures": [],
"example": "examples/document.html",
"href": "document.html",
"reference": "references/shapes/document.md"
Expand Down Expand Up @@ -76,7 +76,7 @@
"id": "editorial",
"pickWhen": "Argument-driven long-form where the reader absorbs a sustained position",
"register": "reading",
"requiredFeatures": ["search", "copy_as_prompt"],
"requiredFeatures": ["search", "copy_as_prompt", "thesis"],
"example": "examples/editorial.html",
"href": "editorial.html",
"reference": "references/shapes/editorial.md"
Expand Down
2 changes: 1 addition & 1 deletion examples/checklist.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-shape="document">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
Expand Down
2 changes: 1 addition & 1 deletion examples/comparison.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-shape="comparison">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Expand Down
2 changes: 1 addition & 1 deletion examples/dashboard.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-shape="dashboard">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Expand Down
2 changes: 1 addition & 1 deletion examples/deck-review.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" data-shape="deck-review">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
Expand Down
2 changes: 1 addition & 1 deletion examples/developer.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-shape="developer">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Expand Down
2 changes: 1 addition & 1 deletion examples/document.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-shape="document">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Expand Down
2 changes: 1 addition & 1 deletion examples/editorial.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-shape="editorial">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Expand Down
2 changes: 1 addition & 1 deletion examples/execution-log.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" data-shape="execution-log">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
Expand Down
2 changes: 1 addition & 1 deletion examples/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" data-shape="document">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
Expand Down
2 changes: 1 addition & 1 deletion examples/network-map.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-shape="network-map">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Expand Down
2 changes: 1 addition & 1 deletion examples/podcast-transcript.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" data-shape="podcast">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
Expand Down
2 changes: 1 addition & 1 deletion examples/podcast.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" data-shape="podcast">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
Expand Down
2 changes: 1 addition & 1 deletion examples/primitives.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" data-shape="document">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
Expand Down
2 changes: 1 addition & 1 deletion examples/runbook.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-shape="runbook">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Expand Down
2 changes: 1 addition & 1 deletion examples/timeline.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-shape="timeline">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Expand Down
2 changes: 1 addition & 1 deletion examples/triage-board.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-shape="triage-board">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Expand Down
2 changes: 2 additions & 0 deletions scripts/check-contracts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const FEATURE_KEYS = new Set([
"toggle",
"textarea",
"cross_highlight",
"thesis",
"hide_sidebar",
]);
const NOT_SHAPES = new Set(["checklist", "podcast-transcript"]);
const STALE_COUNTS = [
Expand Down
7 changes: 7 additions & 0 deletions scripts/lib/html-checks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ export const FEATURES = {
cross_highlight: /scrollIntoView|IntersectionObserver|classList\.(?:add|toggle)\(/i,
};

// Shape-only detectors. Not part of the ≥3 feature floor. A declared shape
// can require these so a dashboard tagged editorial still fails.
export const SHAPE_FEATURES = {
thesis: /class=["'][^"']*\bthesis\b/i,
hide_sidebar: /hide sidebar|inspector-hidden|hide-sidebar/i,
};

// Resource-LOADING external refs only. Anchor href="https://" is navigation, allowed.
export const EXTERNAL_RESOURCE = [
/<script[^>]+src=['"]https?:/i,
Expand Down
26 changes: 25 additions & 1 deletion scripts/lint-artifact.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,15 @@
// stdlib only, no dependencies.

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";
import { REQUIRED_META, FEATURES, EXTERNAL_RESOURCE, hasMeta, attrValue, hasAccessibleName } from "./lib/html-checks.mjs";
import { REQUIRED_META, FEATURES, SHAPE_FEATURES, EXTERNAL_RESOURCE, hasMeta, attrValue, hasAccessibleName } from "./lib/html-checks.mjs";

const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const SHAPE_REGISTRY = JSON.parse(fs.readFileSync(path.join(REPO, "contracts/shapes.json"), "utf8"));
const SHAPE_BY_ID = Object.fromEntries(SHAPE_REGISTRY.shapes.map((s) => [s.id, s]));
const FEATURE_DETECTORS = { ...FEATURES, ...SHAPE_FEATURES };

const argv = process.argv.slice(2);
const opts = {
Expand Down Expand Up @@ -96,6 +103,23 @@ function lintOne(file) {
if (!opts.reference && features.length < 3)
fails.push(`only ${features.length} HTML-native feature(s) detected (${features.join(", ") || "none"}); need ≥3 — this reads as styled prose, not an artifact`);

// --- declared shape (read <html data-shape> only, never gallery-card attrs) ---
if (!opts.reference) {
const htmlTag = html.match(/<html\b[^>]*>/i)?.[0] || "";
const declared = attrValue(htmlTag, "data-shape");
if (!declared) {
fails.push('missing <html data-shape="…"> — set it to a contracts/shapes.json id');
} else if (!SHAPE_BY_ID[declared]) {
fails.push(`data-shape="${declared}" is not a contracts/shapes.json id`);
} else {
for (const key of SHAPE_BY_ID[declared].requiredFeatures || []) {
const rx = FEATURE_DETECTORS[key];
if (!rx) fails.push(`data-shape="${declared}" requires unknown feature ${key}`);
else if (!rx.test(html)) fails.push(`data-shape="${declared}" is missing required feature ${key}`);
}
}
}

// --- copy-as-prompt contract ---
// Trigger on the JS identifiers of a real control, or on "copy as prompt"
// INSIDE a <button> element (mirrors review-contracts.mjs's button-context
Expand Down
41 changes: 38 additions & 3 deletions tests/test_linters.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def test_lint_artifact_passes_on_example():


def test_lint_artifact_passes_on_scatter_primitive():
r = _run("scripts/lint-artifact.mjs", "examples/primitives/10-scatter.html")
r = _run("scripts/lint-artifact.mjs", "--reference", "examples/primitives/10-scatter.html")
assert r.returncode == 0, r.stdout + r.stderr


Expand Down Expand Up @@ -125,7 +125,7 @@ def test_lint_artifact_flags_placeholder_path(tmp_path):
def test_lint_artifact_longform_floor(tmp_path):
small = tmp_path / "small.html"
small.write_text(
'<!doctype html><html lang="en"><head>'
'<!doctype html><html lang="en" data-shape="document"><head>'
'<meta name="viewport" content="width=device-width, initial-scale=1">'
'<link rel="icon" href="data:,">'
'<meta name="description" content="x">'
Expand Down Expand Up @@ -230,7 +230,7 @@ def test_lint_artifact_reference_flag_skips_feature_floor():


_META_HEAD = (
'<!doctype html><html lang="en"><head>'
'<!doctype html><html lang="en" data-shape="document"><head>'
'<meta name="viewport" content="width=device-width, initial-scale=1">'
'<link rel="icon" href="data:,">'
'<meta name="description" content="x">'
Expand Down Expand Up @@ -420,6 +420,41 @@ def test_gallery_pages_pass_the_full_artifact_gate():
assert feat in r.stdout, f"{feat} should be detected in the rebuilt galleries"


def test_lint_artifact_editorial_table_fixture_fails_declared_shape(tmp_path):
"""Omega-3 shaped table tagged editorial, without editorial required
features, must fail and name the missing feature."""
bad = tmp_path / "omega3.html"
bad.write_text(
'<!doctype html><html lang="en" data-shape="editorial"><head>'
'<meta name="viewport" content="width=device-width, initial-scale=1">'
'<link rel="icon" href="data:,">'
'<meta name="description" content="x">'
'<meta property="og:title" content="x">'
'<meta property="og:description" content="x">'
'<meta property="og:type" content="article">'
'<meta property="og:site_name" content="x">'
'<meta name="twitter:card" content="summary">'
"</head><body>"
"<table><tr><td>EPA</td><td>1860</td></tr></table>"
"<article class=\"editorial\"><p>An essay about supplements.</p></article>"
"</body></html>"
)
r = _run("scripts/lint-artifact.mjs", str(bad))
assert r.returncode == 1
assert "search" in r.stdout or "copy_as_prompt" in r.stdout or "thesis" in r.stdout


def test_lint_artifact_rejects_invented_shape(tmp_path):
bad = tmp_path / "invented.html"
bad.write_text(
_META_HEAD.replace('data-shape="document"', 'data-shape="not-a-shape"')
+ '<svg></svg><input type="search"><table></table></body></html>'
)
r = _run("scripts/lint-artifact.mjs", str(bad))
assert r.returncode == 1
assert "contracts/shapes.json" in r.stdout


def test_review_contracts_copy_prompt_trigger_is_per_button(tmp_path):
"""review-contracts.mjs used a `<button[\\s\\S]*?copy as prompt` regex that
spanned unrelated elements: any button anywhere + a prose mention of the
Expand Down
Loading