From a7fc509e685befd2ed7d92a5beb24d9ada1ca2d7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 4 Oct 2026 21:01:13 +0000 Subject: [PATCH] feat(lint-artifact): require html data-shape from the registry The lint reads data-shape on the html element only. A value that is not a contracts/shapes.json id fails. Editorial requires thesis so a dashboard tagged editorial still fails. Co-authored-by: Todd Dailey --- SKILL.md | 2 +- contracts/shapes.json | 4 ++-- examples/checklist.html | 2 +- examples/comparison.html | 2 +- examples/dashboard.html | 2 +- examples/deck-review.html | 2 +- examples/developer.html | 2 +- examples/document.html | 2 +- examples/editorial.html | 2 +- examples/execution-log.html | 2 +- examples/index.html | 2 +- examples/network-map.html | 2 +- examples/podcast-transcript.html | 2 +- examples/podcast.html | 2 +- examples/primitives.html | 2 +- examples/runbook.html | 2 +- examples/timeline.html | 2 +- examples/triage-board.html | 2 +- scripts/check-contracts.mjs | 2 ++ scripts/lib/html-checks.mjs | 7 ++++++ scripts/lint-artifact.mjs | 26 +++++++++++++++++++- tests/test_linters.py | 41 +++++++++++++++++++++++++++++--- 22 files changed, 91 insertions(+), 23 deletions(-) diff --git a/SKILL.md b/SKILL.md index 07e6905..7acc771 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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 `` (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. `` 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. diff --git a/contracts/shapes.json b/contracts/shapes.json index 0f1f11b..59afa27 100644 --- a/contracts/shapes.json +++ b/contracts/shapes.json @@ -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" @@ -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" diff --git a/examples/checklist.html b/examples/checklist.html index 4427628..2f3d887 100644 --- a/examples/checklist.html +++ b/examples/checklist.html @@ -1,5 +1,5 @@ - + diff --git a/examples/comparison.html b/examples/comparison.html index a82d1a0..9c17307 100644 --- a/examples/comparison.html +++ b/examples/comparison.html @@ -1,5 +1,5 @@ - + diff --git a/examples/dashboard.html b/examples/dashboard.html index 7a2011e..5a67838 100644 --- a/examples/dashboard.html +++ b/examples/dashboard.html @@ -1,5 +1,5 @@ - + diff --git a/examples/deck-review.html b/examples/deck-review.html index a684b09..fe798c3 100644 --- a/examples/deck-review.html +++ b/examples/deck-review.html @@ -1,5 +1,5 @@ - + diff --git a/examples/developer.html b/examples/developer.html index 8588a01..4363a19 100644 --- a/examples/developer.html +++ b/examples/developer.html @@ -1,5 +1,5 @@ - + diff --git a/examples/document.html b/examples/document.html index 52016bd..1ab4433 100644 --- a/examples/document.html +++ b/examples/document.html @@ -1,5 +1,5 @@ - + diff --git a/examples/editorial.html b/examples/editorial.html index 4400393..b4d6cbf 100644 --- a/examples/editorial.html +++ b/examples/editorial.html @@ -1,5 +1,5 @@ - + diff --git a/examples/execution-log.html b/examples/execution-log.html index 8db23ac..dea7ab7 100644 --- a/examples/execution-log.html +++ b/examples/execution-log.html @@ -1,5 +1,5 @@ - + diff --git a/examples/index.html b/examples/index.html index 715654f..01bab48 100644 --- a/examples/index.html +++ b/examples/index.html @@ -1,5 +1,5 @@ - + diff --git a/examples/network-map.html b/examples/network-map.html index 6c7c250..de864ab 100644 --- a/examples/network-map.html +++ b/examples/network-map.html @@ -1,5 +1,5 @@ - + diff --git a/examples/podcast-transcript.html b/examples/podcast-transcript.html index 9a1af21..5c97e73 100644 --- a/examples/podcast-transcript.html +++ b/examples/podcast-transcript.html @@ -1,5 +1,5 @@ - + diff --git a/examples/podcast.html b/examples/podcast.html index bb34e5a..7cf4fc7 100644 --- a/examples/podcast.html +++ b/examples/podcast.html @@ -1,5 +1,5 @@ - + diff --git a/examples/primitives.html b/examples/primitives.html index 353ded9..d4c595d 100644 --- a/examples/primitives.html +++ b/examples/primitives.html @@ -1,5 +1,5 @@ - + diff --git a/examples/runbook.html b/examples/runbook.html index c657d62..e7b022c 100644 --- a/examples/runbook.html +++ b/examples/runbook.html @@ -1,5 +1,5 @@ - + diff --git a/examples/timeline.html b/examples/timeline.html index a99ea9b..e057642 100644 --- a/examples/timeline.html +++ b/examples/timeline.html @@ -1,5 +1,5 @@ - + diff --git a/examples/triage-board.html b/examples/triage-board.html index c4826e4..bb9418f 100644 --- a/examples/triage-board.html +++ b/examples/triage-board.html @@ -1,5 +1,5 @@ - + diff --git a/scripts/check-contracts.mjs b/scripts/check-contracts.mjs index 686a7fe..15fcd40 100644 --- a/scripts/check-contracts.mjs +++ b/scripts/check-contracts.mjs @@ -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 = [ diff --git a/scripts/lib/html-checks.mjs b/scripts/lib/html-checks.mjs index 45e45c6..cc670a2 100644 --- a/scripts/lib/html-checks.mjs +++ b/scripts/lib/html-checks.mjs @@ -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 = [ /]+src=['"]https?:/i, diff --git a/scripts/lint-artifact.mjs b/scripts/lint-artifact.mjs index 0b244cc..eec5ab0 100644 --- a/scripts/lint-artifact.mjs +++ b/scripts/lint-artifact.mjs @@ -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 = { @@ -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 only, never gallery-card attrs) --- + if (!opts.reference) { + const htmlTag = html.match(/]*>/i)?.[0] || ""; + const declared = attrValue(htmlTag, "data-shape"); + if (!declared) { + fails.push('missing — 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