@domphy/doctor 0.17.0 → 0.18.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/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
- "use strict";var x=Object.defineProperty;var V=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var E=(e,t)=>{for(var n in t)x(e,n,{get:t[n],enumerable:!0})},F=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of I(t))!C.call(e,a)&&a!==n&&x(e,a,{get:()=>t[a],enumerable:!(r=V(t,a))||r.enumerable});return e};var M=e=>F(x({},"__esModule",{value:!0}),e);var ee={};E(ee,{diagnose:()=>T,fix:()=>O,format:()=>_,validate:()=>S});module.exports=M(ee);var h=require("@domphy/core"),g=require("@domphy/palette"),z=new Set([...h.HtmlTags,...h.SvgTags]),H=new Set(h.VoidTags),G=new Set(["$","style","_key","_portal","_context","_metadata"]),P=new Set(["fontSize","lineHeight","fontWeight","letterSpacing","fontFamily","textDecoration"]),j=new Set(["color","backgroundColor","background","borderColor","border","outlineColor","outline","fill","stroke"]),B=/#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?)\s*\(/,W=new Set(["margin","marginTop","marginRight","marginBottom","marginLeft","marginInline","marginBlock","marginInlineStart","marginInlineEnd","marginBlockStart","marginBlockEnd","padding","paddingTop","paddingRight","paddingBottom","paddingLeft","paddingInline","paddingBlock","paddingInlineStart","paddingInlineEnd","paddingBlockStart","paddingBlockEnd","gap","rowGap","columnGap"]),U=/^(\d+(?:\.\d+)?)(rem|em|px)$/;function w(e){let t=e.match(/^(increase|decrease|shift)-(\d+)$/);return t?{family:t[1],n:parseInt(t[2],10)}:null}function K(e){if(e==="inherit"||e==="base"||/^-?\d+$/.test(e))return!0;let t=w(e);return t?t.n<=17:!1}function Y(e){try{let t=e.trim(),n;if(t.startsWith("#")){let i=t;if(i.length===9&&(i=i.slice(0,7)),i.length===5&&(i=i.slice(0,4)),i.length===4&&(i=`#${i[1]}${i[1]}${i[2]}${i[2]}${i[3]}${i[3]}`),i.length!==7)return null;n=(0,g.hexToRgb)(i)}else if(/^rgba?\s*\(/.test(t))n=(0,g.cssRgbToRgb)(t);else return null;let r=(0,g.rgbToLab)(n),a=(0,g.labToLch)(r);return[a[0],a[1],a[2]]}catch(t){return null}}function q(e){let[t,n,r]=e,a=Math.round((t-50)/10),i=Math.max(-9,Math.min(9,a)),u;Math.abs(i)<=1?u='"base"':i<0?u=`"decrease-${Math.abs(i)}"`:u=`"increase-${i}"`;let l;return n<12?l="neutral":r<30||r>=330?l="error":r<75?l="warning":r<165?l="success":(r<265,l="primary"),`(l) => themeColor(l, ${u}, "${l}") [perceptual LCH L=${Math.round(t)} C=${Math.round(n)} h=${Math.round(r)}\xB0]`}function J(e,t){let n=U.exec(t);if(!n)return null;let r=parseFloat(n[1]),a=n[2],i;return a==="rem"||a==="em"?i=Math.round(r*4):i=Math.round(r/4),i<=0?null:`${e}: themeSpacing(${i}) \u2014 themeSpacing(n)=n/4em, so ${i}/4=${i/4}em \u2248 ${t} at default density`}function v(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function N(e){for(let t in e)if(z.has(t))return t}function T(e,t={}){let n=[];return $(e,"",n,!1,t.runReactive!==!1),n}function $(e,t,n,r,a){var R;if(typeof e=="function"){if(!a)return;let s;try{s=e(()=>{})}catch(c){return}$(s,t,n,!0,a);return}if(Array.isArray(e)){let s=e.filter(o=>v(o)&&N(o));r&&(s.length>1&&s.some(o=>o._key===void 0)&&n.push({rule:"missing-key",severity:"warning",path:t||"(list)",message:"Dynamic list child without `_key` \u2014 reordered/keyed lists need a stable `_key` for correct reconcile.",hint:"Add `_key: <stable id>` to each item produced by the reactive function."}),s.length>1&&s.every((o,f)=>o._key===f)&&n.push({rule:"unstable-key",severity:"warning",path:t||"(list)",message:"Dynamic list `_key` values are the array index (0, 1, 2, \u2026) \u2014 index keys are unstable across reorders/inserts.",hint:"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index."}));let c=new Map;for(let o of s){let f=o._key;if(f==null)continue;let d=`${typeof f}:${String(f)}`;c.set(d,((R=c.get(d))!=null?R:0)+1)}for(let[o,f]of c)if(f>1){let d=o.slice(o.indexOf(":")+1);n.push({rule:"duplicate-key",severity:"error",path:t||"(list)",message:`Duplicate \`_key\` "${d}" among ${f} siblings \u2014 keys must be unique within a list.`,hint:"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant)."})}e.forEach((o,f)=>{$(o,`${t}[${f}]`,n,!1,a)});return}if(!v(e))return;let i=e,u=N(i),l=u?t?`${t} > ${u}`:u:t||"(root)";if(!u){let s=Object.keys(i).filter(c=>!G.has(c)&&!c.startsWith("_on")&&!c.startsWith("on")&&!c.startsWith("data")&&!c.startsWith("aria"));s.length===1&&n.push({rule:"unknown-tag",severity:"warning",path:l,message:`"${s[0]}" is not a known HTML/SVG tag \u2014 likely a typo.`,hint:"An element's first key must be a valid tag (div, button, span, \u2026)."});return}let m=i[u];if(H.has(u)&&m!==null&&m!==void 0&&n.push({rule:"void-content",severity:"error",path:l,message:`Void tag "${u}" must have null content (got ${Array.isArray(m)?"array":typeof m}).`,hint:`Write { ${u}: null, \u2026 } and put attributes as sibling keys.`}),v(i.style)){let s=i.style;for(let c in s){let o=s[c];if(P.has(c)&&typeof o!="function"&&n.push({rule:"inline-typography",severity:"warning",path:l,message:`Inline \`${c}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."}),j.has(c)&&typeof o=="string"&&B.test(o)){let f=Y(o),d=f?q(f):"(l) => themeColor(l, tone, colorName)";n.push({rule:"raw-theme-value",severity:"info",path:l,message:`Inline \`${c}\` uses a literal color (${o}).`,hint:`Prefer a theme token \u2014 ${d} \u2014 so theming and dark mode apply.`})}if(W.has(c)&&typeof o=="string"){let f=J(c,o);f&&n.push({rule:"raw-spacing-value",severity:"info",path:l,message:`Inline \`${c}: "${o}"\` uses a literal spacing value.`,hint:`Prefer themeSpacing() for theme density: ${f}`})}}}let y=i.dataTone;if(typeof y=="string")if(!K(y))n.push({rule:"unknown-tone",severity:"warning",path:l,message:`\`dataTone\` "${y}" is not a valid tone.`,hint:'Use "inherit", "base", a number, or "shift-N"/"increase-N"/"decrease-N" with N \u2264 17 (the ramp has 18 steps). Words like "surface"/"text" are not tones.'});else{let s=w(y);(s==null?void 0:s.family)==="shift"&&s.n>=4&&s.n<=13&&n.push({rule:"middle-surface-anchor",severity:"warning",path:l,message:`\`dataTone: "${y}"\` uses a mid-ramp surface anchor (steps 4\u201313). Child tones derived from this surface may clamp and collapse contrast.`,hint:"Prefer edge anchors: shift-0\u20133 for light surfaces, shift-14\u201317 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions."})}let k=i.dataDensity;if(typeof k=="string"&&k!=="inherit"){let s=w(k);!s||s.family==="shift"?n.push({rule:"unknown-density",severity:"warning",path:l,message:`\`dataDensity\` "${k}" is not a valid density offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20134. "shift-" is not valid for density.'}):s.n>4&&n.push({rule:"unknown-density",severity:"error",path:l,message:`\`dataDensity\` "${k}" N=${s.n} is out of range \u2014 the density scale has 5 steps (max offset: 4).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 4. Density factors: [0.75, 1, 1.5, 2, 2.5].'})}let b=i.dataSize;if(typeof b=="string"&&b!=="inherit"){let s=w(b);!s||s.family==="shift"?n.push({rule:"unknown-size",severity:"warning",path:l,message:`\`dataSize\` "${b}" is not a valid size offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20137. "shift-" is not valid for size.'}):s.n>7&&n.push({rule:"unknown-size",severity:"error",path:l,message:`\`dataSize\` "${b}" N=${s.n} is out of range \u2014 the size scale has 8 steps (max offset: 7).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 7.'})}$(m,l,n,!1,a)}function S(e,t={}){let n=T(e,t),r={error:0,warning:0,info:0,total:n.length};for(let a of n)r[a.severity]+=1;return{ok:r.error===0,issues:n,summary:r}}function _(e){if(e.length===0)return"\u2713 No issues found.";let t=n=>n==="error"?"\u2717":n==="warning"?"\u26A0":"i";return e.map(n=>`${t(n.severity)} [${n.rule}] ${n.path}
1
+ "use strict";var D=Object.defineProperty;var V=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var E=Object.prototype.hasOwnProperty;var F=(e,t)=>{for(var n in t)D(e,n,{get:t[n],enumerable:!0})},M=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of C(t))!E.call(e,a)&&a!==n&&D(e,a,{get:()=>t[a],enumerable:!(r=V(t,a))||r.enumerable});return e};var z=e=>M(D({},"__esModule",{value:!0}),e);var Z={};F(Z,{diagnose:()=>T,fix:()=>_,format:()=>N,validate:()=>S});module.exports=z(Z);var g=require("@domphy/palette");var p=require("@domphy/core"),P=new Set([...p.HtmlTags,...p.SvgTags]),w=new Set(p.VoidTags);function d(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function k(e){for(let t in e)if(P.has(t))return t}var B=new Set(["$","style","_key","_portal","_context","_metadata"]),H=new Set(["fontSize","lineHeight","fontWeight","letterSpacing","fontFamily","textDecoration"]),j=new Set(["color","backgroundColor","background","borderColor","border","outlineColor","outline","fill","stroke"]),G=/#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?)\s*\(/,W=new Set(["margin","marginTop","marginRight","marginBottom","marginLeft","marginInline","marginBlock","marginInlineStart","marginInlineEnd","marginBlockStart","marginBlockEnd","padding","paddingTop","paddingRight","paddingBottom","paddingLeft","paddingInline","paddingBlock","paddingInlineStart","paddingInlineEnd","paddingBlockStart","paddingBlockEnd","gap","rowGap","columnGap"]),U=/^(\d+(?:\.\d+)?)(rem|em|px)$/;function x(e){let t=e.match(/^(increase|decrease|shift)-(\d+)$/);return t?{family:t[1],n:parseInt(t[2],10)}:null}function K(e){if(e==="inherit"||e==="base"||/^-?\d+$/.test(e))return!0;let t=x(e);return t?t.n<=17:!1}function Y(e){try{let t=e.trim(),n;if(t.startsWith("#")){let i=t;if(i.length===9&&(i=i.slice(0,7)),i.length===5&&(i=i.slice(0,4)),i.length===4&&(i=`#${i[1]}${i[1]}${i[2]}${i[2]}${i[3]}${i[3]}`),i.length!==7)return null;n=(0,g.hexToRgb)(i)}else if(/^rgba?\s*\(/.test(t))n=(0,g.cssRgbToRgb)(t);else return null;let r=(0,g.rgbToLab)(n),a=(0,g.labToLch)(r);return[a[0],a[1],a[2]]}catch(t){return null}}var q=/#[0-9a-fA-F]{3,8}\b|rgba?\s*\([^)]*\)/;function J(e){let t=q.exec(e);return t?t[0]:null}function Q(e){let[t,n,r]=e,a=Math.round((t-50)/10),i=Math.max(-9,Math.min(9,a)),u;Math.abs(i)<=1?u='"base"':i<0?u=`"decrease-${Math.abs(i)}"`:u=`"increase-${i}"`;let l;return n<12?l="neutral":r<30||r>=330?l="error":r<75?l="warning":r<165?l="success":(r<265,l="primary"),`(l) => themeColor(l, ${u}, "${l}") [perceptual LCH L=${Math.round(t)} C=${Math.round(n)} h=${Math.round(r)}\xB0]`}function X(e,t){let n=U.exec(t);if(!n)return null;let r=parseFloat(n[1]),a=n[2],i;return a==="rem"||a==="em"?i=Math.round(r*4):i=Math.round(r/4),i<=0?null:`${e}: themeSpacing(${i}) \u2014 themeSpacing(n)=n/4em, so ${i}/4=${i/4}em \u2248 ${t} at default density`}function T(e,t={}){let n=[];return v(e,"",n,!1,t.runReactive!==!1),n}function v(e,t,n,r,a){var O,L;if(typeof e=="function"){if(!a)return;let s;try{s=e(()=>{})}catch(c){return}v(s,t,n,!0,a);return}if(Array.isArray(e)){let s=e.filter(o=>d(o)&&k(o));r&&(s.length>1&&s.some(o=>o._key===void 0)&&n.push({rule:"missing-key",severity:"warning",path:t||"(list)",message:"Dynamic list child without `_key` \u2014 reordered/keyed lists need a stable `_key` for correct reconcile.",hint:"Add `_key: <stable id>` to each item produced by the reactive function."}),s.length>1&&s.every((o,f)=>o._key===f)&&n.push({rule:"unstable-key",severity:"warning",path:t||"(list)",message:"Dynamic list `_key` values are the array index (0, 1, 2, \u2026) \u2014 index keys are unstable across reorders/inserts.",hint:"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index."}));let c=new Map;for(let o of s){let f=o._key;if(f==null)continue;let h=`${typeof f}:${String(f)}`;c.set(h,((O=c.get(h))!=null?O:0)+1)}for(let[o,f]of c)if(f>1){let h=o.slice(o.indexOf(":")+1);n.push({rule:"duplicate-key",severity:"error",path:t||"(list)",message:`Duplicate \`_key\` "${h}" among ${f} siblings \u2014 keys must be unique within a list.`,hint:"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant)."})}e.forEach((o,f)=>{v(o,`${t}[${f}]`,n,!1,a)});return}if(!d(e))return;let i=e,u=k(i),l=u?t?`${t} > ${u}`:u:t||"(root)";if(!u){let s=Object.keys(i).filter(c=>!B.has(c)&&!c.startsWith("_on")&&!c.startsWith("on")&&!c.startsWith("data")&&!c.startsWith("aria"));s.length===1&&n.push({rule:"unknown-tag",severity:"warning",path:l,message:`"${s[0]}" is not a known HTML/SVG tag \u2014 likely a typo.`,hint:"An element's first key must be a valid tag (div, button, span, \u2026)."});return}let m=i[u];if(w.has(u)&&m!==null&&m!==void 0&&n.push({rule:"void-content",severity:"error",path:l,message:`Void tag "${u}" must have null content (got ${Array.isArray(m)?"array":typeof m}).`,hint:`Write { ${u}: null, \u2026 } and put attributes as sibling keys.`}),d(i.style)){let s=i.style;for(let c in s){let o=s[c];if(H.has(c)&&typeof o!="function"&&n.push({rule:"inline-typography",severity:"warning",path:l,message:`Inline \`${c}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."}),j.has(c)&&typeof o=="string"&&G.test(o)){let f=(L=J(o))!=null?L:o,h=Y(f),I=h?Q(h):"(l) => themeColor(l, tone, colorName)";n.push({rule:"raw-theme-value",severity:"info",path:l,message:`Inline \`${c}\` uses a literal color (${o}).`,hint:`Prefer a theme token \u2014 ${I} \u2014 so theming and dark mode apply.`})}if(W.has(c)&&typeof o=="string"){let f=X(c,o);f&&n.push({rule:"raw-spacing-value",severity:"info",path:l,message:`Inline \`${c}: "${o}"\` uses a literal spacing value.`,hint:`Prefer themeSpacing() for theme density: ${f}`})}}}let y=i.dataTone;if(typeof y=="string")if(!K(y))n.push({rule:"unknown-tone",severity:"warning",path:l,message:`\`dataTone\` "${y}" is not a valid tone.`,hint:'Use "inherit", "base", a number, or "shift-N"/"increase-N"/"decrease-N" with N \u2264 17 (the ramp has 18 steps). Words like "surface"/"text" are not tones.'});else{let s=x(y);(s==null?void 0:s.family)==="shift"&&s.n>=4&&s.n<=13&&n.push({rule:"middle-surface-anchor",severity:"warning",path:l,message:`\`dataTone: "${y}"\` uses a mid-ramp surface anchor (steps 4\u201313). Child tones derived from this surface may clamp and collapse contrast.`,hint:"Prefer edge anchors: shift-0\u20133 for light surfaces, shift-14\u201317 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions."})}let b=i.dataDensity;if(typeof b=="string"&&b!=="inherit"){let s=x(b);!s||s.family==="shift"?n.push({rule:"unknown-density",severity:"warning",path:l,message:`\`dataDensity\` "${b}" is not a valid density offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20134. "shift-" is not valid for density.'}):s.n>4&&n.push({rule:"unknown-density",severity:"error",path:l,message:`\`dataDensity\` "${b}" N=${s.n} is out of range \u2014 the density scale has 5 steps (max offset: 4).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 4. Density factors: [0.75, 1, 1.5, 2, 2.5].'})}let $=i.dataSize;if(typeof $=="string"&&$!=="inherit"){let s=x($);!s||s.family==="shift"?n.push({rule:"unknown-size",severity:"warning",path:l,message:`\`dataSize\` "${$}" is not a valid size offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20137. "shift-" is not valid for size.'}):s.n>7&&n.push({rule:"unknown-size",severity:"error",path:l,message:`\`dataSize\` "${$}" N=${s.n} is out of range \u2014 the size scale has 8 steps (max offset: 7).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 7.'})}v(m,l,n,!1,a)}function S(e,t={}){let n=T(e,t),r={error:0,warning:0,info:0,total:n.length};for(let a of n)r[a.severity]+=1;return{ok:r.error===0,issues:n,summary:r}}function N(e){if(e.length===0)return"\u2713 No issues found.";let t=n=>n==="error"?"\u2717":n==="warning"?"\u26A0":"i";return e.map(n=>`${t(n.severity)} [${n.rule}] ${n.path}
2
2
  ${n.message}${n.hint?`
3
3
  \u2192 ${n.hint}`:""}`).join(`
4
- `)}var p=require("@domphy/core");var Q=new Set([...p.HtmlTags,...p.SvgTags]),X=new Set(p.VoidTags);function L(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Z(e){for(let t in e)if(Q.has(t))return t}function A(e){if(Array.isArray(e))return e.map(A);if(L(e)){let t={};for(let n in e)t[n]=A(e[n]);return t}return e}function O(e,t={}){let n=A(e),r=[];return D(n,"",r),{tree:n,applied:r,report:S(n,t)}}function D(e,t,n){if(Array.isArray(e)){for(let[i,u]of e.entries())D(u,`${t}[${i}]`,n);return}if(!L(e))return;let r=Z(e);if(!r)return;let a=t?`${t} > ${r}`:r;X.has(r)&&e[r]!==null&&e[r]!==void 0&&(e[r]=null,n.push({rule:"void-content",path:a,message:`Void tag <${r}> cannot have content \u2014 cleared to null.`})),D(e[r],a,n)}0&&(module.exports={diagnose,fix,format,validate});
4
+ `)}function A(e){if(Array.isArray(e))return e.map(A);if(d(e)){let t={};for(let n in e)t[n]=A(e[n]);return t}return e}function _(e,t={}){let n=A(e),r=[];return R(n,"",r),{tree:n,applied:r,report:S(n,t)}}function R(e,t,n){if(Array.isArray(e)){for(let[i,u]of e.entries())R(u,`${t}[${i}]`,n);return}if(!d(e))return;let r=k(e);if(!r)return;let a=t?`${t} > ${r}`:r;w.has(r)&&e[r]!==null&&e[r]!==void 0&&(e[r]=null,n.push({rule:"void-content",path:a,message:`Void tag <${r}> cannot have content \u2014 cleared to null.`})),R(e[r],a,n)}0&&(module.exports={diagnose,fix,format,validate});
5
5
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/diagnose.ts","../src/fix.ts"],"sourcesContent":["// @domphy/doctor — static analyzer for Domphy element trees. Catches\n// non-idiomatic patterns (inline typography, literal theme colors, unknown\n// tones, void-tag content, missing/duplicate/unstable _key on lists, unknown\n// tags) so humans and AI agents get a feedback loop to self-correct generated\n// code. `validate()` is the aggregate entry point.\n\nexport type {\n DiagnoseOptions,\n Diagnostic,\n Severity,\n ValidationReport,\n ValidationSummary,\n} from \"./diagnose.js\";\nexport { diagnose, format, validate } from \"./diagnose.js\";\nexport type { AppliedFix, FixResult } from \"./fix.js\";\nexport { fix } from \"./fix.js\";\n","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\nimport { cssRgbToRgb, hexToRgb, labToLch, rgbToLab } from \"@domphy/palette\";\n\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport interface Diagnostic {\n /** Rule id, e.g. \"inline-typography\". */\n rule: string;\n severity: Severity;\n /** Human path to the offending node, e.g. \"div > ul > li\". */\n path: string;\n message: string;\n /** How to fix it. */\n hint?: string;\n}\n\nexport interface DiagnoseOptions {\n /**\n * Invoke reactive content functions `(listener) => …` with a no-op listener to\n * analyze their output (catches missing `_key` in dynamic lists). Default true.\n * Set false if your reactive functions have side effects.\n */\n runReactive?: boolean;\n}\n\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\nconst VOID = new Set<string>(VoidTags);\nconst RESERVED = new Set([\n \"$\",\n \"style\",\n \"_key\",\n \"_portal\",\n \"_context\",\n \"_metadata\",\n]);\n\n// Typography style properties that must not be set inline — use patches instead.\n// Expanded from bench data: fontFamily + textDecoration were missing and caused\n// agents to write { style: { fontFamily: \"...\" } } without correction.\nconst TYPOGRAPHY_STYLE = new Set([\n \"fontSize\",\n \"lineHeight\",\n \"fontWeight\",\n \"letterSpacing\",\n \"fontFamily\",\n \"textDecoration\",\n]);\n\n// Color-bearing style props that should resolve through a theme token rather\n// than a literal value, so theming and dark mode apply. Shorthands\n// (background/border/outline) are included because they often carry a color.\nconst COLOR_STYLE = new Set([\n \"color\",\n \"backgroundColor\",\n \"background\",\n \"borderColor\",\n \"border\",\n \"outlineColor\",\n \"outline\",\n \"fill\",\n \"stroke\",\n]);\n\n// A literal color value: hex (#rgb … #rrggbbaa) or an rgb()/rgba()/hsl()/hsla()\n// function. Keywords like transparent/currentColor/inherit are intentionally\n// allowed — they carry no theme meaning.\nconst LITERAL_COLOR = /#[0-9a-fA-F]{3,8}\\b|\\b(?:rgba?|hsla?)\\s*\\(/;\n\n// Spacing style properties where literal rem/em/px values should use themeSpacing().\n// These are layout, not typography, but themeSpacing() ensures density consistency.\n// Logical properties (paddingBlock, paddingInline, etc.) are included — they are\n// used in Domphy patches and must also go through themeSpacing() for density scaling.\nconst SPACING_STYLE = new Set([\n \"margin\",\n \"marginTop\",\n \"marginRight\",\n \"marginBottom\",\n \"marginLeft\",\n \"marginInline\",\n \"marginBlock\",\n \"marginInlineStart\",\n \"marginInlineEnd\",\n \"marginBlockStart\",\n \"marginBlockEnd\",\n \"padding\",\n \"paddingTop\",\n \"paddingRight\",\n \"paddingBottom\",\n \"paddingLeft\",\n \"paddingInline\",\n \"paddingBlock\",\n \"paddingInlineStart\",\n \"paddingInlineEnd\",\n \"paddingBlockStart\",\n \"paddingBlockEnd\",\n \"gap\",\n \"rowGap\",\n \"columnGap\",\n]);\n\n// Matches literal spacing values like \"16px\", \"1.5rem\", \"2em\" but not \"auto\",\n// \"inherit\", \"0\" (unitless zero is fine), or computed values.\nconst LITERAL_SPACING = /^(\\d+(?:\\.\\d+)?)(rem|em|px)$/;\n\n// Parses \"increase-N\" / \"decrease-N\" / \"shift-N\" into family + numeric offset.\n// Returns null when the pattern doesn't match (grammar error).\nfunction parseOffset(\n value: string,\n): { family: \"increase\" | \"decrease\" | \"shift\"; n: number } | null {\n const m = value.match(/^(increase|decrease|shift)-(\\d+)$/);\n if (!m) return null;\n return {\n family: m[1] as \"increase\" | \"decrease\" | \"shift\",\n n: parseInt(m[2], 10),\n };\n}\n\n// Valid `dataTone` grammar AND range:\n// \"inherit\", \"base\", a bare integer, or shift-N/increase-N/decrease-N where N ≤ 17.\n// The default Domphy theme has 18 tone steps (0–17). Values with valid grammar\n// but N > 17 are also rejected here so they surface as `unknown-tone` errors.\nfunction isValidTone(value: string): boolean {\n if (value === \"inherit\" || value === \"base\") return true;\n if (/^-?\\d+$/.test(value)) return true;\n const parsed = parseOffset(value);\n if (!parsed) return false;\n return parsed.n <= 17; // tone ramp has 18 steps: 0–17\n}\n\n// ─── Chromametry integration ─────────────────────────────────────────────────\n\n/**\n * Parses a CSS color literal (hex or rgb/rgba) into LCH [L, C, h].\n * Returns null if parsing fails or the format is unsupported (named colors, hsl).\n * Uses @domphy/palette math (CIELAB via D65 reference white).\n */\nfunction parseLiteralToLch(value: string): [number, number, number] | null {\n try {\n const trimmed = value.trim();\n let rgb: number[];\n\n if (trimmed.startsWith(\"#\")) {\n let hex = trimmed;\n if (hex.length === 9) hex = hex.slice(0, 7); // strip alpha #rrggbbaa → #rrggbb\n if (hex.length === 5) hex = hex.slice(0, 4); // strip alpha #rgba → #rgb\n if (hex.length === 4) {\n hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;\n }\n if (hex.length !== 7) return null;\n rgb = hexToRgb(hex);\n } else if (/^rgba?\\s*\\(/.test(trimmed)) {\n rgb = cssRgbToRgb(trimmed);\n } else {\n return null; // hsl, named colors, custom-properties — skip\n }\n\n const lab = rgbToLab(rgb);\n const lch = labToLch(lab);\n return [lch[0], lch[1], lch[2]];\n } catch {\n return null;\n }\n}\n\n/**\n * Converts LCH coordinates into a concrete `themeColor()` call suggestion plus\n * a perceptual description. The tone and color-family are approximations for the\n * default Domphy theme (light, 10 neutral tones, base at mid-lightness).\n */\nfunction buildColorHint(lch: [number, number, number]): string {\n const [L, C, h] = lch;\n\n // Map lightness to a Domphy tone relative to base (~L50).\n // Each step ≈ 10 lightness units — clamp to ±9 (max offset in a 10-step ramp).\n const rawOffset = Math.round((L - 50) / 10);\n const offset = Math.max(-9, Math.min(9, rawOffset));\n let toneStr: string;\n if (Math.abs(offset) <= 1) toneStr = '\"base\"';\n else if (offset < 0) toneStr = `\"decrease-${Math.abs(offset)}\"`;\n else toneStr = `\"increase-${offset}\"`;\n\n // Infer the most likely semantic color family from chroma + hue.\n let colorFamily: string;\n if (C < 12) colorFamily = \"neutral\";\n else if (h < 30 || h >= 330)\n colorFamily = \"error\"; // red spectrum\n else if (h < 75)\n colorFamily = \"warning\"; // orange-yellow\n else if (h < 165)\n colorFamily = \"success\"; // green\n else if (h < 265)\n colorFamily = \"primary\"; // blue-indigo\n else colorFamily = \"primary\"; // violet → treat as primary\n\n return (\n `(l) => themeColor(l, ${toneStr}, \"${colorFamily}\") ` +\n `[perceptual LCH L=${Math.round(L)} C=${Math.round(C)} h=${Math.round(h)}°]`\n );\n}\n\n/**\n * Converts a literal spacing value like \"16px\" / \"1.5rem\" / \"2em\" into a\n * themeSpacing(n) suggestion. themeSpacing(n) = n/4 em, so n=4 → 1em ≈ 16px.\n */\nfunction buildSpacingHint(prop: string, value: string): string | null {\n const match = LITERAL_SPACING.exec(value);\n if (!match) return null;\n const amount = parseFloat(match[1]);\n const unit = match[2];\n let n: number;\n if (unit === \"rem\" || unit === \"em\") {\n n = Math.round(amount * 4);\n } else {\n // px: assume default 16px/rem → 1em = 16px\n n = Math.round(amount / 4);\n }\n if (n <= 0) return null;\n return `${prop}: themeSpacing(${n}) — themeSpacing(n)=n/4em, so ${n}/4=${n / 4}em ≈ ${value} at default density`;\n}\n\n// ─── Tree walkers ─────────────────────────────────────────────────────────────\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction findTag(element: Record<string, unknown>): string | undefined {\n for (const key in element) {\n if (TAGS.has(key)) return key;\n }\n return undefined;\n}\n\n/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */\nexport function diagnose(\n root: unknown,\n options: DiagnoseOptions = {},\n): Diagnostic[] {\n const out: Diagnostic[] = [];\n walk(root, \"\", out, false, options.runReactive !== false);\n return out;\n}\n\nfunction walk(\n node: unknown,\n path: string,\n out: Diagnostic[],\n dynamic: boolean,\n runReactive: boolean,\n): void {\n if (typeof node === \"function\") {\n if (!runReactive) return;\n let result: unknown;\n try {\n result = (node as (listener: unknown) => unknown)(() => {});\n } catch {\n return; // reactive fn threw without a real runtime — skip\n }\n walk(result, path, out, true, runReactive);\n return;\n }\n\n if (Array.isArray(node)) {\n const elementItems = node.filter(\n (child) => isPlainObject(child) && findTag(child),\n ) as Record<string, unknown>[];\n\n if (dynamic) {\n if (\n elementItems.length > 1 &&\n elementItems.some((item) => item._key === undefined)\n ) {\n out.push({\n rule: \"missing-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list child without `_key` — reordered/keyed lists need a stable `_key` for correct reconcile.\",\n hint: \"Add `_key: <stable id>` to each item produced by the reactive function.\",\n });\n }\n\n // unstable-key (heuristic): in a dynamic list every `_key` equals its\n // sibling position (0, 1, 2, …). That is the runtime footprint of\n // `items.map((item, i) => ({ …, _key: i }))` — an array-index key, which\n // defeats the point of keying because keys shift when the list reorders.\n if (\n elementItems.length > 1 &&\n elementItems.every((item, index) => item._key === index)\n ) {\n out.push({\n rule: \"unstable-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list `_key` values are the array index (0, 1, 2, …) — index keys are unstable across reorders/inserts.\",\n hint: \"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index.\",\n });\n }\n }\n\n // duplicate-key: two siblings sharing the same `_key` value break reconcile\n const seenKeys = new Map<string, number>();\n for (const item of elementItems) {\n const key = item._key;\n if (key === undefined || key === null) continue;\n const literalKey = `${typeof key}:${String(key)}`;\n seenKeys.set(literalKey, (seenKeys.get(literalKey) ?? 0) + 1);\n }\n for (const [literalKey, count] of seenKeys) {\n if (count > 1) {\n const value = literalKey.slice(literalKey.indexOf(\":\") + 1);\n out.push({\n rule: \"duplicate-key\",\n severity: \"error\",\n path: path || \"(list)\",\n message: `Duplicate \\`_key\\` \"${value}\" among ${count} siblings — keys must be unique within a list.`,\n hint: \"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant).\",\n });\n }\n }\n\n node.forEach((child, index) => {\n walk(child, `${path}[${index}]`, out, false, runReactive);\n });\n return;\n }\n\n if (!isPlainObject(node)) return;\n\n const element = node;\n const tag = findTag(element);\n const here = tag ? (path ? `${path} > ${tag}` : tag) : path || \"(root)\";\n\n if (!tag) {\n const contentKeys = Object.keys(element).filter(\n (key) =>\n !RESERVED.has(key) &&\n !key.startsWith(\"_on\") &&\n !key.startsWith(\"on\") &&\n !key.startsWith(\"data\") &&\n !key.startsWith(\"aria\"),\n );\n if (contentKeys.length === 1) {\n out.push({\n rule: \"unknown-tag\",\n severity: \"warning\",\n path: here,\n message: `\"${contentKeys[0]}\" is not a known HTML/SVG tag — likely a typo.`,\n hint: \"An element's first key must be a valid tag (div, button, span, …).\",\n });\n }\n return;\n }\n\n const content = element[tag];\n\n if (VOID.has(tag) && content !== null && content !== undefined) {\n out.push({\n rule: \"void-content\",\n severity: \"error\",\n path: here,\n message: `Void tag \"${tag}\" must have null content (got ${Array.isArray(content) ? \"array\" : typeof content}).`,\n hint: `Write { ${tag}: null, … } and put attributes as sibling keys.`,\n });\n }\n\n if (isPlainObject(element.style)) {\n const style = element.style;\n for (const prop in style) {\n const value = style[prop];\n\n // inline-typography: typography properties must come from patches, not\n // inline style. fontFamily and textDecoration were missing from the original\n // set and are added here based on bench data showing persistent violations.\n if (TYPOGRAPHY_STYLE.has(prop) && typeof value !== \"function\") {\n out.push({\n rule: \"inline-typography\",\n severity: \"warning\",\n path: here,\n message: `Inline \\`${prop}\\` — avoid inline typography styles.`,\n hint: \"Use a typography patch (paragraph()/heading()/small()/strong()/…) via $ so the theme owns the type scale.\",\n });\n }\n\n // raw-theme-value: literal color values bypass theming/dark mode.\n // Enhanced with @domphy/palette chromametry: converts the literal color to\n // LCH and suggests the nearest themeColor() call with perceptual coordinates.\n if (\n COLOR_STYLE.has(prop) &&\n typeof value === \"string\" &&\n LITERAL_COLOR.test(value)\n ) {\n const lch = parseLiteralToLch(value);\n const colorHint = lch\n ? buildColorHint(lch)\n : \"(l) => themeColor(l, tone, colorName)\";\n\n out.push({\n rule: \"raw-theme-value\",\n severity: \"info\",\n path: here,\n message: `Inline \\`${prop}\\` uses a literal color (${value}).`,\n hint: `Prefer a theme token — ${colorHint} — so theming and dark mode apply.`,\n });\n }\n\n // raw-spacing-value: literal rem/em/px spacing values should use themeSpacing()\n // to respect the theme's density system. info-severity (soft recommendation).\n if (SPACING_STYLE.has(prop) && typeof value === \"string\") {\n const spacingHint = buildSpacingHint(prop, value);\n if (spacingHint) {\n out.push({\n rule: \"raw-spacing-value\",\n severity: \"info\",\n path: here,\n message: `Inline \\`${prop}: \"${value}\"\\` uses a literal spacing value.`,\n hint: `Prefer themeSpacing() for theme density: ${spacingHint}`,\n });\n }\n }\n }\n }\n\n // unknown-tone: dataTone is not valid grammar, or it's valid grammar but the\n // numeric offset is out of the 18-step ramp range (0–17).\n const dataTone = element.dataTone;\n if (typeof dataTone === \"string\") {\n if (!isValidTone(dataTone)) {\n out.push({\n rule: \"unknown-tone\",\n severity: \"warning\",\n path: here,\n message: `\\`dataTone\\` \"${dataTone}\" is not a valid tone.`,\n hint: 'Use \"inherit\", \"base\", a number, or \"shift-N\"/\"increase-N\"/\"decrease-N\" with N ≤ 17 (the ramp has 18 steps). Words like \"surface\"/\"text\" are not tones.',\n });\n } else {\n // middle-surface-anchor: shift-4 through shift-13 sets a mid-ramp surface\n // anchor. Children's tones may clamp and fold back, collapsing the contrast\n // between background and text. Edge anchors (0–3 light, 14–17 dark) are safe.\n const parsed = parseOffset(dataTone);\n if (parsed?.family === \"shift\" && parsed.n >= 4 && parsed.n <= 13) {\n out.push({\n rule: \"middle-surface-anchor\",\n severity: \"warning\",\n path: here,\n message: `\\`dataTone: \"${dataTone}\"\\` uses a mid-ramp surface anchor (steps 4–13). Child tones derived from this surface may clamp and collapse contrast.`,\n hint: \"Prefer edge anchors: shift-0–3 for light surfaces, shift-14–17 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions.\",\n });\n }\n }\n }\n\n // unknown-density: dataDensity value is invalid grammar or out of the 5-step\n // density range (increase/decrease 0–4; the scale factors are 0.75, 1, 1.5, 2, 2.5).\n const dataDensity = element.dataDensity;\n if (typeof dataDensity === \"string\" && dataDensity !== \"inherit\") {\n const parsed = parseOffset(dataDensity);\n if (!parsed || parsed.family === \"shift\") {\n out.push({\n rule: \"unknown-density\",\n severity: \"warning\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" is not a valid density offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–4. \"shift-\" is not valid for density.',\n });\n } else if (parsed.n > 4) {\n out.push({\n rule: \"unknown-density\",\n severity: \"error\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" N=${parsed.n} is out of range — the density scale has 5 steps (max offset: 4).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 4. Density factors: [0.75, 1, 1.5, 2, 2.5].',\n });\n }\n }\n\n // unknown-size: dataSize value is invalid grammar or out of the 8-step size\n // range (increase/decrease 0–7).\n const dataSize = element.dataSize;\n if (typeof dataSize === \"string\" && dataSize !== \"inherit\") {\n const parsed = parseOffset(dataSize);\n if (!parsed || parsed.family === \"shift\") {\n out.push({\n rule: \"unknown-size\",\n severity: \"warning\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" is not a valid size offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–7. \"shift-\" is not valid for size.',\n });\n } else if (parsed.n > 7) {\n out.push({\n rule: \"unknown-size\",\n severity: \"error\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" N=${parsed.n} is out of range — the size scale has 8 steps (max offset: 7).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 7.',\n });\n }\n }\n\n walk(content, here, out, false, runReactive);\n}\n\n/** Issue counts by severity, plus the grand total. */\nexport interface ValidationSummary {\n error: number;\n warning: number;\n info: number;\n total: number;\n}\n\n/** Structured result of {@link validate}: pass/fail flag, issues, and counts. */\nexport interface ValidationReport {\n /** True when there are no `error`-severity diagnostics. */\n ok: boolean;\n /** Every diagnostic found, across all rules (alias of `diagnose` output). */\n issues: Diagnostic[];\n summary: ValidationSummary;\n}\n\n/**\n * Runs every diagnose rule and returns a structured report (pass/fail flag,\n * the issue list, and counts by severity). `ok` is false when any `error`\n * diagnostic is present; warnings/info do not flip `ok`. Use this as the single\n * programmatic entry point; `diagnose`/`format` remain available for raw access.\n */\nexport function validate(\n root: unknown,\n options: DiagnoseOptions = {},\n): ValidationReport {\n const issues = diagnose(root, options);\n const summary: ValidationSummary = {\n error: 0,\n warning: 0,\n info: 0,\n total: issues.length,\n };\n for (const issue of issues) summary[issue.severity] += 1;\n return { ok: summary.error === 0, issues, summary };\n}\n\n/** Formats diagnostics as a readable report (one line per issue). */\nexport function format(diagnostics: Diagnostic[]): string {\n if (diagnostics.length === 0) return \"✓ No issues found.\";\n const icon = (s: Severity) =>\n s === \"error\" ? \"✗\" : s === \"warning\" ? \"⚠\" : \"i\";\n return diagnostics\n .map(\n (d) =>\n `${icon(d.severity)} [${d.rule}] ${d.path}\\n ${d.message}${d.hint ? `\\n → ${d.hint}` : \"\"}`,\n )\n .join(\"\\n\");\n}\n","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\nimport {\n type DiagnoseOptions,\n type ValidationReport,\n validate,\n} from \"./diagnose.js\";\n\n// Autofix for Domphy element trees. We ONLY apply transforms that are provably\n// lossless — they fix structurally-invalid input without guessing intent. Every\n// other diagnostic (missing/unstable keys, inline typography, literal colors,\n// unknown tones/tags) needs semantic intent the tree does not carry, so applying\n// a \"fix\" would corrupt the author's meaning (e.g. an index key is itself the\n// unstable-key anti-pattern). Those are returned in `report` for the model or a\n// human to resolve. The fixer set is a registry so safe transforms can be added.\n\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\nconst VOID = new Set<string>(VoidTags);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction findTag(element: Record<string, unknown>): string | undefined {\n for (const key in element) {\n if (TAGS.has(key)) return key;\n }\n return undefined;\n}\n\n// Structural clone that preserves functions (reactive `(listener) => …` values)\n// by reference — a JSON clone would drop them. Primitives pass through.\nfunction cloneTree(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(cloneTree);\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const key in value) out[key] = cloneTree(value[key]);\n return out;\n }\n return value;\n}\n\n/** One applied lossless fix. */\nexport interface AppliedFix {\n rule: string;\n /** Human path to the node, e.g. \"div > input\". */\n path: string;\n message: string;\n}\n\n/** Result of {@link fix}: the corrected tree, what was applied, and what remains. */\nexport interface FixResult {\n /** A deep copy of the input with lossless fixes applied (functions preserved). */\n tree: unknown;\n /** The lossless fixes that were applied. */\n applied: AppliedFix[];\n /** validate() run on the fixed tree — `report.issues` are the manual remainder. */\n report: ValidationReport;\n}\n\n/**\n * Applies every provably-lossless fix to a copy of the tree and returns the\n * result plus a fresh validation report. Currently fixes `void-content` (a void\n * tag like input/img/br cannot have children, so its content is set to null).\n * Issues that need intent are left untouched and surface in `report` — this\n * includes `raw-spacing-value` and `raw-theme-value` (require semantic choices)\n * and key rules (require stable identity from data, not the tree shape).\n */\nexport function fix(root: unknown, options: DiagnoseOptions = {}): FixResult {\n const tree = cloneTree(root);\n const applied: AppliedFix[] = [];\n walkFix(tree, \"\", applied);\n return { tree, applied, report: validate(tree, options) };\n}\n\nfunction walkFix(node: unknown, path: string, applied: AppliedFix[]): void {\n if (Array.isArray(node)) {\n for (const [index, child] of node.entries()) {\n walkFix(child, `${path}[${index}]`, applied);\n }\n return;\n }\n if (!isPlainObject(node)) return;\n\n const tag = findTag(node);\n if (!tag) return;\n const here = path ? `${path} > ${tag}` : tag;\n\n // void-content: a void tag renders no children, so any content is invalid and\n // cannot be rendered — clearing it to null is lossless.\n if (VOID.has(tag) && node[tag] !== null && node[tag] !== undefined) {\n node[tag] = null;\n applied.push({\n rule: \"void-content\",\n path: here,\n message: `Void tag <${tag}> cannot have content — cleared to null.`,\n });\n }\n\n walkFix(node[tag], here, applied);\n}\n"],"mappings":"yaAAA,IAAAA,GAAA,GAAAC,EAAAD,GAAA,cAAAE,EAAA,QAAAC,EAAA,WAAAC,EAAA,aAAAC,IAAA,eAAAC,EAAAN,ICAA,IAAAO,EAA4C,wBAC5CC,EAA0D,2BAwBpDC,EAAO,IAAI,IAAY,CAAC,GAAG,WAAU,GAAG,SAAO,CAAC,EAChDC,EAAO,IAAI,IAAY,UAAQ,EAC/BC,EAAW,IAAI,IAAI,CACvB,IACA,QACA,OACA,UACA,WACA,WACF,CAAC,EAKKC,EAAmB,IAAI,IAAI,CAC/B,WACA,aACA,aACA,gBACA,aACA,gBACF,CAAC,EAKKC,EAAc,IAAI,IAAI,CAC1B,QACA,kBACA,aACA,cACA,SACA,eACA,UACA,OACA,QACF,CAAC,EAKKC,EAAgB,6CAMhBC,EAAgB,IAAI,IAAI,CAC5B,SACA,YACA,cACA,eACA,aACA,eACA,cACA,oBACA,kBACA,mBACA,iBACA,UACA,aACA,eACA,gBACA,cACA,gBACA,eACA,qBACA,mBACA,oBACA,kBACA,MACA,SACA,WACF,CAAC,EAIKC,EAAkB,+BAIxB,SAASC,EACPC,EACiE,CACjE,IAAMC,EAAID,EAAM,MAAM,mCAAmC,EACzD,OAAKC,EACE,CACL,OAAQA,EAAE,CAAC,EACX,EAAG,SAASA,EAAE,CAAC,EAAG,EAAE,CACtB,EAJe,IAKjB,CAMA,SAASC,EAAYF,EAAwB,CAE3C,GADIA,IAAU,WAAaA,IAAU,QACjC,UAAU,KAAKA,CAAK,EAAG,MAAO,GAClC,IAAMG,EAASJ,EAAYC,CAAK,EAChC,OAAKG,EACEA,EAAO,GAAK,GADC,EAEtB,CASA,SAASC,EAAkBJ,EAAgD,CACzE,GAAI,CACF,IAAMK,EAAUL,EAAM,KAAK,EACvBM,EAEJ,GAAID,EAAQ,WAAW,GAAG,EAAG,CAC3B,IAAIE,EAAMF,EAMV,GALIE,EAAI,SAAW,IAAGA,EAAMA,EAAI,MAAM,EAAG,CAAC,GACtCA,EAAI,SAAW,IAAGA,EAAMA,EAAI,MAAM,EAAG,CAAC,GACtCA,EAAI,SAAW,IACjBA,EAAM,IAAIA,EAAI,CAAC,CAAC,GAAGA,EAAI,CAAC,CAAC,GAAGA,EAAI,CAAC,CAAC,GAAGA,EAAI,CAAC,CAAC,GAAGA,EAAI,CAAC,CAAC,GAAGA,EAAI,CAAC,CAAC,IAE3DA,EAAI,SAAW,EAAG,OAAO,KAC7BD,KAAM,YAASC,CAAG,CACpB,SAAW,cAAc,KAAKF,CAAO,EACnCC,KAAM,eAAYD,CAAO,MAEzB,QAAO,KAGT,IAAMG,KAAM,YAASF,CAAG,EAClBG,KAAM,YAASD,CAAG,EACxB,MAAO,CAACC,EAAI,CAAC,EAAGA,EAAI,CAAC,EAAGA,EAAI,CAAC,CAAC,CAChC,OAAQC,EAAA,CACN,OAAO,IACT,CACF,CAOA,SAASC,EAAeF,EAAuC,CAC7D,GAAM,CAACG,EAAGC,EAAGC,CAAC,EAAIL,EAIZM,EAAY,KAAK,OAAOH,EAAI,IAAM,EAAE,EACpCI,EAAS,KAAK,IAAI,GAAI,KAAK,IAAI,EAAGD,CAAS,CAAC,EAC9CE,EACA,KAAK,IAAID,CAAM,GAAK,EAAGC,EAAU,SAC5BD,EAAS,EAAGC,EAAU,aAAa,KAAK,IAAID,CAAM,CAAC,IACvDC,EAAU,aAAaD,CAAM,IAGlC,IAAIE,EACJ,OAAIL,EAAI,GAAIK,EAAc,UACjBJ,EAAI,IAAMA,GAAK,IACtBI,EAAc,QACPJ,EAAI,GACXI,EAAc,UACPJ,EAAI,IACXI,EAAc,WACPJ,EAAI,IACXI,EAAc,WAId,wBAAwBD,CAAO,MAAMC,CAAW,wBAC3B,KAAK,MAAMN,CAAC,CAAC,MAAM,KAAK,MAAMC,CAAC,CAAC,MAAM,KAAK,MAAMC,CAAC,CAAC,OAE5E,CAMA,SAASK,EAAiBC,EAAcpB,EAA8B,CACpE,IAAMqB,EAAQvB,EAAgB,KAAKE,CAAK,EACxC,GAAI,CAACqB,EAAO,OAAO,KACnB,IAAMC,EAAS,WAAWD,EAAM,CAAC,CAAC,EAC5BE,EAAOF,EAAM,CAAC,EAChBG,EAOJ,OANID,IAAS,OAASA,IAAS,KAC7BC,EAAI,KAAK,MAAMF,EAAS,CAAC,EAGzBE,EAAI,KAAK,MAAMF,EAAS,CAAC,EAEvBE,GAAK,EAAU,KACZ,GAAGJ,CAAI,kBAAkBI,CAAC,uCAAkCA,CAAC,MAAMA,EAAI,CAAC,aAAQxB,CAAK,qBAC9F,CAIA,SAASyB,EAAczB,EAAkD,CACvE,OAAO,OAAOA,GAAU,UAAYA,IAAU,MAAQ,CAAC,MAAM,QAAQA,CAAK,CAC5E,CAEA,SAAS0B,EAAQC,EAAsD,CACrE,QAAWC,KAAOD,EAChB,GAAIpC,EAAK,IAAIqC,CAAG,EAAG,OAAOA,CAG9B,CAGO,SAASC,EACdC,EACAC,EAA2B,CAAC,EACd,CACd,IAAMC,EAAoB,CAAC,EAC3B,OAAAC,EAAKH,EAAM,GAAIE,EAAK,GAAOD,EAAQ,cAAgB,EAAK,EACjDC,CACT,CAEA,SAASC,EACPC,EACAC,EACAH,EACAI,EACAC,EACM,CAzPR,IAAAC,EA0PE,GAAI,OAAOJ,GAAS,WAAY,CAC9B,GAAI,CAACG,EAAa,OAClB,IAAIE,EACJ,GAAI,CACFA,EAAUL,EAAwC,IAAM,CAAC,CAAC,CAC5D,OAAQxB,EAAA,CACN,MACF,CACAuB,EAAKM,EAAQJ,EAAMH,EAAK,GAAMK,CAAW,EACzC,MACF,CAEA,GAAI,MAAM,QAAQH,CAAI,EAAG,CACvB,IAAMM,EAAeN,EAAK,OACvBO,GAAUhB,EAAcgB,CAAK,GAAKf,EAAQe,CAAK,CAClD,EAEIL,IAEAI,EAAa,OAAS,GACtBA,EAAa,KAAME,GAASA,EAAK,OAAS,MAAS,GAEnDV,EAAI,KAAK,CACP,KAAM,cACN,SAAU,UACV,KAAMG,GAAQ,SACd,QACE,6GACF,KAAM,yEACR,CAAC,EAQDK,EAAa,OAAS,GACtBA,EAAa,MAAM,CAACE,EAAMC,IAAUD,EAAK,OAASC,CAAK,GAEvDX,EAAI,KAAK,CACP,KAAM,eACN,SAAU,UACV,KAAMG,GAAQ,SACd,QACE,2HACF,KAAM,oFACR,CAAC,GAKL,IAAMS,EAAW,IAAI,IACrB,QAAWF,KAAQF,EAAc,CAC/B,IAAMZ,EAAMc,EAAK,KACjB,GAAyBd,GAAQ,KAAM,SACvC,IAAMiB,EAAa,GAAG,OAAOjB,CAAG,IAAI,OAAOA,CAAG,CAAC,GAC/CgB,EAAS,IAAIC,IAAaP,EAAAM,EAAS,IAAIC,CAAU,IAAvB,KAAAP,EAA4B,GAAK,CAAC,CAC9D,CACA,OAAW,CAACO,EAAYC,CAAK,IAAKF,EAChC,GAAIE,EAAQ,EAAG,CACb,IAAM9C,EAAQ6C,EAAW,MAAMA,EAAW,QAAQ,GAAG,EAAI,CAAC,EAC1Db,EAAI,KAAK,CACP,KAAM,gBACN,SAAU,QACV,KAAMG,GAAQ,SACd,QAAS,uBAAuBnC,CAAK,WAAW8C,CAAK,sDACrD,KAAM,gFACR,CAAC,CACH,CAGFZ,EAAK,QAAQ,CAACO,EAAOE,IAAU,CAC7BV,EAAKQ,EAAO,GAAGN,CAAI,IAAIQ,CAAK,IAAKX,EAAK,GAAOK,CAAW,CAC1D,CAAC,EACD,MACF,CAEA,GAAI,CAACZ,EAAcS,CAAI,EAAG,OAE1B,IAAMP,EAAUO,EACVa,EAAMrB,EAAQC,CAAO,EACrBqB,EAAOD,EAAOZ,EAAO,GAAGA,CAAI,MAAMY,CAAG,GAAKA,EAAOZ,GAAQ,SAE/D,GAAI,CAACY,EAAK,CACR,IAAME,EAAc,OAAO,KAAKtB,CAAO,EAAE,OACtCC,GACC,CAACnC,EAAS,IAAImC,CAAG,GACjB,CAACA,EAAI,WAAW,KAAK,GACrB,CAACA,EAAI,WAAW,IAAI,GACpB,CAACA,EAAI,WAAW,MAAM,GACtB,CAACA,EAAI,WAAW,MAAM,CAC1B,EACIqB,EAAY,SAAW,GACzBjB,EAAI,KAAK,CACP,KAAM,cACN,SAAU,UACV,KAAMgB,EACN,QAAS,IAAIC,EAAY,CAAC,CAAC,sDAC3B,KAAM,yEACR,CAAC,EAEH,MACF,CAEA,IAAMC,EAAUvB,EAAQoB,CAAG,EAY3B,GAVIvD,EAAK,IAAIuD,CAAG,GAAKG,IAAY,MAAQA,IAAY,QACnDlB,EAAI,KAAK,CACP,KAAM,eACN,SAAU,QACV,KAAMgB,EACN,QAAS,aAAaD,CAAG,iCAAiC,MAAM,QAAQG,CAAO,EAAI,QAAU,OAAOA,CAAO,KAC3G,KAAM,WAAWH,CAAG,sDACtB,CAAC,EAGCtB,EAAcE,EAAQ,KAAK,EAAG,CAChC,IAAMwB,EAAQxB,EAAQ,MACtB,QAAWP,KAAQ+B,EAAO,CACxB,IAAMnD,EAAQmD,EAAM/B,CAAI,EAkBxB,GAbI1B,EAAiB,IAAI0B,CAAI,GAAK,OAAOpB,GAAU,YACjDgC,EAAI,KAAK,CACP,KAAM,oBACN,SAAU,UACV,KAAMgB,EACN,QAAS,YAAY5B,CAAI,4CACzB,KAAM,gHACR,CAAC,EAODzB,EAAY,IAAIyB,CAAI,GACpB,OAAOpB,GAAU,UACjBJ,EAAc,KAAKI,CAAK,EACxB,CACA,IAAMS,EAAML,EAAkBJ,CAAK,EAC7BoD,EAAY3C,EACdE,EAAeF,CAAG,EAClB,wCAEJuB,EAAI,KAAK,CACP,KAAM,kBACN,SAAU,OACV,KAAMgB,EACN,QAAS,YAAY5B,CAAI,4BAA4BpB,CAAK,KAC1D,KAAM,+BAA0BoD,CAAS,yCAC3C,CAAC,CACH,CAIA,GAAIvD,EAAc,IAAIuB,CAAI,GAAK,OAAOpB,GAAU,SAAU,CACxD,IAAMqD,EAAclC,EAAiBC,EAAMpB,CAAK,EAC5CqD,GACFrB,EAAI,KAAK,CACP,KAAM,oBACN,SAAU,OACV,KAAMgB,EACN,QAAS,YAAY5B,CAAI,MAAMpB,CAAK,oCACpC,KAAM,4CAA4CqD,CAAW,EAC/D,CAAC,CAEL,CACF,CACF,CAIA,IAAMC,EAAW3B,EAAQ,SACzB,GAAI,OAAO2B,GAAa,SACtB,GAAI,CAACpD,EAAYoD,CAAQ,EACvBtB,EAAI,KAAK,CACP,KAAM,eACN,SAAU,UACV,KAAMgB,EACN,QAAS,iBAAiBM,CAAQ,yBAClC,KAAM,8JACR,CAAC,MACI,CAIL,IAAMnD,EAASJ,EAAYuD,CAAQ,GAC/BnD,GAAA,YAAAA,EAAQ,UAAW,SAAWA,EAAO,GAAK,GAAKA,EAAO,GAAK,IAC7D6B,EAAI,KAAK,CACP,KAAM,wBACN,SAAU,UACV,KAAMgB,EACN,QAAS,gBAAgBM,CAAQ,+HACjC,KAAM,iKACR,CAAC,CAEL,CAKF,IAAMC,EAAc5B,EAAQ,YAC5B,GAAI,OAAO4B,GAAgB,UAAYA,IAAgB,UAAW,CAChE,IAAMpD,EAASJ,EAAYwD,CAAW,EAClC,CAACpD,GAAUA,EAAO,SAAW,QAC/B6B,EAAI,KAAK,CACP,KAAM,kBACN,SAAU,UACV,KAAMgB,EACN,QAAS,oBAAoBO,CAAW,mCACxC,KAAM,sGACR,CAAC,EACQpD,EAAO,EAAI,GACpB6B,EAAI,KAAK,CACP,KAAM,kBACN,SAAU,QACV,KAAMgB,EACN,QAAS,oBAAoBO,CAAW,OAAOpD,EAAO,CAAC,yEACvD,KAAM,6FACR,CAAC,CAEL,CAIA,IAAMqD,EAAW7B,EAAQ,SACzB,GAAI,OAAO6B,GAAa,UAAYA,IAAa,UAAW,CAC1D,IAAMrD,EAASJ,EAAYyD,CAAQ,EAC/B,CAACrD,GAAUA,EAAO,SAAW,QAC/B6B,EAAI,KAAK,CACP,KAAM,eACN,SAAU,UACV,KAAMgB,EACN,QAAS,iBAAiBQ,CAAQ,gCAClC,KAAM,mGACR,CAAC,EACQrD,EAAO,EAAI,GACpB6B,EAAI,KAAK,CACP,KAAM,eACN,SAAU,QACV,KAAMgB,EACN,QAAS,iBAAiBQ,CAAQ,OAAOrD,EAAO,CAAC,sEACjD,KAAM,oDACR,CAAC,CAEL,CAEA8B,EAAKiB,EAASF,EAAMhB,EAAK,GAAOK,CAAW,CAC7C,CAyBO,SAASoB,EACd3B,EACAC,EAA2B,CAAC,EACV,CAClB,IAAM2B,EAAS7B,EAASC,EAAMC,CAAO,EAC/B4B,EAA6B,CACjC,MAAO,EACP,QAAS,EACT,KAAM,EACN,MAAOD,EAAO,MAChB,EACA,QAAWE,KAASF,EAAQC,EAAQC,EAAM,QAAQ,GAAK,EACvD,MAAO,CAAE,GAAID,EAAQ,QAAU,EAAG,OAAAD,EAAQ,QAAAC,CAAQ,CACpD,CAGO,SAASE,EAAOC,EAAmC,CACxD,GAAIA,EAAY,SAAW,EAAG,MAAO,0BACrC,IAAMC,EAAQC,GACZA,IAAM,QAAU,SAAMA,IAAM,UAAY,SAAM,IAChD,OAAOF,EACJ,IACEG,GACC,GAAGF,EAAKE,EAAE,QAAQ,CAAC,KAAKA,EAAE,IAAI,KAAKA,EAAE,IAAI;AAAA,IAAOA,EAAE,OAAO,GAAGA,EAAE,KAAO;AAAA,WAASA,EAAE,IAAI,GAAK,EAAE,EAC/F,EACC,KAAK;AAAA,CAAI,CACd,CCziBA,IAAAC,EAA4C,wBAe5C,IAAMC,EAAO,IAAI,IAAY,CAAC,GAAG,WAAU,GAAG,SAAO,CAAC,EAChDC,EAAO,IAAI,IAAY,UAAQ,EAErC,SAASC,EAAcC,EAAkD,CACvE,OAAO,OAAOA,GAAU,UAAYA,IAAU,MAAQ,CAAC,MAAM,QAAQA,CAAK,CAC5E,CAEA,SAASC,EAAQC,EAAsD,CACrE,QAAWC,KAAOD,EAChB,GAAIL,EAAK,IAAIM,CAAG,EAAG,OAAOA,CAG9B,CAIA,SAASC,EAAUJ,EAAyB,CAC1C,GAAI,MAAM,QAAQA,CAAK,EAAG,OAAOA,EAAM,IAAII,CAAS,EACpD,GAAIL,EAAcC,CAAK,EAAG,CACxB,IAAMK,EAA+B,CAAC,EACtC,QAAWF,KAAOH,EAAOK,EAAIF,CAAG,EAAIC,EAAUJ,EAAMG,CAAG,CAAC,EACxD,OAAOE,CACT,CACA,OAAOL,CACT,CA4BO,SAASM,EAAIC,EAAeC,EAA2B,CAAC,EAAc,CAC3E,IAAMC,EAAOL,EAAUG,CAAI,EACrBG,EAAwB,CAAC,EAC/B,OAAAC,EAAQF,EAAM,GAAIC,CAAO,EAClB,CAAE,KAAAD,EAAM,QAAAC,EAAS,OAAQE,EAASH,EAAMD,CAAO,CAAE,CAC1D,CAEA,SAASG,EAAQE,EAAeC,EAAcJ,EAA6B,CACzE,GAAI,MAAM,QAAQG,CAAI,EAAG,CACvB,OAAW,CAACE,EAAOC,CAAK,IAAKH,EAAK,QAAQ,EACxCF,EAAQK,EAAO,GAAGF,CAAI,IAAIC,CAAK,IAAKL,CAAO,EAE7C,MACF,CACA,GAAI,CAACX,EAAcc,CAAI,EAAG,OAE1B,IAAMI,EAAMhB,EAAQY,CAAI,EACxB,GAAI,CAACI,EAAK,OACV,IAAMC,EAAOJ,EAAO,GAAGA,CAAI,MAAMG,CAAG,GAAKA,EAIrCnB,EAAK,IAAImB,CAAG,GAAKJ,EAAKI,CAAG,IAAM,MAAQJ,EAAKI,CAAG,IAAM,SACvDJ,EAAKI,CAAG,EAAI,KACZP,EAAQ,KAAK,CACX,KAAM,eACN,KAAMQ,EACN,QAAS,aAAaD,CAAG,+CAC3B,CAAC,GAGHN,EAAQE,EAAKI,CAAG,EAAGC,EAAMR,CAAO,CAClC","names":["src_exports","__export","diagnose","fix","format","validate","__toCommonJS","import_core","import_palette","TAGS","VOID","RESERVED","TYPOGRAPHY_STYLE","COLOR_STYLE","LITERAL_COLOR","SPACING_STYLE","LITERAL_SPACING","parseOffset","value","m","isValidTone","parsed","parseLiteralToLch","trimmed","rgb","hex","lab","lch","e","buildColorHint","L","C","h","rawOffset","offset","toneStr","colorFamily","buildSpacingHint","prop","match","amount","unit","n","isPlainObject","findTag","element","key","diagnose","root","options","out","walk","node","path","dynamic","runReactive","_a","result","elementItems","child","item","index","seenKeys","literalKey","count","tag","here","contentKeys","content","style","colorHint","spacingHint","dataTone","dataDensity","dataSize","validate","issues","summary","issue","format","diagnostics","icon","s","d","import_core","TAGS","VOID","isPlainObject","value","findTag","element","key","cloneTree","out","fix","root","options","tree","applied","walkFix","validate","node","path","index","child","tag","here"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/diagnose.ts","../src/shared.ts","../src/fix.ts"],"sourcesContent":["// @domphy/doctor — static analyzer for Domphy element trees. Catches\n// non-idiomatic patterns (inline typography, literal theme colors, unknown\n// tones, void-tag content, missing/duplicate/unstable _key on lists, unknown\n// tags) so humans and AI agents get a feedback loop to self-correct generated\n// code. `validate()` is the aggregate entry point.\n\nexport type {\n DiagnoseOptions,\n Diagnostic,\n Severity,\n ValidationReport,\n ValidationSummary,\n} from \"./diagnose.js\";\nexport { diagnose, format, validate } from \"./diagnose.js\";\nexport type { AppliedFix, FixResult } from \"./fix.js\";\nexport { fix } from \"./fix.js\";\n","import { cssRgbToRgb, hexToRgb, labToLch, rgbToLab } from \"@domphy/palette\";\nimport { findTag, isPlainObject, VOID } from \"./shared.js\";\n\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport interface Diagnostic {\n /** Rule id, e.g. \"inline-typography\". */\n rule: string;\n severity: Severity;\n /** Human path to the offending node, e.g. \"div > ul > li\". */\n path: string;\n message: string;\n /** How to fix it. */\n hint?: string;\n}\n\nexport interface DiagnoseOptions {\n /**\n * Invoke reactive content functions `(listener) => …` with a no-op listener to\n * analyze their output (catches missing `_key` in dynamic lists). Default true.\n * Set false if your reactive functions have side effects.\n */\n runReactive?: boolean;\n}\n\nconst RESERVED = new Set([\n \"$\",\n \"style\",\n \"_key\",\n \"_portal\",\n \"_context\",\n \"_metadata\",\n]);\n\n// Typography style properties that must not be set inline — use patches instead.\n// Expanded from bench data: fontFamily + textDecoration were missing and caused\n// agents to write { style: { fontFamily: \"...\" } } without correction.\nconst TYPOGRAPHY_STYLE = new Set([\n \"fontSize\",\n \"lineHeight\",\n \"fontWeight\",\n \"letterSpacing\",\n \"fontFamily\",\n \"textDecoration\",\n]);\n\n// Color-bearing style props that should resolve through a theme token rather\n// than a literal value, so theming and dark mode apply. Shorthands\n// (background/border/outline) are included because they often carry a color.\nconst COLOR_STYLE = new Set([\n \"color\",\n \"backgroundColor\",\n \"background\",\n \"borderColor\",\n \"border\",\n \"outlineColor\",\n \"outline\",\n \"fill\",\n \"stroke\",\n]);\n\n// A literal color value: hex (#rgb … #rrggbbaa) or an rgb()/rgba()/hsl()/hsla()\n// function. Keywords like transparent/currentColor/inherit are intentionally\n// allowed — they carry no theme meaning.\nconst LITERAL_COLOR = /#[0-9a-fA-F]{3,8}\\b|\\b(?:rgba?|hsla?)\\s*\\(/;\n\n// Spacing style properties where literal rem/em/px values should use themeSpacing().\n// These are layout, not typography, but themeSpacing() ensures density consistency.\n// Logical properties (paddingBlock, paddingInline, etc.) are included — they are\n// used in Domphy patches and must also go through themeSpacing() for density scaling.\nconst SPACING_STYLE = new Set([\n \"margin\",\n \"marginTop\",\n \"marginRight\",\n \"marginBottom\",\n \"marginLeft\",\n \"marginInline\",\n \"marginBlock\",\n \"marginInlineStart\",\n \"marginInlineEnd\",\n \"marginBlockStart\",\n \"marginBlockEnd\",\n \"padding\",\n \"paddingTop\",\n \"paddingRight\",\n \"paddingBottom\",\n \"paddingLeft\",\n \"paddingInline\",\n \"paddingBlock\",\n \"paddingInlineStart\",\n \"paddingInlineEnd\",\n \"paddingBlockStart\",\n \"paddingBlockEnd\",\n \"gap\",\n \"rowGap\",\n \"columnGap\",\n]);\n\n// Matches literal spacing values like \"16px\", \"1.5rem\", \"2em\" but not \"auto\",\n// \"inherit\", \"0\" (unitless zero is fine), or computed values.\nconst LITERAL_SPACING = /^(\\d+(?:\\.\\d+)?)(rem|em|px)$/;\n\n// Parses \"increase-N\" / \"decrease-N\" / \"shift-N\" into family + numeric offset.\n// Returns null when the pattern doesn't match (grammar error).\nfunction parseOffset(\n value: string,\n): { family: \"increase\" | \"decrease\" | \"shift\"; n: number } | null {\n const m = value.match(/^(increase|decrease|shift)-(\\d+)$/);\n if (!m) return null;\n return {\n family: m[1] as \"increase\" | \"decrease\" | \"shift\",\n n: parseInt(m[2], 10),\n };\n}\n\n// Valid `dataTone` grammar AND range:\n// \"inherit\", \"base\", a bare integer, or shift-N/increase-N/decrease-N where N ≤ 17.\n// The default Domphy theme has 18 tone steps (0–17). Values with valid grammar\n// but N > 17 are also rejected here so they surface as `unknown-tone` errors.\nfunction isValidTone(value: string): boolean {\n if (value === \"inherit\" || value === \"base\") return true;\n if (/^-?\\d+$/.test(value)) return true;\n const parsed = parseOffset(value);\n if (!parsed) return false;\n return parsed.n <= 17; // tone ramp has 18 steps: 0–17\n}\n\n// ─── Chromametry integration ─────────────────────────────────────────────────\n\n/**\n * Parses a CSS color literal (hex or rgb/rgba) into LCH [L, C, h].\n * Returns null if parsing fails or the format is unsupported (named colors, hsl).\n * Uses @domphy/palette math (CIELAB via D65 reference white).\n */\nfunction parseLiteralToLch(value: string): [number, number, number] | null {\n try {\n const trimmed = value.trim();\n let rgb: number[];\n\n if (trimmed.startsWith(\"#\")) {\n let hex = trimmed;\n if (hex.length === 9) hex = hex.slice(0, 7); // strip alpha #rrggbbaa → #rrggbb\n if (hex.length === 5) hex = hex.slice(0, 4); // strip alpha #rgba → #rgb\n if (hex.length === 4) {\n hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;\n }\n if (hex.length !== 7) return null;\n rgb = hexToRgb(hex);\n } else if (/^rgba?\\s*\\(/.test(trimmed)) {\n rgb = cssRgbToRgb(trimmed);\n } else {\n return null; // hsl, named colors, custom-properties — skip\n }\n\n const lab = rgbToLab(rgb);\n const lch = labToLch(lab);\n return [lch[0], lch[1], lch[2]];\n } catch {\n return null;\n }\n}\n\n// Pulls the first parseable color token out of a (possibly shorthand) value.\n// Shorthands such as \"1px solid #ccc\" or \"0 0 4px rgba(0,0,0,.5)\" embed the\n// color among other tokens, so `parseLiteralToLch` (which expects the value to\n// START with the color) would otherwise miss it. Matches a #hex literal or a\n// complete rgb()/rgba() call (with its arguments). hsl()/named colors are left\n// for the generic fallback, matching parseLiteralToLch's own coverage.\nconst EMBEDDED_COLOR = /#[0-9a-fA-F]{3,8}\\b|rgba?\\s*\\([^)]*\\)/;\n\nfunction extractColorLiteral(value: string): string | null {\n const match = EMBEDDED_COLOR.exec(value);\n return match ? match[0] : null;\n}\n\n/**\n * Converts LCH coordinates into a concrete `themeColor()` call suggestion plus\n * a perceptual description. The tone and color-family are approximations for the\n * default Domphy theme (light, 10 neutral tones, base at mid-lightness).\n */\nfunction buildColorHint(lch: [number, number, number]): string {\n const [L, C, h] = lch;\n\n // Map lightness to a Domphy tone relative to base (~L50).\n // Each step ≈ 10 lightness units — clamp to ±9 (max offset in a 10-step ramp).\n const rawOffset = Math.round((L - 50) / 10);\n const offset = Math.max(-9, Math.min(9, rawOffset));\n let toneStr: string;\n if (Math.abs(offset) <= 1) toneStr = '\"base\"';\n else if (offset < 0) toneStr = `\"decrease-${Math.abs(offset)}\"`;\n else toneStr = `\"increase-${offset}\"`;\n\n // Infer the most likely semantic color family from chroma + hue.\n let colorFamily: string;\n if (C < 12) colorFamily = \"neutral\";\n else if (h < 30 || h >= 330)\n colorFamily = \"error\"; // red spectrum\n else if (h < 75)\n colorFamily = \"warning\"; // orange-yellow\n else if (h < 165)\n colorFamily = \"success\"; // green\n else if (h < 265)\n colorFamily = \"primary\"; // blue-indigo\n else colorFamily = \"primary\"; // violet → treat as primary\n\n return (\n `(l) => themeColor(l, ${toneStr}, \"${colorFamily}\") ` +\n `[perceptual LCH L=${Math.round(L)} C=${Math.round(C)} h=${Math.round(h)}°]`\n );\n}\n\n/**\n * Converts a literal spacing value like \"16px\" / \"1.5rem\" / \"2em\" into a\n * themeSpacing(n) suggestion. themeSpacing(n) = n/4 em, so n=4 → 1em ≈ 16px.\n */\nfunction buildSpacingHint(prop: string, value: string): string | null {\n const match = LITERAL_SPACING.exec(value);\n if (!match) return null;\n const amount = parseFloat(match[1]);\n const unit = match[2];\n let n: number;\n if (unit === \"rem\" || unit === \"em\") {\n n = Math.round(amount * 4);\n } else {\n // px: assume default 16px/rem → 1em = 16px\n n = Math.round(amount / 4);\n }\n if (n <= 0) return null;\n return `${prop}: themeSpacing(${n}) — themeSpacing(n)=n/4em, so ${n}/4=${n / 4}em ≈ ${value} at default density`;\n}\n\n// ─── Tree walkers ─────────────────────────────────────────────────────────────\n\n/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */\nexport function diagnose(\n root: unknown,\n options: DiagnoseOptions = {},\n): Diagnostic[] {\n const out: Diagnostic[] = [];\n walk(root, \"\", out, false, options.runReactive !== false);\n return out;\n}\n\nfunction walk(\n node: unknown,\n path: string,\n out: Diagnostic[],\n dynamic: boolean,\n runReactive: boolean,\n): void {\n if (typeof node === \"function\") {\n if (!runReactive) return;\n let result: unknown;\n try {\n result = (node as (listener: unknown) => unknown)(() => {});\n } catch {\n return; // reactive fn threw without a real runtime — skip\n }\n walk(result, path, out, true, runReactive);\n return;\n }\n\n if (Array.isArray(node)) {\n const elementItems = node.filter(\n (child) => isPlainObject(child) && findTag(child),\n ) as Record<string, unknown>[];\n\n if (dynamic) {\n if (\n elementItems.length > 1 &&\n elementItems.some((item) => item._key === undefined)\n ) {\n out.push({\n rule: \"missing-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list child without `_key` — reordered/keyed lists need a stable `_key` for correct reconcile.\",\n hint: \"Add `_key: <stable id>` to each item produced by the reactive function.\",\n });\n }\n\n // unstable-key (heuristic): in a dynamic list every `_key` equals its\n // sibling position (0, 1, 2, …). That is the runtime footprint of\n // `items.map((item, i) => ({ …, _key: i }))` — an array-index key, which\n // defeats the point of keying because keys shift when the list reorders.\n if (\n elementItems.length > 1 &&\n elementItems.every((item, index) => item._key === index)\n ) {\n out.push({\n rule: \"unstable-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list `_key` values are the array index (0, 1, 2, …) — index keys are unstable across reorders/inserts.\",\n hint: \"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index.\",\n });\n }\n }\n\n // duplicate-key: two siblings sharing the same `_key` value break reconcile\n const seenKeys = new Map<string, number>();\n for (const item of elementItems) {\n const key = item._key;\n if (key === undefined || key === null) continue;\n const literalKey = `${typeof key}:${String(key)}`;\n seenKeys.set(literalKey, (seenKeys.get(literalKey) ?? 0) + 1);\n }\n for (const [literalKey, count] of seenKeys) {\n if (count > 1) {\n const value = literalKey.slice(literalKey.indexOf(\":\") + 1);\n out.push({\n rule: \"duplicate-key\",\n severity: \"error\",\n path: path || \"(list)\",\n message: `Duplicate \\`_key\\` \"${value}\" among ${count} siblings — keys must be unique within a list.`,\n hint: \"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant).\",\n });\n }\n }\n\n node.forEach((child, index) => {\n walk(child, `${path}[${index}]`, out, false, runReactive);\n });\n return;\n }\n\n if (!isPlainObject(node)) return;\n\n const element = node;\n const tag = findTag(element);\n const here = tag ? (path ? `${path} > ${tag}` : tag) : path || \"(root)\";\n\n if (!tag) {\n const contentKeys = Object.keys(element).filter(\n (key) =>\n !RESERVED.has(key) &&\n !key.startsWith(\"_on\") &&\n !key.startsWith(\"on\") &&\n !key.startsWith(\"data\") &&\n !key.startsWith(\"aria\"),\n );\n if (contentKeys.length === 1) {\n out.push({\n rule: \"unknown-tag\",\n severity: \"warning\",\n path: here,\n message: `\"${contentKeys[0]}\" is not a known HTML/SVG tag — likely a typo.`,\n hint: \"An element's first key must be a valid tag (div, button, span, …).\",\n });\n }\n return;\n }\n\n const content = element[tag];\n\n if (VOID.has(tag) && content !== null && content !== undefined) {\n out.push({\n rule: \"void-content\",\n severity: \"error\",\n path: here,\n message: `Void tag \"${tag}\" must have null content (got ${Array.isArray(content) ? \"array\" : typeof content}).`,\n hint: `Write { ${tag}: null, … } and put attributes as sibling keys.`,\n });\n }\n\n if (isPlainObject(element.style)) {\n const style = element.style;\n for (const prop in style) {\n const value = style[prop];\n\n // inline-typography: typography properties must come from patches, not\n // inline style. fontFamily and textDecoration were missing from the original\n // set and are added here based on bench data showing persistent violations.\n if (TYPOGRAPHY_STYLE.has(prop) && typeof value !== \"function\") {\n out.push({\n rule: \"inline-typography\",\n severity: \"warning\",\n path: here,\n message: `Inline \\`${prop}\\` — avoid inline typography styles.`,\n hint: \"Use a typography patch (paragraph()/heading()/small()/strong()/…) via $ so the theme owns the type scale.\",\n });\n }\n\n // raw-theme-value: literal color values bypass theming/dark mode.\n // Enhanced with @domphy/palette chromametry: converts the literal color to\n // LCH and suggests the nearest themeColor() call with perceptual coordinates.\n if (\n COLOR_STYLE.has(prop) &&\n typeof value === \"string\" &&\n LITERAL_COLOR.test(value)\n ) {\n // For shorthands (border/outline/background) the color is embedded in a\n // larger string, so extract the color token first; fall back to the\n // whole value (covers the simple `color: \"#fff\"` case).\n const colorLiteral = extractColorLiteral(value) ?? value;\n const lch = parseLiteralToLch(colorLiteral);\n const colorHint = lch\n ? buildColorHint(lch)\n : \"(l) => themeColor(l, tone, colorName)\";\n\n out.push({\n rule: \"raw-theme-value\",\n severity: \"info\",\n path: here,\n message: `Inline \\`${prop}\\` uses a literal color (${value}).`,\n hint: `Prefer a theme token — ${colorHint} — so theming and dark mode apply.`,\n });\n }\n\n // raw-spacing-value: literal rem/em/px spacing values should use themeSpacing()\n // to respect the theme's density system. info-severity (soft recommendation).\n if (SPACING_STYLE.has(prop) && typeof value === \"string\") {\n const spacingHint = buildSpacingHint(prop, value);\n if (spacingHint) {\n out.push({\n rule: \"raw-spacing-value\",\n severity: \"info\",\n path: here,\n message: `Inline \\`${prop}: \"${value}\"\\` uses a literal spacing value.`,\n hint: `Prefer themeSpacing() for theme density: ${spacingHint}`,\n });\n }\n }\n }\n }\n\n // unknown-tone: dataTone is not valid grammar, or it's valid grammar but the\n // numeric offset is out of the 18-step ramp range (0–17).\n const dataTone = element.dataTone;\n if (typeof dataTone === \"string\") {\n if (!isValidTone(dataTone)) {\n out.push({\n rule: \"unknown-tone\",\n severity: \"warning\",\n path: here,\n message: `\\`dataTone\\` \"${dataTone}\" is not a valid tone.`,\n hint: 'Use \"inherit\", \"base\", a number, or \"shift-N\"/\"increase-N\"/\"decrease-N\" with N ≤ 17 (the ramp has 18 steps). Words like \"surface\"/\"text\" are not tones.',\n });\n } else {\n // middle-surface-anchor: shift-4 through shift-13 sets a mid-ramp surface\n // anchor. Children's tones may clamp and fold back, collapsing the contrast\n // between background and text. Edge anchors (0–3 light, 14–17 dark) are safe.\n const parsed = parseOffset(dataTone);\n if (parsed?.family === \"shift\" && parsed.n >= 4 && parsed.n <= 13) {\n out.push({\n rule: \"middle-surface-anchor\",\n severity: \"warning\",\n path: here,\n message: `\\`dataTone: \"${dataTone}\"\\` uses a mid-ramp surface anchor (steps 4–13). Child tones derived from this surface may clamp and collapse contrast.`,\n hint: \"Prefer edge anchors: shift-0–3 for light surfaces, shift-14–17 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions.\",\n });\n }\n }\n }\n\n // unknown-density: dataDensity value is invalid grammar or out of the 5-step\n // density range (increase/decrease 0–4; the scale factors are 0.75, 1, 1.5, 2, 2.5).\n const dataDensity = element.dataDensity;\n if (typeof dataDensity === \"string\" && dataDensity !== \"inherit\") {\n const parsed = parseOffset(dataDensity);\n if (!parsed || parsed.family === \"shift\") {\n out.push({\n rule: \"unknown-density\",\n severity: \"warning\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" is not a valid density offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–4. \"shift-\" is not valid for density.',\n });\n } else if (parsed.n > 4) {\n out.push({\n rule: \"unknown-density\",\n severity: \"error\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" N=${parsed.n} is out of range — the density scale has 5 steps (max offset: 4).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 4. Density factors: [0.75, 1, 1.5, 2, 2.5].',\n });\n }\n }\n\n // unknown-size: dataSize value is invalid grammar or out of the 8-step size\n // range (increase/decrease 0–7).\n const dataSize = element.dataSize;\n if (typeof dataSize === \"string\" && dataSize !== \"inherit\") {\n const parsed = parseOffset(dataSize);\n if (!parsed || parsed.family === \"shift\") {\n out.push({\n rule: \"unknown-size\",\n severity: \"warning\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" is not a valid size offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–7. \"shift-\" is not valid for size.',\n });\n } else if (parsed.n > 7) {\n out.push({\n rule: \"unknown-size\",\n severity: \"error\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" N=${parsed.n} is out of range — the size scale has 8 steps (max offset: 7).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 7.',\n });\n }\n }\n\n walk(content, here, out, false, runReactive);\n}\n\n/** Issue counts by severity, plus the grand total. */\nexport interface ValidationSummary {\n error: number;\n warning: number;\n info: number;\n total: number;\n}\n\n/** Structured result of {@link validate}: pass/fail flag, issues, and counts. */\nexport interface ValidationReport {\n /** True when there are no `error`-severity diagnostics. */\n ok: boolean;\n /** Every diagnostic found, across all rules (alias of `diagnose` output). */\n issues: Diagnostic[];\n summary: ValidationSummary;\n}\n\n/**\n * Runs every diagnose rule and returns a structured report (pass/fail flag,\n * the issue list, and counts by severity). `ok` is false when any `error`\n * diagnostic is present; warnings/info do not flip `ok`. Use this as the single\n * programmatic entry point; `diagnose`/`format` remain available for raw access.\n */\nexport function validate(\n root: unknown,\n options: DiagnoseOptions = {},\n): ValidationReport {\n const issues = diagnose(root, options);\n const summary: ValidationSummary = {\n error: 0,\n warning: 0,\n info: 0,\n total: issues.length,\n };\n for (const issue of issues) summary[issue.severity] += 1;\n return { ok: summary.error === 0, issues, summary };\n}\n\n/** Formats diagnostics as a readable report (one line per issue). */\nexport function format(diagnostics: Diagnostic[]): string {\n if (diagnostics.length === 0) return \"✓ No issues found.\";\n const icon = (s: Severity) =>\n s === \"error\" ? \"✗\" : s === \"warning\" ? \"⚠\" : \"i\";\n return diagnostics\n .map(\n (d) =>\n `${icon(d.severity)} [${d.rule}] ${d.path}\\n ${d.message}${d.hint ? `\\n → ${d.hint}` : \"\"}`,\n )\n .join(\"\\n\");\n}\n","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\n\n// Internal helpers shared by diagnose.ts and fix.ts. Kept in one module so the\n// tag tables and the tree-shape predicates have a single source of truth.\n\n/** Every valid HTML and SVG tag name. */\nexport const TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\n\n/** Tags that render no children (input, img, br, …). */\nexport const VOID = new Set<string>(VoidTags);\n\n/** True for a non-array object (a Domphy element or a plain record). */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Returns the element's tag key (the first key that names a valid tag). */\nexport function findTag(element: Record<string, unknown>): string | undefined {\n for (const key in element) {\n if (TAGS.has(key)) return key;\n }\n return undefined;\n}\n","import {\n type DiagnoseOptions,\n type ValidationReport,\n validate,\n} from \"./diagnose.js\";\nimport { findTag, isPlainObject, VOID } from \"./shared.js\";\n\n// Autofix for Domphy element trees. We ONLY apply transforms that are provably\n// lossless — they fix structurally-invalid input without guessing intent. Every\n// other diagnostic (missing/unstable keys, inline typography, literal colors,\n// unknown tones/tags) needs semantic intent the tree does not carry, so applying\n// a \"fix\" would corrupt the author's meaning (e.g. an index key is itself the\n// unstable-key anti-pattern). Those are returned in `report` for the model or a\n// human to resolve. The fixer set is a registry so safe transforms can be added.\n\n// Structural clone that preserves functions (reactive `(listener) => …` values)\n// by reference — a JSON clone would drop them. Primitives pass through.\nfunction cloneTree(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(cloneTree);\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const key in value) out[key] = cloneTree(value[key]);\n return out;\n }\n return value;\n}\n\n/** One applied lossless fix. */\nexport interface AppliedFix {\n rule: string;\n /** Human path to the node, e.g. \"div > input\". */\n path: string;\n message: string;\n}\n\n/** Result of {@link fix}: the corrected tree, what was applied, and what remains. */\nexport interface FixResult {\n /** A deep copy of the input with lossless fixes applied (functions preserved). */\n tree: unknown;\n /** The lossless fixes that were applied. */\n applied: AppliedFix[];\n /** validate() run on the fixed tree — `report.issues` are the manual remainder. */\n report: ValidationReport;\n}\n\n/**\n * Applies every provably-lossless fix to a copy of the tree and returns the\n * result plus a fresh validation report. Currently fixes `void-content` (a void\n * tag like input/img/br cannot have children, so its content is set to null).\n * Issues that need intent are left untouched and surface in `report` — this\n * includes `raw-spacing-value` and `raw-theme-value` (require semantic choices)\n * and key rules (require stable identity from data, not the tree shape).\n */\nexport function fix(root: unknown, options: DiagnoseOptions = {}): FixResult {\n const tree = cloneTree(root);\n const applied: AppliedFix[] = [];\n walkFix(tree, \"\", applied);\n return { tree, applied, report: validate(tree, options) };\n}\n\nfunction walkFix(node: unknown, path: string, applied: AppliedFix[]): void {\n if (Array.isArray(node)) {\n for (const [index, child] of node.entries()) {\n walkFix(child, `${path}[${index}]`, applied);\n }\n return;\n }\n if (!isPlainObject(node)) return;\n\n const tag = findTag(node);\n if (!tag) return;\n const here = path ? `${path} > ${tag}` : tag;\n\n // void-content: a void tag renders no children, so any content is invalid and\n // cannot be rendered — clearing it to null is lossless.\n if (VOID.has(tag) && node[tag] !== null && node[tag] !== undefined) {\n node[tag] = null;\n applied.push({\n rule: \"void-content\",\n path: here,\n message: `Void tag <${tag}> cannot have content — cleared to null.`,\n });\n }\n\n walkFix(node[tag], here, applied);\n}\n"],"mappings":"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","names":["src_exports","__export","diagnose","fix","format","validate","__toCommonJS","import_palette","import_core","TAGS","VOID","isPlainObject","value","findTag","element","key","RESERVED","TYPOGRAPHY_STYLE","COLOR_STYLE","LITERAL_COLOR","SPACING_STYLE","LITERAL_SPACING","parseOffset","value","m","isValidTone","parsed","parseLiteralToLch","trimmed","rgb","hex","lab","lch","e","EMBEDDED_COLOR","extractColorLiteral","match","buildColorHint","L","C","h","rawOffset","offset","toneStr","colorFamily","buildSpacingHint","prop","amount","unit","n","diagnose","root","options","out","walk","node","path","dynamic","runReactive","_a","_b","result","elementItems","child","isPlainObject","findTag","item","index","seenKeys","key","literalKey","count","element","tag","here","contentKeys","content","VOID","style","colorLiteral","colorHint","spacingHint","dataTone","dataDensity","dataSize","validate","issues","summary","issue","format","diagnostics","icon","s","d","cloneTree","value","isPlainObject","out","key","fix","root","options","tree","applied","walkFix","validate","node","path","index","child","tag","findTag","here","VOID"]}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import{HtmlTags as D,SvgTags as R,VoidTags as N}from"@domphy/core";import{cssRgbToRgb as _,hexToRgb as L,labToLch as O,rgbToLab as V}from"@domphy/palette";var I=new Set([...D,...R]),C=new Set(N),E=new Set(["$","style","_key","_portal","_context","_metadata"]),F=new Set(["fontSize","lineHeight","fontWeight","letterSpacing","fontFamily","textDecoration"]),M=new Set(["color","backgroundColor","background","borderColor","border","outlineColor","outline","fill","stroke"]),z=/#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?)\s*\(/,H=new Set(["margin","marginTop","marginRight","marginBottom","marginLeft","marginInline","marginBlock","marginInlineStart","marginInlineEnd","marginBlockStart","marginBlockEnd","padding","paddingTop","paddingRight","paddingBottom","paddingLeft","paddingInline","paddingBlock","paddingInlineStart","paddingInlineEnd","paddingBlockStart","paddingBlockEnd","gap","rowGap","columnGap"]),G=/^(\d+(?:\.\d+)?)(rem|em|px)$/;function y(e){let t=e.match(/^(increase|decrease|shift)-(\d+)$/);return t?{family:t[1],n:parseInt(t[2],10)}:null}function P(e){if(e==="inherit"||e==="base"||/^-?\d+$/.test(e))return!0;let t=y(e);return t?t.n<=17:!1}function j(e){try{let t=e.trim(),n;if(t.startsWith("#")){let i=t;if(i.length===9&&(i=i.slice(0,7)),i.length===5&&(i=i.slice(0,4)),i.length===4&&(i=`#${i[1]}${i[1]}${i[2]}${i[2]}${i[3]}${i[3]}`),i.length!==7)return null;n=L(i)}else if(/^rgba?\s*\(/.test(t))n=_(t);else return null;let r=V(n),c=O(r);return[c[0],c[1],c[2]]}catch(t){return null}}function B(e){let[t,n,r]=e,c=Math.round((t-50)/10),i=Math.max(-9,Math.min(9,c)),u;Math.abs(i)<=1?u='"base"':i<0?u=`"decrease-${Math.abs(i)}"`:u=`"increase-${i}"`;let o;return n<12?o="neutral":r<30||r>=330?o="error":r<75?o="warning":r<165?o="success":(r<265,o="primary"),`(l) => themeColor(l, ${u}, "${o}") [perceptual LCH L=${Math.round(t)} C=${Math.round(n)} h=${Math.round(r)}\xB0]`}function W(e,t){let n=G.exec(t);if(!n)return null;let r=parseFloat(n[1]),c=n[2],i;return c==="rem"||c==="em"?i=Math.round(r*4):i=Math.round(r/4),i<=0?null:`${e}: themeSpacing(${i}) \u2014 themeSpacing(n)=n/4em, so ${i}/4=${i/4}em \u2248 ${t} at default density`}function b(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function v(e){for(let t in e)if(I.has(t))return t}function T(e,t={}){let n=[];return k(e,"",n,!1,t.runReactive!==!1),n}function k(e,t,n,r,c){var x;if(typeof e=="function"){if(!c)return;let s;try{s=e(()=>{})}catch(l){return}k(s,t,n,!0,c);return}if(Array.isArray(e)){let s=e.filter(a=>b(a)&&v(a));r&&(s.length>1&&s.some(a=>a._key===void 0)&&n.push({rule:"missing-key",severity:"warning",path:t||"(list)",message:"Dynamic list child without `_key` \u2014 reordered/keyed lists need a stable `_key` for correct reconcile.",hint:"Add `_key: <stable id>` to each item produced by the reactive function."}),s.length>1&&s.every((a,f)=>a._key===f)&&n.push({rule:"unstable-key",severity:"warning",path:t||"(list)",message:"Dynamic list `_key` values are the array index (0, 1, 2, \u2026) \u2014 index keys are unstable across reorders/inserts.",hint:"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index."}));let l=new Map;for(let a of s){let f=a._key;if(f==null)continue;let g=`${typeof f}:${String(f)}`;l.set(g,((x=l.get(g))!=null?x:0)+1)}for(let[a,f]of l)if(f>1){let g=a.slice(a.indexOf(":")+1);n.push({rule:"duplicate-key",severity:"error",path:t||"(list)",message:`Duplicate \`_key\` "${g}" among ${f} siblings \u2014 keys must be unique within a list.`,hint:"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant)."})}e.forEach((a,f)=>{k(a,`${t}[${f}]`,n,!1,c)});return}if(!b(e))return;let i=e,u=v(i),o=u?t?`${t} > ${u}`:u:t||"(root)";if(!u){let s=Object.keys(i).filter(l=>!E.has(l)&&!l.startsWith("_on")&&!l.startsWith("on")&&!l.startsWith("data")&&!l.startsWith("aria"));s.length===1&&n.push({rule:"unknown-tag",severity:"warning",path:o,message:`"${s[0]}" is not a known HTML/SVG tag \u2014 likely a typo.`,hint:"An element's first key must be a valid tag (div, button, span, \u2026)."});return}let d=i[u];if(C.has(u)&&d!==null&&d!==void 0&&n.push({rule:"void-content",severity:"error",path:o,message:`Void tag "${u}" must have null content (got ${Array.isArray(d)?"array":typeof d}).`,hint:`Write { ${u}: null, \u2026 } and put attributes as sibling keys.`}),b(i.style)){let s=i.style;for(let l in s){let a=s[l];if(F.has(l)&&typeof a!="function"&&n.push({rule:"inline-typography",severity:"warning",path:o,message:`Inline \`${l}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."}),M.has(l)&&typeof a=="string"&&z.test(a)){let f=j(a),g=f?B(f):"(l) => themeColor(l, tone, colorName)";n.push({rule:"raw-theme-value",severity:"info",path:o,message:`Inline \`${l}\` uses a literal color (${a}).`,hint:`Prefer a theme token \u2014 ${g} \u2014 so theming and dark mode apply.`})}if(H.has(l)&&typeof a=="string"){let f=W(l,a);f&&n.push({rule:"raw-spacing-value",severity:"info",path:o,message:`Inline \`${l}: "${a}"\` uses a literal spacing value.`,hint:`Prefer themeSpacing() for theme density: ${f}`})}}}let h=i.dataTone;if(typeof h=="string")if(!P(h))n.push({rule:"unknown-tone",severity:"warning",path:o,message:`\`dataTone\` "${h}" is not a valid tone.`,hint:'Use "inherit", "base", a number, or "shift-N"/"increase-N"/"decrease-N" with N \u2264 17 (the ramp has 18 steps). Words like "surface"/"text" are not tones.'});else{let s=y(h);(s==null?void 0:s.family)==="shift"&&s.n>=4&&s.n<=13&&n.push({rule:"middle-surface-anchor",severity:"warning",path:o,message:`\`dataTone: "${h}"\` uses a mid-ramp surface anchor (steps 4\u201313). Child tones derived from this surface may clamp and collapse contrast.`,hint:"Prefer edge anchors: shift-0\u20133 for light surfaces, shift-14\u201317 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions."})}let p=i.dataDensity;if(typeof p=="string"&&p!=="inherit"){let s=y(p);!s||s.family==="shift"?n.push({rule:"unknown-density",severity:"warning",path:o,message:`\`dataDensity\` "${p}" is not a valid density offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20134. "shift-" is not valid for density.'}):s.n>4&&n.push({rule:"unknown-density",severity:"error",path:o,message:`\`dataDensity\` "${p}" N=${s.n} is out of range \u2014 the density scale has 5 steps (max offset: 4).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 4. Density factors: [0.75, 1, 1.5, 2, 2.5].'})}let m=i.dataSize;if(typeof m=="string"&&m!=="inherit"){let s=y(m);!s||s.family==="shift"?n.push({rule:"unknown-size",severity:"warning",path:o,message:`\`dataSize\` "${m}" is not a valid size offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20137. "shift-" is not valid for size.'}):s.n>7&&n.push({rule:"unknown-size",severity:"error",path:o,message:`\`dataSize\` "${m}" N=${s.n} is out of range \u2014 the size scale has 8 steps (max offset: 7).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 7.'})}k(d,o,n,!1,c)}function w(e,t={}){let n=T(e,t),r={error:0,warning:0,info:0,total:n.length};for(let c of n)r[c.severity]+=1;return{ok:r.error===0,issues:n,summary:r}}function U(e){if(e.length===0)return"\u2713 No issues found.";let t=n=>n==="error"?"\u2717":n==="warning"?"\u26A0":"i";return e.map(n=>`${t(n.severity)} [${n.rule}] ${n.path}
2
- ${n.message}${n.hint?`
3
- \u2192 ${n.hint}`:""}`).join(`
4
- `)}import{HtmlTags as K,SvgTags as Y,VoidTags as q}from"@domphy/core";var J=new Set([...K,...Y]),Q=new Set(q);function A(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function X(e){for(let t in e)if(J.has(t))return t}function $(e){if(Array.isArray(e))return e.map($);if(A(e)){let t={};for(let n in e)t[n]=$(e[n]);return t}return e}function Z(e,t={}){let n=$(e),r=[];return S(n,"",r),{tree:n,applied:r,report:w(n,t)}}function S(e,t,n){if(Array.isArray(e)){for(let[i,u]of e.entries())S(u,`${t}[${i}]`,n);return}if(!A(e))return;let r=X(e);if(!r)return;let c=t?`${t} > ${r}`:r;Q.has(r)&&e[r]!==null&&e[r]!==void 0&&(e[r]=null,n.push({rule:"void-content",path:c,message:`Void tag <${r}> cannot have content \u2014 cleared to null.`})),S(e[r],c,n)}export{T as diagnose,Z as fix,U as format,w as validate};
1
+ import{cssRgbToRgb as I,hexToRgb as V,labToLch as C,rgbToLab as E}from"@domphy/palette";import{HtmlTags as O,SvgTags as L,VoidTags as N}from"@domphy/core";var _=new Set([...O,...L]),$=new Set(N);function h(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}function b(n){for(let t in n)if(_.has(t))return t}var F=new Set(["$","style","_key","_portal","_context","_metadata"]),M=new Set(["fontSize","lineHeight","fontWeight","letterSpacing","fontFamily","textDecoration"]),z=new Set(["color","backgroundColor","background","borderColor","border","outlineColor","outline","fill","stroke"]),P=/#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?)\s*\(/,B=new Set(["margin","marginTop","marginRight","marginBottom","marginLeft","marginInline","marginBlock","marginInlineStart","marginInlineEnd","marginBlockStart","marginBlockEnd","padding","paddingTop","paddingRight","paddingBottom","paddingLeft","paddingInline","paddingBlock","paddingInlineStart","paddingInlineEnd","paddingBlockStart","paddingBlockEnd","gap","rowGap","columnGap"]),H=/^(\d+(?:\.\d+)?)(rem|em|px)$/;function k(n){let t=n.match(/^(increase|decrease|shift)-(\d+)$/);return t?{family:t[1],n:parseInt(t[2],10)}:null}function j(n){if(n==="inherit"||n==="base"||/^-?\d+$/.test(n))return!0;let t=k(n);return t?t.n<=17:!1}function G(n){try{let t=n.trim(),e;if(t.startsWith("#")){let i=t;if(i.length===9&&(i=i.slice(0,7)),i.length===5&&(i=i.slice(0,4)),i.length===4&&(i=`#${i[1]}${i[1]}${i[2]}${i[2]}${i[3]}${i[3]}`),i.length!==7)return null;e=V(i)}else if(/^rgba?\s*\(/.test(t))e=I(t);else return null;let r=E(e),c=C(r);return[c[0],c[1],c[2]]}catch(t){return null}}var W=/#[0-9a-fA-F]{3,8}\b|rgba?\s*\([^)]*\)/;function U(n){let t=W.exec(n);return t?t[0]:null}function K(n){let[t,e,r]=n,c=Math.round((t-50)/10),i=Math.max(-9,Math.min(9,c)),u;Math.abs(i)<=1?u='"base"':i<0?u=`"decrease-${Math.abs(i)}"`:u=`"increase-${i}"`;let o;return e<12?o="neutral":r<30||r>=330?o="error":r<75?o="warning":r<165?o="success":(r<265,o="primary"),`(l) => themeColor(l, ${u}, "${o}") [perceptual LCH L=${Math.round(t)} C=${Math.round(e)} h=${Math.round(r)}\xB0]`}function Y(n,t){let e=H.exec(t);if(!e)return null;let r=parseFloat(e[1]),c=e[2],i;return c==="rem"||c==="em"?i=Math.round(r*4):i=Math.round(r/4),i<=0?null:`${n}: themeSpacing(${i}) \u2014 themeSpacing(n)=n/4em, so ${i}/4=${i/4}em \u2248 ${t} at default density`}function A(n,t={}){let e=[];return w(n,"",e,!1,t.runReactive!==!1),e}function w(n,t,e,r,c){var D,T;if(typeof n=="function"){if(!c)return;let s;try{s=n(()=>{})}catch(l){return}w(s,t,e,!0,c);return}if(Array.isArray(n)){let s=n.filter(a=>h(a)&&b(a));r&&(s.length>1&&s.some(a=>a._key===void 0)&&e.push({rule:"missing-key",severity:"warning",path:t||"(list)",message:"Dynamic list child without `_key` \u2014 reordered/keyed lists need a stable `_key` for correct reconcile.",hint:"Add `_key: <stable id>` to each item produced by the reactive function."}),s.length>1&&s.every((a,f)=>a._key===f)&&e.push({rule:"unstable-key",severity:"warning",path:t||"(list)",message:"Dynamic list `_key` values are the array index (0, 1, 2, \u2026) \u2014 index keys are unstable across reorders/inserts.",hint:"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index."}));let l=new Map;for(let a of s){let f=a._key;if(f==null)continue;let g=`${typeof f}:${String(f)}`;l.set(g,((D=l.get(g))!=null?D:0)+1)}for(let[a,f]of l)if(f>1){let g=a.slice(a.indexOf(":")+1);e.push({rule:"duplicate-key",severity:"error",path:t||"(list)",message:`Duplicate \`_key\` "${g}" among ${f} siblings \u2014 keys must be unique within a list.`,hint:"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant)."})}n.forEach((a,f)=>{w(a,`${t}[${f}]`,e,!1,c)});return}if(!h(n))return;let i=n,u=b(i),o=u?t?`${t} > ${u}`:u:t||"(root)";if(!u){let s=Object.keys(i).filter(l=>!F.has(l)&&!l.startsWith("_on")&&!l.startsWith("on")&&!l.startsWith("data")&&!l.startsWith("aria"));s.length===1&&e.push({rule:"unknown-tag",severity:"warning",path:o,message:`"${s[0]}" is not a known HTML/SVG tag \u2014 likely a typo.`,hint:"An element's first key must be a valid tag (div, button, span, \u2026)."});return}let d=i[u];if($.has(u)&&d!==null&&d!==void 0&&e.push({rule:"void-content",severity:"error",path:o,message:`Void tag "${u}" must have null content (got ${Array.isArray(d)?"array":typeof d}).`,hint:`Write { ${u}: null, \u2026 } and put attributes as sibling keys.`}),h(i.style)){let s=i.style;for(let l in s){let a=s[l];if(M.has(l)&&typeof a!="function"&&e.push({rule:"inline-typography",severity:"warning",path:o,message:`Inline \`${l}\` \u2014 avoid inline typography styles.`,hint:"Use a typography patch (paragraph()/heading()/small()/strong()/\u2026) via $ so the theme owns the type scale."}),z.has(l)&&typeof a=="string"&&P.test(a)){let f=(T=U(a))!=null?T:a,g=G(f),R=g?K(g):"(l) => themeColor(l, tone, colorName)";e.push({rule:"raw-theme-value",severity:"info",path:o,message:`Inline \`${l}\` uses a literal color (${a}).`,hint:`Prefer a theme token \u2014 ${R} \u2014 so theming and dark mode apply.`})}if(B.has(l)&&typeof a=="string"){let f=Y(l,a);f&&e.push({rule:"raw-spacing-value",severity:"info",path:o,message:`Inline \`${l}: "${a}"\` uses a literal spacing value.`,hint:`Prefer themeSpacing() for theme density: ${f}`})}}}let p=i.dataTone;if(typeof p=="string")if(!j(p))e.push({rule:"unknown-tone",severity:"warning",path:o,message:`\`dataTone\` "${p}" is not a valid tone.`,hint:'Use "inherit", "base", a number, or "shift-N"/"increase-N"/"decrease-N" with N \u2264 17 (the ramp has 18 steps). Words like "surface"/"text" are not tones.'});else{let s=k(p);(s==null?void 0:s.family)==="shift"&&s.n>=4&&s.n<=13&&e.push({rule:"middle-surface-anchor",severity:"warning",path:o,message:`\`dataTone: "${p}"\` uses a mid-ramp surface anchor (steps 4\u201313). Child tones derived from this surface may clamp and collapse contrast.`,hint:"Prefer edge anchors: shift-0\u20133 for light surfaces, shift-14\u201317 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions."})}let m=i.dataDensity;if(typeof m=="string"&&m!=="inherit"){let s=k(m);!s||s.family==="shift"?e.push({rule:"unknown-density",severity:"warning",path:o,message:`\`dataDensity\` "${m}" is not a valid density offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20134. "shift-" is not valid for density.'}):s.n>4&&e.push({rule:"unknown-density",severity:"error",path:o,message:`\`dataDensity\` "${m}" N=${s.n} is out of range \u2014 the density scale has 5 steps (max offset: 4).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 4. Density factors: [0.75, 1, 1.5, 2, 2.5].'})}let y=i.dataSize;if(typeof y=="string"&&y!=="inherit"){let s=k(y);!s||s.family==="shift"?e.push({rule:"unknown-size",severity:"warning",path:o,message:`\`dataSize\` "${y}" is not a valid size offset.`,hint:'Use "inherit", "increase-N", or "decrease-N" where N is 0\u20137. "shift-" is not valid for size.'}):s.n>7&&e.push({rule:"unknown-size",severity:"error",path:o,message:`\`dataSize\` "${y}" N=${s.n} is out of range \u2014 the size scale has 8 steps (max offset: 7).`,hint:'Use "increase-N" or "decrease-N" where N \u2264 7.'})}w(d,o,e,!1,c)}function x(n,t={}){let e=A(n,t),r={error:0,warning:0,info:0,total:e.length};for(let c of e)r[c.severity]+=1;return{ok:r.error===0,issues:e,summary:r}}function q(n){if(n.length===0)return"\u2713 No issues found.";let t=e=>e==="error"?"\u2717":e==="warning"?"\u26A0":"i";return n.map(e=>`${t(e.severity)} [${e.rule}] ${e.path}
2
+ ${e.message}${e.hint?`
3
+ \u2192 ${e.hint}`:""}`).join(`
4
+ `)}function v(n){if(Array.isArray(n))return n.map(v);if(h(n)){let t={};for(let e in n)t[e]=v(n[e]);return t}return n}function J(n,t={}){let e=v(n),r=[];return S(e,"",r),{tree:e,applied:r,report:x(e,t)}}function S(n,t,e){if(Array.isArray(n)){for(let[i,u]of n.entries())S(u,`${t}[${i}]`,e);return}if(!h(n))return;let r=b(n);if(!r)return;let c=t?`${t} > ${r}`:r;$.has(r)&&n[r]!==null&&n[r]!==void 0&&(n[r]=null,e.push({rule:"void-content",path:c,message:`Void tag <${r}> cannot have content \u2014 cleared to null.`})),S(n[r],c,e)}export{A as diagnose,J as fix,q as format,x as validate};
5
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/diagnose.ts","../src/fix.ts"],"sourcesContent":["import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\nimport { cssRgbToRgb, hexToRgb, labToLch, rgbToLab } from \"@domphy/palette\";\n\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport interface Diagnostic {\n /** Rule id, e.g. \"inline-typography\". */\n rule: string;\n severity: Severity;\n /** Human path to the offending node, e.g. \"div > ul > li\". */\n path: string;\n message: string;\n /** How to fix it. */\n hint?: string;\n}\n\nexport interface DiagnoseOptions {\n /**\n * Invoke reactive content functions `(listener) => …` with a no-op listener to\n * analyze their output (catches missing `_key` in dynamic lists). Default true.\n * Set false if your reactive functions have side effects.\n */\n runReactive?: boolean;\n}\n\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\nconst VOID = new Set<string>(VoidTags);\nconst RESERVED = new Set([\n \"$\",\n \"style\",\n \"_key\",\n \"_portal\",\n \"_context\",\n \"_metadata\",\n]);\n\n// Typography style properties that must not be set inline — use patches instead.\n// Expanded from bench data: fontFamily + textDecoration were missing and caused\n// agents to write { style: { fontFamily: \"...\" } } without correction.\nconst TYPOGRAPHY_STYLE = new Set([\n \"fontSize\",\n \"lineHeight\",\n \"fontWeight\",\n \"letterSpacing\",\n \"fontFamily\",\n \"textDecoration\",\n]);\n\n// Color-bearing style props that should resolve through a theme token rather\n// than a literal value, so theming and dark mode apply. Shorthands\n// (background/border/outline) are included because they often carry a color.\nconst COLOR_STYLE = new Set([\n \"color\",\n \"backgroundColor\",\n \"background\",\n \"borderColor\",\n \"border\",\n \"outlineColor\",\n \"outline\",\n \"fill\",\n \"stroke\",\n]);\n\n// A literal color value: hex (#rgb … #rrggbbaa) or an rgb()/rgba()/hsl()/hsla()\n// function. Keywords like transparent/currentColor/inherit are intentionally\n// allowed — they carry no theme meaning.\nconst LITERAL_COLOR = /#[0-9a-fA-F]{3,8}\\b|\\b(?:rgba?|hsla?)\\s*\\(/;\n\n// Spacing style properties where literal rem/em/px values should use themeSpacing().\n// These are layout, not typography, but themeSpacing() ensures density consistency.\n// Logical properties (paddingBlock, paddingInline, etc.) are included — they are\n// used in Domphy patches and must also go through themeSpacing() for density scaling.\nconst SPACING_STYLE = new Set([\n \"margin\",\n \"marginTop\",\n \"marginRight\",\n \"marginBottom\",\n \"marginLeft\",\n \"marginInline\",\n \"marginBlock\",\n \"marginInlineStart\",\n \"marginInlineEnd\",\n \"marginBlockStart\",\n \"marginBlockEnd\",\n \"padding\",\n \"paddingTop\",\n \"paddingRight\",\n \"paddingBottom\",\n \"paddingLeft\",\n \"paddingInline\",\n \"paddingBlock\",\n \"paddingInlineStart\",\n \"paddingInlineEnd\",\n \"paddingBlockStart\",\n \"paddingBlockEnd\",\n \"gap\",\n \"rowGap\",\n \"columnGap\",\n]);\n\n// Matches literal spacing values like \"16px\", \"1.5rem\", \"2em\" but not \"auto\",\n// \"inherit\", \"0\" (unitless zero is fine), or computed values.\nconst LITERAL_SPACING = /^(\\d+(?:\\.\\d+)?)(rem|em|px)$/;\n\n// Parses \"increase-N\" / \"decrease-N\" / \"shift-N\" into family + numeric offset.\n// Returns null when the pattern doesn't match (grammar error).\nfunction parseOffset(\n value: string,\n): { family: \"increase\" | \"decrease\" | \"shift\"; n: number } | null {\n const m = value.match(/^(increase|decrease|shift)-(\\d+)$/);\n if (!m) return null;\n return {\n family: m[1] as \"increase\" | \"decrease\" | \"shift\",\n n: parseInt(m[2], 10),\n };\n}\n\n// Valid `dataTone` grammar AND range:\n// \"inherit\", \"base\", a bare integer, or shift-N/increase-N/decrease-N where N ≤ 17.\n// The default Domphy theme has 18 tone steps (0–17). Values with valid grammar\n// but N > 17 are also rejected here so they surface as `unknown-tone` errors.\nfunction isValidTone(value: string): boolean {\n if (value === \"inherit\" || value === \"base\") return true;\n if (/^-?\\d+$/.test(value)) return true;\n const parsed = parseOffset(value);\n if (!parsed) return false;\n return parsed.n <= 17; // tone ramp has 18 steps: 0–17\n}\n\n// ─── Chromametry integration ─────────────────────────────────────────────────\n\n/**\n * Parses a CSS color literal (hex or rgb/rgba) into LCH [L, C, h].\n * Returns null if parsing fails or the format is unsupported (named colors, hsl).\n * Uses @domphy/palette math (CIELAB via D65 reference white).\n */\nfunction parseLiteralToLch(value: string): [number, number, number] | null {\n try {\n const trimmed = value.trim();\n let rgb: number[];\n\n if (trimmed.startsWith(\"#\")) {\n let hex = trimmed;\n if (hex.length === 9) hex = hex.slice(0, 7); // strip alpha #rrggbbaa → #rrggbb\n if (hex.length === 5) hex = hex.slice(0, 4); // strip alpha #rgba → #rgb\n if (hex.length === 4) {\n hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;\n }\n if (hex.length !== 7) return null;\n rgb = hexToRgb(hex);\n } else if (/^rgba?\\s*\\(/.test(trimmed)) {\n rgb = cssRgbToRgb(trimmed);\n } else {\n return null; // hsl, named colors, custom-properties — skip\n }\n\n const lab = rgbToLab(rgb);\n const lch = labToLch(lab);\n return [lch[0], lch[1], lch[2]];\n } catch {\n return null;\n }\n}\n\n/**\n * Converts LCH coordinates into a concrete `themeColor()` call suggestion plus\n * a perceptual description. The tone and color-family are approximations for the\n * default Domphy theme (light, 10 neutral tones, base at mid-lightness).\n */\nfunction buildColorHint(lch: [number, number, number]): string {\n const [L, C, h] = lch;\n\n // Map lightness to a Domphy tone relative to base (~L50).\n // Each step ≈ 10 lightness units — clamp to ±9 (max offset in a 10-step ramp).\n const rawOffset = Math.round((L - 50) / 10);\n const offset = Math.max(-9, Math.min(9, rawOffset));\n let toneStr: string;\n if (Math.abs(offset) <= 1) toneStr = '\"base\"';\n else if (offset < 0) toneStr = `\"decrease-${Math.abs(offset)}\"`;\n else toneStr = `\"increase-${offset}\"`;\n\n // Infer the most likely semantic color family from chroma + hue.\n let colorFamily: string;\n if (C < 12) colorFamily = \"neutral\";\n else if (h < 30 || h >= 330)\n colorFamily = \"error\"; // red spectrum\n else if (h < 75)\n colorFamily = \"warning\"; // orange-yellow\n else if (h < 165)\n colorFamily = \"success\"; // green\n else if (h < 265)\n colorFamily = \"primary\"; // blue-indigo\n else colorFamily = \"primary\"; // violet → treat as primary\n\n return (\n `(l) => themeColor(l, ${toneStr}, \"${colorFamily}\") ` +\n `[perceptual LCH L=${Math.round(L)} C=${Math.round(C)} h=${Math.round(h)}°]`\n );\n}\n\n/**\n * Converts a literal spacing value like \"16px\" / \"1.5rem\" / \"2em\" into a\n * themeSpacing(n) suggestion. themeSpacing(n) = n/4 em, so n=4 → 1em ≈ 16px.\n */\nfunction buildSpacingHint(prop: string, value: string): string | null {\n const match = LITERAL_SPACING.exec(value);\n if (!match) return null;\n const amount = parseFloat(match[1]);\n const unit = match[2];\n let n: number;\n if (unit === \"rem\" || unit === \"em\") {\n n = Math.round(amount * 4);\n } else {\n // px: assume default 16px/rem → 1em = 16px\n n = Math.round(amount / 4);\n }\n if (n <= 0) return null;\n return `${prop}: themeSpacing(${n}) — themeSpacing(n)=n/4em, so ${n}/4=${n / 4}em ≈ ${value} at default density`;\n}\n\n// ─── Tree walkers ─────────────────────────────────────────────────────────────\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction findTag(element: Record<string, unknown>): string | undefined {\n for (const key in element) {\n if (TAGS.has(key)) return key;\n }\n return undefined;\n}\n\n/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */\nexport function diagnose(\n root: unknown,\n options: DiagnoseOptions = {},\n): Diagnostic[] {\n const out: Diagnostic[] = [];\n walk(root, \"\", out, false, options.runReactive !== false);\n return out;\n}\n\nfunction walk(\n node: unknown,\n path: string,\n out: Diagnostic[],\n dynamic: boolean,\n runReactive: boolean,\n): void {\n if (typeof node === \"function\") {\n if (!runReactive) return;\n let result: unknown;\n try {\n result = (node as (listener: unknown) => unknown)(() => {});\n } catch {\n return; // reactive fn threw without a real runtime — skip\n }\n walk(result, path, out, true, runReactive);\n return;\n }\n\n if (Array.isArray(node)) {\n const elementItems = node.filter(\n (child) => isPlainObject(child) && findTag(child),\n ) as Record<string, unknown>[];\n\n if (dynamic) {\n if (\n elementItems.length > 1 &&\n elementItems.some((item) => item._key === undefined)\n ) {\n out.push({\n rule: \"missing-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list child without `_key` — reordered/keyed lists need a stable `_key` for correct reconcile.\",\n hint: \"Add `_key: <stable id>` to each item produced by the reactive function.\",\n });\n }\n\n // unstable-key (heuristic): in a dynamic list every `_key` equals its\n // sibling position (0, 1, 2, …). That is the runtime footprint of\n // `items.map((item, i) => ({ …, _key: i }))` — an array-index key, which\n // defeats the point of keying because keys shift when the list reorders.\n if (\n elementItems.length > 1 &&\n elementItems.every((item, index) => item._key === index)\n ) {\n out.push({\n rule: \"unstable-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list `_key` values are the array index (0, 1, 2, …) — index keys are unstable across reorders/inserts.\",\n hint: \"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index.\",\n });\n }\n }\n\n // duplicate-key: two siblings sharing the same `_key` value break reconcile\n const seenKeys = new Map<string, number>();\n for (const item of elementItems) {\n const key = item._key;\n if (key === undefined || key === null) continue;\n const literalKey = `${typeof key}:${String(key)}`;\n seenKeys.set(literalKey, (seenKeys.get(literalKey) ?? 0) + 1);\n }\n for (const [literalKey, count] of seenKeys) {\n if (count > 1) {\n const value = literalKey.slice(literalKey.indexOf(\":\") + 1);\n out.push({\n rule: \"duplicate-key\",\n severity: \"error\",\n path: path || \"(list)\",\n message: `Duplicate \\`_key\\` \"${value}\" among ${count} siblings — keys must be unique within a list.`,\n hint: \"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant).\",\n });\n }\n }\n\n node.forEach((child, index) => {\n walk(child, `${path}[${index}]`, out, false, runReactive);\n });\n return;\n }\n\n if (!isPlainObject(node)) return;\n\n const element = node;\n const tag = findTag(element);\n const here = tag ? (path ? `${path} > ${tag}` : tag) : path || \"(root)\";\n\n if (!tag) {\n const contentKeys = Object.keys(element).filter(\n (key) =>\n !RESERVED.has(key) &&\n !key.startsWith(\"_on\") &&\n !key.startsWith(\"on\") &&\n !key.startsWith(\"data\") &&\n !key.startsWith(\"aria\"),\n );\n if (contentKeys.length === 1) {\n out.push({\n rule: \"unknown-tag\",\n severity: \"warning\",\n path: here,\n message: `\"${contentKeys[0]}\" is not a known HTML/SVG tag — likely a typo.`,\n hint: \"An element's first key must be a valid tag (div, button, span, …).\",\n });\n }\n return;\n }\n\n const content = element[tag];\n\n if (VOID.has(tag) && content !== null && content !== undefined) {\n out.push({\n rule: \"void-content\",\n severity: \"error\",\n path: here,\n message: `Void tag \"${tag}\" must have null content (got ${Array.isArray(content) ? \"array\" : typeof content}).`,\n hint: `Write { ${tag}: null, … } and put attributes as sibling keys.`,\n });\n }\n\n if (isPlainObject(element.style)) {\n const style = element.style;\n for (const prop in style) {\n const value = style[prop];\n\n // inline-typography: typography properties must come from patches, not\n // inline style. fontFamily and textDecoration were missing from the original\n // set and are added here based on bench data showing persistent violations.\n if (TYPOGRAPHY_STYLE.has(prop) && typeof value !== \"function\") {\n out.push({\n rule: \"inline-typography\",\n severity: \"warning\",\n path: here,\n message: `Inline \\`${prop}\\` — avoid inline typography styles.`,\n hint: \"Use a typography patch (paragraph()/heading()/small()/strong()/…) via $ so the theme owns the type scale.\",\n });\n }\n\n // raw-theme-value: literal color values bypass theming/dark mode.\n // Enhanced with @domphy/palette chromametry: converts the literal color to\n // LCH and suggests the nearest themeColor() call with perceptual coordinates.\n if (\n COLOR_STYLE.has(prop) &&\n typeof value === \"string\" &&\n LITERAL_COLOR.test(value)\n ) {\n const lch = parseLiteralToLch(value);\n const colorHint = lch\n ? buildColorHint(lch)\n : \"(l) => themeColor(l, tone, colorName)\";\n\n out.push({\n rule: \"raw-theme-value\",\n severity: \"info\",\n path: here,\n message: `Inline \\`${prop}\\` uses a literal color (${value}).`,\n hint: `Prefer a theme token — ${colorHint} — so theming and dark mode apply.`,\n });\n }\n\n // raw-spacing-value: literal rem/em/px spacing values should use themeSpacing()\n // to respect the theme's density system. info-severity (soft recommendation).\n if (SPACING_STYLE.has(prop) && typeof value === \"string\") {\n const spacingHint = buildSpacingHint(prop, value);\n if (spacingHint) {\n out.push({\n rule: \"raw-spacing-value\",\n severity: \"info\",\n path: here,\n message: `Inline \\`${prop}: \"${value}\"\\` uses a literal spacing value.`,\n hint: `Prefer themeSpacing() for theme density: ${spacingHint}`,\n });\n }\n }\n }\n }\n\n // unknown-tone: dataTone is not valid grammar, or it's valid grammar but the\n // numeric offset is out of the 18-step ramp range (0–17).\n const dataTone = element.dataTone;\n if (typeof dataTone === \"string\") {\n if (!isValidTone(dataTone)) {\n out.push({\n rule: \"unknown-tone\",\n severity: \"warning\",\n path: here,\n message: `\\`dataTone\\` \"${dataTone}\" is not a valid tone.`,\n hint: 'Use \"inherit\", \"base\", a number, or \"shift-N\"/\"increase-N\"/\"decrease-N\" with N ≤ 17 (the ramp has 18 steps). Words like \"surface\"/\"text\" are not tones.',\n });\n } else {\n // middle-surface-anchor: shift-4 through shift-13 sets a mid-ramp surface\n // anchor. Children's tones may clamp and fold back, collapsing the contrast\n // between background and text. Edge anchors (0–3 light, 14–17 dark) are safe.\n const parsed = parseOffset(dataTone);\n if (parsed?.family === \"shift\" && parsed.n >= 4 && parsed.n <= 13) {\n out.push({\n rule: \"middle-surface-anchor\",\n severity: \"warning\",\n path: here,\n message: `\\`dataTone: \"${dataTone}\"\\` uses a mid-ramp surface anchor (steps 4–13). Child tones derived from this surface may clamp and collapse contrast.`,\n hint: \"Prefer edge anchors: shift-0–3 for light surfaces, shift-14–17 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions.\",\n });\n }\n }\n }\n\n // unknown-density: dataDensity value is invalid grammar or out of the 5-step\n // density range (increase/decrease 0–4; the scale factors are 0.75, 1, 1.5, 2, 2.5).\n const dataDensity = element.dataDensity;\n if (typeof dataDensity === \"string\" && dataDensity !== \"inherit\") {\n const parsed = parseOffset(dataDensity);\n if (!parsed || parsed.family === \"shift\") {\n out.push({\n rule: \"unknown-density\",\n severity: \"warning\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" is not a valid density offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–4. \"shift-\" is not valid for density.',\n });\n } else if (parsed.n > 4) {\n out.push({\n rule: \"unknown-density\",\n severity: \"error\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" N=${parsed.n} is out of range — the density scale has 5 steps (max offset: 4).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 4. Density factors: [0.75, 1, 1.5, 2, 2.5].',\n });\n }\n }\n\n // unknown-size: dataSize value is invalid grammar or out of the 8-step size\n // range (increase/decrease 0–7).\n const dataSize = element.dataSize;\n if (typeof dataSize === \"string\" && dataSize !== \"inherit\") {\n const parsed = parseOffset(dataSize);\n if (!parsed || parsed.family === \"shift\") {\n out.push({\n rule: \"unknown-size\",\n severity: \"warning\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" is not a valid size offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–7. \"shift-\" is not valid for size.',\n });\n } else if (parsed.n > 7) {\n out.push({\n rule: \"unknown-size\",\n severity: \"error\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" N=${parsed.n} is out of range — the size scale has 8 steps (max offset: 7).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 7.',\n });\n }\n }\n\n walk(content, here, out, false, runReactive);\n}\n\n/** Issue counts by severity, plus the grand total. */\nexport interface ValidationSummary {\n error: number;\n warning: number;\n info: number;\n total: number;\n}\n\n/** Structured result of {@link validate}: pass/fail flag, issues, and counts. */\nexport interface ValidationReport {\n /** True when there are no `error`-severity diagnostics. */\n ok: boolean;\n /** Every diagnostic found, across all rules (alias of `diagnose` output). */\n issues: Diagnostic[];\n summary: ValidationSummary;\n}\n\n/**\n * Runs every diagnose rule and returns a structured report (pass/fail flag,\n * the issue list, and counts by severity). `ok` is false when any `error`\n * diagnostic is present; warnings/info do not flip `ok`. Use this as the single\n * programmatic entry point; `diagnose`/`format` remain available for raw access.\n */\nexport function validate(\n root: unknown,\n options: DiagnoseOptions = {},\n): ValidationReport {\n const issues = diagnose(root, options);\n const summary: ValidationSummary = {\n error: 0,\n warning: 0,\n info: 0,\n total: issues.length,\n };\n for (const issue of issues) summary[issue.severity] += 1;\n return { ok: summary.error === 0, issues, summary };\n}\n\n/** Formats diagnostics as a readable report (one line per issue). */\nexport function format(diagnostics: Diagnostic[]): string {\n if (diagnostics.length === 0) return \"✓ No issues found.\";\n const icon = (s: Severity) =>\n s === \"error\" ? \"✗\" : s === \"warning\" ? \"⚠\" : \"i\";\n return diagnostics\n .map(\n (d) =>\n `${icon(d.severity)} [${d.rule}] ${d.path}\\n ${d.message}${d.hint ? `\\n → ${d.hint}` : \"\"}`,\n )\n .join(\"\\n\");\n}\n","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\nimport {\n type DiagnoseOptions,\n type ValidationReport,\n validate,\n} from \"./diagnose.js\";\n\n// Autofix for Domphy element trees. We ONLY apply transforms that are provably\n// lossless — they fix structurally-invalid input without guessing intent. Every\n// other diagnostic (missing/unstable keys, inline typography, literal colors,\n// unknown tones/tags) needs semantic intent the tree does not carry, so applying\n// a \"fix\" would corrupt the author's meaning (e.g. an index key is itself the\n// unstable-key anti-pattern). Those are returned in `report` for the model or a\n// human to resolve. The fixer set is a registry so safe transforms can be added.\n\nconst TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\nconst VOID = new Set<string>(VoidTags);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction findTag(element: Record<string, unknown>): string | undefined {\n for (const key in element) {\n if (TAGS.has(key)) return key;\n }\n return undefined;\n}\n\n// Structural clone that preserves functions (reactive `(listener) => …` values)\n// by reference — a JSON clone would drop them. Primitives pass through.\nfunction cloneTree(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(cloneTree);\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const key in value) out[key] = cloneTree(value[key]);\n return out;\n }\n return value;\n}\n\n/** One applied lossless fix. */\nexport interface AppliedFix {\n rule: string;\n /** Human path to the node, e.g. \"div > input\". */\n path: string;\n message: string;\n}\n\n/** Result of {@link fix}: the corrected tree, what was applied, and what remains. */\nexport interface FixResult {\n /** A deep copy of the input with lossless fixes applied (functions preserved). */\n tree: unknown;\n /** The lossless fixes that were applied. */\n applied: AppliedFix[];\n /** validate() run on the fixed tree — `report.issues` are the manual remainder. */\n report: ValidationReport;\n}\n\n/**\n * Applies every provably-lossless fix to a copy of the tree and returns the\n * result plus a fresh validation report. Currently fixes `void-content` (a void\n * tag like input/img/br cannot have children, so its content is set to null).\n * Issues that need intent are left untouched and surface in `report` — this\n * includes `raw-spacing-value` and `raw-theme-value` (require semantic choices)\n * and key rules (require stable identity from data, not the tree shape).\n */\nexport function fix(root: unknown, options: DiagnoseOptions = {}): FixResult {\n const tree = cloneTree(root);\n const applied: AppliedFix[] = [];\n walkFix(tree, \"\", applied);\n return { tree, applied, report: validate(tree, options) };\n}\n\nfunction walkFix(node: unknown, path: string, applied: AppliedFix[]): void {\n if (Array.isArray(node)) {\n for (const [index, child] of node.entries()) {\n walkFix(child, `${path}[${index}]`, applied);\n }\n return;\n }\n if (!isPlainObject(node)) return;\n\n const tag = findTag(node);\n if (!tag) return;\n const here = path ? `${path} > ${tag}` : tag;\n\n // void-content: a void tag renders no children, so any content is invalid and\n // cannot be rendered — clearing it to null is lossless.\n if (VOID.has(tag) && node[tag] !== null && node[tag] !== undefined) {\n node[tag] = null;\n applied.push({\n rule: \"void-content\",\n path: here,\n message: `Void tag <${tag}> cannot have content — cleared to null.`,\n });\n }\n\n walkFix(node[tag], here, applied);\n}\n"],"mappings":"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","names":["HtmlTags","SvgTags","VoidTags","cssRgbToRgb","hexToRgb","labToLch","rgbToLab","TAGS","VOID","RESERVED","TYPOGRAPHY_STYLE","COLOR_STYLE","LITERAL_COLOR","SPACING_STYLE","LITERAL_SPACING","parseOffset","value","m","isValidTone","parsed","parseLiteralToLch","trimmed","rgb","hex","lab","lch","e","buildColorHint","L","C","h","rawOffset","offset","toneStr","colorFamily","buildSpacingHint","prop","match","amount","unit","n","isPlainObject","findTag","element","key","diagnose","root","options","out","walk","node","path","dynamic","runReactive","_a","result","elementItems","child","item","index","seenKeys","literalKey","count","tag","here","contentKeys","content","style","colorHint","spacingHint","dataTone","dataDensity","dataSize","validate","issues","summary","issue","format","diagnostics","icon","s","d","HtmlTags","SvgTags","VoidTags","TAGS","HtmlTags","SvgTags","VOID","VoidTags","isPlainObject","value","findTag","element","key","cloneTree","out","fix","root","options","tree","applied","walkFix","validate","node","path","index","child","tag","here"]}
1
+ {"version":3,"sources":["../src/diagnose.ts","../src/shared.ts","../src/fix.ts"],"sourcesContent":["import { cssRgbToRgb, hexToRgb, labToLch, rgbToLab } from \"@domphy/palette\";\nimport { findTag, isPlainObject, VOID } from \"./shared.js\";\n\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport interface Diagnostic {\n /** Rule id, e.g. \"inline-typography\". */\n rule: string;\n severity: Severity;\n /** Human path to the offending node, e.g. \"div > ul > li\". */\n path: string;\n message: string;\n /** How to fix it. */\n hint?: string;\n}\n\nexport interface DiagnoseOptions {\n /**\n * Invoke reactive content functions `(listener) => …` with a no-op listener to\n * analyze their output (catches missing `_key` in dynamic lists). Default true.\n * Set false if your reactive functions have side effects.\n */\n runReactive?: boolean;\n}\n\nconst RESERVED = new Set([\n \"$\",\n \"style\",\n \"_key\",\n \"_portal\",\n \"_context\",\n \"_metadata\",\n]);\n\n// Typography style properties that must not be set inline — use patches instead.\n// Expanded from bench data: fontFamily + textDecoration were missing and caused\n// agents to write { style: { fontFamily: \"...\" } } without correction.\nconst TYPOGRAPHY_STYLE = new Set([\n \"fontSize\",\n \"lineHeight\",\n \"fontWeight\",\n \"letterSpacing\",\n \"fontFamily\",\n \"textDecoration\",\n]);\n\n// Color-bearing style props that should resolve through a theme token rather\n// than a literal value, so theming and dark mode apply. Shorthands\n// (background/border/outline) are included because they often carry a color.\nconst COLOR_STYLE = new Set([\n \"color\",\n \"backgroundColor\",\n \"background\",\n \"borderColor\",\n \"border\",\n \"outlineColor\",\n \"outline\",\n \"fill\",\n \"stroke\",\n]);\n\n// A literal color value: hex (#rgb … #rrggbbaa) or an rgb()/rgba()/hsl()/hsla()\n// function. Keywords like transparent/currentColor/inherit are intentionally\n// allowed — they carry no theme meaning.\nconst LITERAL_COLOR = /#[0-9a-fA-F]{3,8}\\b|\\b(?:rgba?|hsla?)\\s*\\(/;\n\n// Spacing style properties where literal rem/em/px values should use themeSpacing().\n// These are layout, not typography, but themeSpacing() ensures density consistency.\n// Logical properties (paddingBlock, paddingInline, etc.) are included — they are\n// used in Domphy patches and must also go through themeSpacing() for density scaling.\nconst SPACING_STYLE = new Set([\n \"margin\",\n \"marginTop\",\n \"marginRight\",\n \"marginBottom\",\n \"marginLeft\",\n \"marginInline\",\n \"marginBlock\",\n \"marginInlineStart\",\n \"marginInlineEnd\",\n \"marginBlockStart\",\n \"marginBlockEnd\",\n \"padding\",\n \"paddingTop\",\n \"paddingRight\",\n \"paddingBottom\",\n \"paddingLeft\",\n \"paddingInline\",\n \"paddingBlock\",\n \"paddingInlineStart\",\n \"paddingInlineEnd\",\n \"paddingBlockStart\",\n \"paddingBlockEnd\",\n \"gap\",\n \"rowGap\",\n \"columnGap\",\n]);\n\n// Matches literal spacing values like \"16px\", \"1.5rem\", \"2em\" but not \"auto\",\n// \"inherit\", \"0\" (unitless zero is fine), or computed values.\nconst LITERAL_SPACING = /^(\\d+(?:\\.\\d+)?)(rem|em|px)$/;\n\n// Parses \"increase-N\" / \"decrease-N\" / \"shift-N\" into family + numeric offset.\n// Returns null when the pattern doesn't match (grammar error).\nfunction parseOffset(\n value: string,\n): { family: \"increase\" | \"decrease\" | \"shift\"; n: number } | null {\n const m = value.match(/^(increase|decrease|shift)-(\\d+)$/);\n if (!m) return null;\n return {\n family: m[1] as \"increase\" | \"decrease\" | \"shift\",\n n: parseInt(m[2], 10),\n };\n}\n\n// Valid `dataTone` grammar AND range:\n// \"inherit\", \"base\", a bare integer, or shift-N/increase-N/decrease-N where N ≤ 17.\n// The default Domphy theme has 18 tone steps (0–17). Values with valid grammar\n// but N > 17 are also rejected here so they surface as `unknown-tone` errors.\nfunction isValidTone(value: string): boolean {\n if (value === \"inherit\" || value === \"base\") return true;\n if (/^-?\\d+$/.test(value)) return true;\n const parsed = parseOffset(value);\n if (!parsed) return false;\n return parsed.n <= 17; // tone ramp has 18 steps: 0–17\n}\n\n// ─── Chromametry integration ─────────────────────────────────────────────────\n\n/**\n * Parses a CSS color literal (hex or rgb/rgba) into LCH [L, C, h].\n * Returns null if parsing fails or the format is unsupported (named colors, hsl).\n * Uses @domphy/palette math (CIELAB via D65 reference white).\n */\nfunction parseLiteralToLch(value: string): [number, number, number] | null {\n try {\n const trimmed = value.trim();\n let rgb: number[];\n\n if (trimmed.startsWith(\"#\")) {\n let hex = trimmed;\n if (hex.length === 9) hex = hex.slice(0, 7); // strip alpha #rrggbbaa → #rrggbb\n if (hex.length === 5) hex = hex.slice(0, 4); // strip alpha #rgba → #rgb\n if (hex.length === 4) {\n hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;\n }\n if (hex.length !== 7) return null;\n rgb = hexToRgb(hex);\n } else if (/^rgba?\\s*\\(/.test(trimmed)) {\n rgb = cssRgbToRgb(trimmed);\n } else {\n return null; // hsl, named colors, custom-properties — skip\n }\n\n const lab = rgbToLab(rgb);\n const lch = labToLch(lab);\n return [lch[0], lch[1], lch[2]];\n } catch {\n return null;\n }\n}\n\n// Pulls the first parseable color token out of a (possibly shorthand) value.\n// Shorthands such as \"1px solid #ccc\" or \"0 0 4px rgba(0,0,0,.5)\" embed the\n// color among other tokens, so `parseLiteralToLch` (which expects the value to\n// START with the color) would otherwise miss it. Matches a #hex literal or a\n// complete rgb()/rgba() call (with its arguments). hsl()/named colors are left\n// for the generic fallback, matching parseLiteralToLch's own coverage.\nconst EMBEDDED_COLOR = /#[0-9a-fA-F]{3,8}\\b|rgba?\\s*\\([^)]*\\)/;\n\nfunction extractColorLiteral(value: string): string | null {\n const match = EMBEDDED_COLOR.exec(value);\n return match ? match[0] : null;\n}\n\n/**\n * Converts LCH coordinates into a concrete `themeColor()` call suggestion plus\n * a perceptual description. The tone and color-family are approximations for the\n * default Domphy theme (light, 10 neutral tones, base at mid-lightness).\n */\nfunction buildColorHint(lch: [number, number, number]): string {\n const [L, C, h] = lch;\n\n // Map lightness to a Domphy tone relative to base (~L50).\n // Each step ≈ 10 lightness units — clamp to ±9 (max offset in a 10-step ramp).\n const rawOffset = Math.round((L - 50) / 10);\n const offset = Math.max(-9, Math.min(9, rawOffset));\n let toneStr: string;\n if (Math.abs(offset) <= 1) toneStr = '\"base\"';\n else if (offset < 0) toneStr = `\"decrease-${Math.abs(offset)}\"`;\n else toneStr = `\"increase-${offset}\"`;\n\n // Infer the most likely semantic color family from chroma + hue.\n let colorFamily: string;\n if (C < 12) colorFamily = \"neutral\";\n else if (h < 30 || h >= 330)\n colorFamily = \"error\"; // red spectrum\n else if (h < 75)\n colorFamily = \"warning\"; // orange-yellow\n else if (h < 165)\n colorFamily = \"success\"; // green\n else if (h < 265)\n colorFamily = \"primary\"; // blue-indigo\n else colorFamily = \"primary\"; // violet → treat as primary\n\n return (\n `(l) => themeColor(l, ${toneStr}, \"${colorFamily}\") ` +\n `[perceptual LCH L=${Math.round(L)} C=${Math.round(C)} h=${Math.round(h)}°]`\n );\n}\n\n/**\n * Converts a literal spacing value like \"16px\" / \"1.5rem\" / \"2em\" into a\n * themeSpacing(n) suggestion. themeSpacing(n) = n/4 em, so n=4 → 1em ≈ 16px.\n */\nfunction buildSpacingHint(prop: string, value: string): string | null {\n const match = LITERAL_SPACING.exec(value);\n if (!match) return null;\n const amount = parseFloat(match[1]);\n const unit = match[2];\n let n: number;\n if (unit === \"rem\" || unit === \"em\") {\n n = Math.round(amount * 4);\n } else {\n // px: assume default 16px/rem → 1em = 16px\n n = Math.round(amount / 4);\n }\n if (n <= 0) return null;\n return `${prop}: themeSpacing(${n}) — themeSpacing(n)=n/4em, so ${n}/4=${n / 4}em ≈ ${value} at default density`;\n}\n\n// ─── Tree walkers ─────────────────────────────────────────────────────────────\n\n/** Statically analyzes a Domphy element tree and returns idiomatic-usage diagnostics. */\nexport function diagnose(\n root: unknown,\n options: DiagnoseOptions = {},\n): Diagnostic[] {\n const out: Diagnostic[] = [];\n walk(root, \"\", out, false, options.runReactive !== false);\n return out;\n}\n\nfunction walk(\n node: unknown,\n path: string,\n out: Diagnostic[],\n dynamic: boolean,\n runReactive: boolean,\n): void {\n if (typeof node === \"function\") {\n if (!runReactive) return;\n let result: unknown;\n try {\n result = (node as (listener: unknown) => unknown)(() => {});\n } catch {\n return; // reactive fn threw without a real runtime — skip\n }\n walk(result, path, out, true, runReactive);\n return;\n }\n\n if (Array.isArray(node)) {\n const elementItems = node.filter(\n (child) => isPlainObject(child) && findTag(child),\n ) as Record<string, unknown>[];\n\n if (dynamic) {\n if (\n elementItems.length > 1 &&\n elementItems.some((item) => item._key === undefined)\n ) {\n out.push({\n rule: \"missing-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list child without `_key` — reordered/keyed lists need a stable `_key` for correct reconcile.\",\n hint: \"Add `_key: <stable id>` to each item produced by the reactive function.\",\n });\n }\n\n // unstable-key (heuristic): in a dynamic list every `_key` equals its\n // sibling position (0, 1, 2, …). That is the runtime footprint of\n // `items.map((item, i) => ({ …, _key: i }))` — an array-index key, which\n // defeats the point of keying because keys shift when the list reorders.\n if (\n elementItems.length > 1 &&\n elementItems.every((item, index) => item._key === index)\n ) {\n out.push({\n rule: \"unstable-key\",\n severity: \"warning\",\n path: path || \"(list)\",\n message:\n \"Dynamic list `_key` values are the array index (0, 1, 2, …) — index keys are unstable across reorders/inserts.\",\n hint: \"Key by a stable identity from the data (e.g. `_key: item.id`), not the loop index.\",\n });\n }\n }\n\n // duplicate-key: two siblings sharing the same `_key` value break reconcile\n const seenKeys = new Map<string, number>();\n for (const item of elementItems) {\n const key = item._key;\n if (key === undefined || key === null) continue;\n const literalKey = `${typeof key}:${String(key)}`;\n seenKeys.set(literalKey, (seenKeys.get(literalKey) ?? 0) + 1);\n }\n for (const [literalKey, count] of seenKeys) {\n if (count > 1) {\n const value = literalKey.slice(literalKey.indexOf(\":\") + 1);\n out.push({\n rule: \"duplicate-key\",\n severity: \"error\",\n path: path || \"(list)\",\n message: `Duplicate \\`_key\\` \"${value}\" among ${count} siblings — keys must be unique within a list.`,\n hint: \"Give each sibling a distinct stable `_key` (e.g. a record id, not a constant).\",\n });\n }\n }\n\n node.forEach((child, index) => {\n walk(child, `${path}[${index}]`, out, false, runReactive);\n });\n return;\n }\n\n if (!isPlainObject(node)) return;\n\n const element = node;\n const tag = findTag(element);\n const here = tag ? (path ? `${path} > ${tag}` : tag) : path || \"(root)\";\n\n if (!tag) {\n const contentKeys = Object.keys(element).filter(\n (key) =>\n !RESERVED.has(key) &&\n !key.startsWith(\"_on\") &&\n !key.startsWith(\"on\") &&\n !key.startsWith(\"data\") &&\n !key.startsWith(\"aria\"),\n );\n if (contentKeys.length === 1) {\n out.push({\n rule: \"unknown-tag\",\n severity: \"warning\",\n path: here,\n message: `\"${contentKeys[0]}\" is not a known HTML/SVG tag — likely a typo.`,\n hint: \"An element's first key must be a valid tag (div, button, span, …).\",\n });\n }\n return;\n }\n\n const content = element[tag];\n\n if (VOID.has(tag) && content !== null && content !== undefined) {\n out.push({\n rule: \"void-content\",\n severity: \"error\",\n path: here,\n message: `Void tag \"${tag}\" must have null content (got ${Array.isArray(content) ? \"array\" : typeof content}).`,\n hint: `Write { ${tag}: null, … } and put attributes as sibling keys.`,\n });\n }\n\n if (isPlainObject(element.style)) {\n const style = element.style;\n for (const prop in style) {\n const value = style[prop];\n\n // inline-typography: typography properties must come from patches, not\n // inline style. fontFamily and textDecoration were missing from the original\n // set and are added here based on bench data showing persistent violations.\n if (TYPOGRAPHY_STYLE.has(prop) && typeof value !== \"function\") {\n out.push({\n rule: \"inline-typography\",\n severity: \"warning\",\n path: here,\n message: `Inline \\`${prop}\\` — avoid inline typography styles.`,\n hint: \"Use a typography patch (paragraph()/heading()/small()/strong()/…) via $ so the theme owns the type scale.\",\n });\n }\n\n // raw-theme-value: literal color values bypass theming/dark mode.\n // Enhanced with @domphy/palette chromametry: converts the literal color to\n // LCH and suggests the nearest themeColor() call with perceptual coordinates.\n if (\n COLOR_STYLE.has(prop) &&\n typeof value === \"string\" &&\n LITERAL_COLOR.test(value)\n ) {\n // For shorthands (border/outline/background) the color is embedded in a\n // larger string, so extract the color token first; fall back to the\n // whole value (covers the simple `color: \"#fff\"` case).\n const colorLiteral = extractColorLiteral(value) ?? value;\n const lch = parseLiteralToLch(colorLiteral);\n const colorHint = lch\n ? buildColorHint(lch)\n : \"(l) => themeColor(l, tone, colorName)\";\n\n out.push({\n rule: \"raw-theme-value\",\n severity: \"info\",\n path: here,\n message: `Inline \\`${prop}\\` uses a literal color (${value}).`,\n hint: `Prefer a theme token — ${colorHint} — so theming and dark mode apply.`,\n });\n }\n\n // raw-spacing-value: literal rem/em/px spacing values should use themeSpacing()\n // to respect the theme's density system. info-severity (soft recommendation).\n if (SPACING_STYLE.has(prop) && typeof value === \"string\") {\n const spacingHint = buildSpacingHint(prop, value);\n if (spacingHint) {\n out.push({\n rule: \"raw-spacing-value\",\n severity: \"info\",\n path: here,\n message: `Inline \\`${prop}: \"${value}\"\\` uses a literal spacing value.`,\n hint: `Prefer themeSpacing() for theme density: ${spacingHint}`,\n });\n }\n }\n }\n }\n\n // unknown-tone: dataTone is not valid grammar, or it's valid grammar but the\n // numeric offset is out of the 18-step ramp range (0–17).\n const dataTone = element.dataTone;\n if (typeof dataTone === \"string\") {\n if (!isValidTone(dataTone)) {\n out.push({\n rule: \"unknown-tone\",\n severity: \"warning\",\n path: here,\n message: `\\`dataTone\\` \"${dataTone}\" is not a valid tone.`,\n hint: 'Use \"inherit\", \"base\", a number, or \"shift-N\"/\"increase-N\"/\"decrease-N\" with N ≤ 17 (the ramp has 18 steps). Words like \"surface\"/\"text\" are not tones.',\n });\n } else {\n // middle-surface-anchor: shift-4 through shift-13 sets a mid-ramp surface\n // anchor. Children's tones may clamp and fold back, collapsing the contrast\n // between background and text. Edge anchors (0–3 light, 14–17 dark) are safe.\n const parsed = parseOffset(dataTone);\n if (parsed?.family === \"shift\" && parsed.n >= 4 && parsed.n <= 13) {\n out.push({\n rule: \"middle-surface-anchor\",\n severity: \"warning\",\n path: here,\n message: `\\`dataTone: \"${dataTone}\"\\` uses a mid-ramp surface anchor (steps 4–13). Child tones derived from this surface may clamp and collapse contrast.`,\n hint: \"Prefer edge anchors: shift-0–3 for light surfaces, shift-14–17 for dark. Mid anchors are only correct for intentionally inverted/highlighted regions.\",\n });\n }\n }\n }\n\n // unknown-density: dataDensity value is invalid grammar or out of the 5-step\n // density range (increase/decrease 0–4; the scale factors are 0.75, 1, 1.5, 2, 2.5).\n const dataDensity = element.dataDensity;\n if (typeof dataDensity === \"string\" && dataDensity !== \"inherit\") {\n const parsed = parseOffset(dataDensity);\n if (!parsed || parsed.family === \"shift\") {\n out.push({\n rule: \"unknown-density\",\n severity: \"warning\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" is not a valid density offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–4. \"shift-\" is not valid for density.',\n });\n } else if (parsed.n > 4) {\n out.push({\n rule: \"unknown-density\",\n severity: \"error\",\n path: here,\n message: `\\`dataDensity\\` \"${dataDensity}\" N=${parsed.n} is out of range — the density scale has 5 steps (max offset: 4).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 4. Density factors: [0.75, 1, 1.5, 2, 2.5].',\n });\n }\n }\n\n // unknown-size: dataSize value is invalid grammar or out of the 8-step size\n // range (increase/decrease 0–7).\n const dataSize = element.dataSize;\n if (typeof dataSize === \"string\" && dataSize !== \"inherit\") {\n const parsed = parseOffset(dataSize);\n if (!parsed || parsed.family === \"shift\") {\n out.push({\n rule: \"unknown-size\",\n severity: \"warning\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" is not a valid size offset.`,\n hint: 'Use \"inherit\", \"increase-N\", or \"decrease-N\" where N is 0–7. \"shift-\" is not valid for size.',\n });\n } else if (parsed.n > 7) {\n out.push({\n rule: \"unknown-size\",\n severity: \"error\",\n path: here,\n message: `\\`dataSize\\` \"${dataSize}\" N=${parsed.n} is out of range — the size scale has 8 steps (max offset: 7).`,\n hint: 'Use \"increase-N\" or \"decrease-N\" where N ≤ 7.',\n });\n }\n }\n\n walk(content, here, out, false, runReactive);\n}\n\n/** Issue counts by severity, plus the grand total. */\nexport interface ValidationSummary {\n error: number;\n warning: number;\n info: number;\n total: number;\n}\n\n/** Structured result of {@link validate}: pass/fail flag, issues, and counts. */\nexport interface ValidationReport {\n /** True when there are no `error`-severity diagnostics. */\n ok: boolean;\n /** Every diagnostic found, across all rules (alias of `diagnose` output). */\n issues: Diagnostic[];\n summary: ValidationSummary;\n}\n\n/**\n * Runs every diagnose rule and returns a structured report (pass/fail flag,\n * the issue list, and counts by severity). `ok` is false when any `error`\n * diagnostic is present; warnings/info do not flip `ok`. Use this as the single\n * programmatic entry point; `diagnose`/`format` remain available for raw access.\n */\nexport function validate(\n root: unknown,\n options: DiagnoseOptions = {},\n): ValidationReport {\n const issues = diagnose(root, options);\n const summary: ValidationSummary = {\n error: 0,\n warning: 0,\n info: 0,\n total: issues.length,\n };\n for (const issue of issues) summary[issue.severity] += 1;\n return { ok: summary.error === 0, issues, summary };\n}\n\n/** Formats diagnostics as a readable report (one line per issue). */\nexport function format(diagnostics: Diagnostic[]): string {\n if (diagnostics.length === 0) return \"✓ No issues found.\";\n const icon = (s: Severity) =>\n s === \"error\" ? \"✗\" : s === \"warning\" ? \"⚠\" : \"i\";\n return diagnostics\n .map(\n (d) =>\n `${icon(d.severity)} [${d.rule}] ${d.path}\\n ${d.message}${d.hint ? `\\n → ${d.hint}` : \"\"}`,\n )\n .join(\"\\n\");\n}\n","import { HtmlTags, SvgTags, VoidTags } from \"@domphy/core\";\n\n// Internal helpers shared by diagnose.ts and fix.ts. Kept in one module so the\n// tag tables and the tree-shape predicates have a single source of truth.\n\n/** Every valid HTML and SVG tag name. */\nexport const TAGS = new Set<string>([...HtmlTags, ...SvgTags]);\n\n/** Tags that render no children (input, img, br, …). */\nexport const VOID = new Set<string>(VoidTags);\n\n/** True for a non-array object (a Domphy element or a plain record). */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Returns the element's tag key (the first key that names a valid tag). */\nexport function findTag(element: Record<string, unknown>): string | undefined {\n for (const key in element) {\n if (TAGS.has(key)) return key;\n }\n return undefined;\n}\n","import {\n type DiagnoseOptions,\n type ValidationReport,\n validate,\n} from \"./diagnose.js\";\nimport { findTag, isPlainObject, VOID } from \"./shared.js\";\n\n// Autofix for Domphy element trees. We ONLY apply transforms that are provably\n// lossless — they fix structurally-invalid input without guessing intent. Every\n// other diagnostic (missing/unstable keys, inline typography, literal colors,\n// unknown tones/tags) needs semantic intent the tree does not carry, so applying\n// a \"fix\" would corrupt the author's meaning (e.g. an index key is itself the\n// unstable-key anti-pattern). Those are returned in `report` for the model or a\n// human to resolve. The fixer set is a registry so safe transforms can be added.\n\n// Structural clone that preserves functions (reactive `(listener) => …` values)\n// by reference — a JSON clone would drop them. Primitives pass through.\nfunction cloneTree(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(cloneTree);\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const key in value) out[key] = cloneTree(value[key]);\n return out;\n }\n return value;\n}\n\n/** One applied lossless fix. */\nexport interface AppliedFix {\n rule: string;\n /** Human path to the node, e.g. \"div > input\". */\n path: string;\n message: string;\n}\n\n/** Result of {@link fix}: the corrected tree, what was applied, and what remains. */\nexport interface FixResult {\n /** A deep copy of the input with lossless fixes applied (functions preserved). */\n tree: unknown;\n /** The lossless fixes that were applied. */\n applied: AppliedFix[];\n /** validate() run on the fixed tree — `report.issues` are the manual remainder. */\n report: ValidationReport;\n}\n\n/**\n * Applies every provably-lossless fix to a copy of the tree and returns the\n * result plus a fresh validation report. Currently fixes `void-content` (a void\n * tag like input/img/br cannot have children, so its content is set to null).\n * Issues that need intent are left untouched and surface in `report` — this\n * includes `raw-spacing-value` and `raw-theme-value` (require semantic choices)\n * and key rules (require stable identity from data, not the tree shape).\n */\nexport function fix(root: unknown, options: DiagnoseOptions = {}): FixResult {\n const tree = cloneTree(root);\n const applied: AppliedFix[] = [];\n walkFix(tree, \"\", applied);\n return { tree, applied, report: validate(tree, options) };\n}\n\nfunction walkFix(node: unknown, path: string, applied: AppliedFix[]): void {\n if (Array.isArray(node)) {\n for (const [index, child] of node.entries()) {\n walkFix(child, `${path}[${index}]`, applied);\n }\n return;\n }\n if (!isPlainObject(node)) return;\n\n const tag = findTag(node);\n if (!tag) return;\n const here = path ? `${path} > ${tag}` : tag;\n\n // void-content: a void tag renders no children, so any content is invalid and\n // cannot be rendered — clearing it to null is lossless.\n if (VOID.has(tag) && node[tag] !== null && node[tag] !== undefined) {\n node[tag] = null;\n applied.push({\n rule: \"void-content\",\n path: here,\n message: `Void tag <${tag}> cannot have content — cleared to null.`,\n });\n }\n\n walkFix(node[tag], here, applied);\n}\n"],"mappings":"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","names":["cssRgbToRgb","hexToRgb","labToLch","rgbToLab","HtmlTags","SvgTags","VoidTags","TAGS","VOID","isPlainObject","value","findTag","element","key","RESERVED","TYPOGRAPHY_STYLE","COLOR_STYLE","LITERAL_COLOR","SPACING_STYLE","LITERAL_SPACING","parseOffset","value","m","isValidTone","parsed","parseLiteralToLch","trimmed","rgb","hex","hexToRgb","cssRgbToRgb","lab","rgbToLab","lch","labToLch","e","EMBEDDED_COLOR","extractColorLiteral","match","buildColorHint","L","C","h","rawOffset","offset","toneStr","colorFamily","buildSpacingHint","prop","amount","unit","n","diagnose","root","options","out","walk","node","path","dynamic","runReactive","_a","_b","result","elementItems","child","isPlainObject","findTag","item","index","seenKeys","key","literalKey","count","element","tag","here","contentKeys","content","VOID","style","colorLiteral","colorHint","spacingHint","dataTone","dataDensity","dataSize","validate","issues","summary","issue","format","diagnostics","icon","s","d","cloneTree","value","isPlainObject","out","key","fix","root","options","tree","applied","walkFix","validate","node","path","index","child","tag","findTag","here","VOID"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domphy/doctor",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Domphy Doctor - static analyzer that flags non-idiomatic Domphy element trees (AI self-correction)",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -33,18 +33,18 @@
33
33
  "directory": "packages/doctor"
34
34
  },
35
35
  "dependencies": {
36
- "@domphy/palette": "^0.17.0"
36
+ "@domphy/palette": "^0.18.0"
37
37
  },
38
38
  "peerDependencies": {
39
- "@domphy/core": "^0.17.0"
39
+ "@domphy/core": "^0.18.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^25.9.2",
43
43
  "tsup": "^8.5.0",
44
44
  "typescript": "^5.8.3",
45
45
  "vitest": "^4.0.18",
46
- "@domphy/core": "0.17.0",
47
- "@domphy/palette": "0.17.0"
46
+ "@domphy/core": "0.18.0",
47
+ "@domphy/palette": "0.18.0"
48
48
  },
49
49
  "files": [
50
50
  "dist",