@mtdt/observeops-ds-spec 0.1.6 → 0.1.8

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.
@@ -370,5 +370,20 @@
370
370
  },
371
371
  "slots": {
372
372
  "default": "button label / content (text and/or an icon)"
373
- }
374
- }
373
+ },
374
+ "variantEnum": [
375
+ "primary",
376
+ "primary-alt",
377
+ "default",
378
+ "neutral-lighter",
379
+ "neutral-lightest",
380
+ "transparent",
381
+ "error",
382
+ "success",
383
+ "danger",
384
+ "info",
385
+ "neutral",
386
+ "neutral-light",
387
+ "warning"
388
+ ]
389
+ }
@@ -182,5 +182,20 @@
182
182
  ],
183
183
  "doc": "Atoms/Link/Accessibility",
184
184
  "$note": "Catalogue-wide gap SF-001 = no visible :focus-visible ring; tracked in findings/."
185
- }
185
+ },
186
+ "variantEnum": [
187
+ "primary",
188
+ "primary-alt",
189
+ "default",
190
+ "neutral-lighter",
191
+ "neutral-lightest",
192
+ "transparent",
193
+ "error",
194
+ "success",
195
+ "danger",
196
+ "info",
197
+ "neutral",
198
+ "neutral-light",
199
+ "warning"
200
+ ]
186
201
  }
@@ -179,5 +179,19 @@
179
179
  "changelog": [
180
180
  "2026-06-27: added to the Elements site — 13 severity levels in dot / dot+label / solid-chip / coloured-text / bg-fill shapes; source-accurate 11px dot (the Storybook reproduction enlarges to 14px).",
181
181
  "2026-06-29: stripe shown in both real usages — the standalone bar AND the table/list left-rule row."
182
+ ],
183
+ "severityEnum": [
184
+ "down",
185
+ "critical",
186
+ "major",
187
+ "warning",
188
+ "clear",
189
+ "up",
190
+ "maintenance",
191
+ "unreachable",
192
+ "disable",
193
+ "unknown",
194
+ "suspended",
195
+ "none"
182
196
  ]
183
197
  }
@@ -341,5 +341,14 @@
341
341
  ],
342
342
  "doc": "Atoms/Tag/Accessibility",
343
343
  "$note": "Catalogue-wide gap SF-001 = no visible :focus-visible ring; tracked in findings/."
344
- }
344
+ },
345
+ "variantEnum": [
346
+ "tag-primary",
347
+ "tag-green",
348
+ "tag-red",
349
+ "tag-yellow",
350
+ "tag-orange",
351
+ "tag-purple",
352
+ "tag-unknown"
353
+ ]
345
354
  }
@@ -99,6 +99,27 @@ function buildScale() {
99
99
  }
100
100
  const onScale = (v, scale) => scale.some((s) => Math.abs(s - v) <= 1)
101
101
 
102
+ // ── variant/state fidelity: valid values per component, from the registry ─────
103
+ const TAGMAP = { 'obs-button': 'button', 'obs-input': 'input', 'obs-tag': 'tag', 'obs-radio': 'radio', 'obs-select': 'select', 'obs-severity': 'severity', 'obs-checkbox': 'checkbox', 'obs-switch': 'switch', 'obs-link': 'link', 'obs-tags': 'loose-tags', 'obs-date-time-picker': 'date-time-pickers', 'obs-filters': 'filters' }
104
+ const _regCache = {}
105
+ function validValues(tag) {
106
+ const id = TAGMAP[tag]; if (!id) return null
107
+ if (_regCache[id] !== undefined) return _regCache[id]
108
+ const p = path.join(DS, 'components', 'registry', `${id}.json`)
109
+ if (!fs.existsSync(p)) return (_regCache[id] = null)
110
+ const r = JSON.parse(fs.readFileSync(p, 'utf8'))
111
+ const props = r.props || {}
112
+ const enumOf = (k) => (props[k] && Array.isArray(props[k].enum) && props[k].enum.length ? props[k].enum : null)
113
+ // `variantEnum` / `severityEnum` are purpose-built fields listing the CANONICAL DS variant values that
114
+ // match the obs-* component + the CSS-class form (e.g. tag-green) — the values a rendered page actually
115
+ // uses. (The product-API `props.variant.enum` documents MTag's raw variants, which differ.) size/type
116
+ // use the reliable prop enum.
117
+ return (_regCache[id] = { id,
118
+ variant: (Array.isArray(r.variantEnum) && r.variantEnum.length) ? r.variantEnum : null,
119
+ severity: (Array.isArray(r.severityEnum) && r.severityEnum.length) ? r.severityEnum : null,
120
+ size: enumOf('size'), type: enumOf('type') })
121
+ }
122
+
102
123
  // ── static server (reused from diff.mjs) ─────────────────────────────────────
103
124
  const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff': 'font/woff', '.woff2': 'font/woff2' }
104
125
  function serve(root) {
@@ -144,7 +165,17 @@ const MEASURE = () => {
144
165
  const txt = (el.textContent || '').trim()
145
166
  return hasBg && rad >= 4 && r.width > 0 && r.width < 160 && r.height < 40 && txt.length >= 1 && txt.length <= 24 && el.children.length <= 1
146
167
  }).map((el) => (el.textContent || '').trim().slice(0, 24))
147
- return { colors, spaces, dsInteractive, rawControls, chips: [...new Set(chips)], bodyBg: getComputedStyle(document.body).backgroundColor }
168
+ // capture each DS component instance + its variant/size/type/state attrs (validated vs the registry node-side)
169
+ const dsInstances = [...document.querySelectorAll(DS.join(','))].map((el) => ({
170
+ tag: el.tagName.toLowerCase(),
171
+ variant: el.getAttribute('variant'),
172
+ size: el.getAttribute('size'),
173
+ type: el.getAttribute('type'),
174
+ severity: el.getAttribute('severity'),
175
+ ariaLabel: el.getAttribute('aria-label') || el.getAttribute('aria-labelledby'),
176
+ hasText: (el.textContent || '').trim().length > 0,
177
+ }))
178
+ return { colors, spaces, dsInteractive, rawControls, chips: [...new Set(chips)], dsInstances, bodyBg: getComputedStyle(document.body).backgroundColor }
148
179
  }
149
180
 
150
181
  ;(async () => {
@@ -222,21 +253,38 @@ const MEASURE = () => {
222
253
  // and no fabricated Tag/Badge look-alikes. A DS page renders controls via obs-* (shadow DOM), so any
223
254
  // raw light-DOM control is a non-DS element. This is the "use DS components, not look-alikes" check.
224
255
  const rawN = m.rawControls.length
225
- const denom = m.dsInteractive + rawN
226
- const componentScore = denom === 0 ? 100 : Math.round((m.dsInteractive / denom) * 100)
227
256
  for (const rc of m.rawControls) violations.component.push({ detail: `raw <${rc.tag}${rc.role ? ' role=' + rc.role : ''}>${rc.text ? ` "${rc.text}"` : ''} — use ${rc.suggest}, not a raw element` })
228
257
  if (m.chips.length) violations.component.push({ advisory: true, detail: `possible fabricated chip(s) — use obs-tag / obs-severity: ${m.chips.slice(0, 6).join(', ')}` })
229
258
 
259
+ // variant/state fidelity — every variant/size/type used must be a REAL registry value (not invented),
260
+ // and required state must be present (icon-only control needs an aria-label).
261
+ let checked = 0, invalid = 0
262
+ for (const inst of (m.dsInstances || [])) {
263
+ const vv = validValues(inst.tag); if (!vv) continue
264
+ const bad = (attr, val, allowed) => { if (val == null || !allowed || !allowed.length) return; checked++; if (!allowed.includes(val)) { invalid++; violations.component.push({ detail: `invented ${attr} "${val}" on <${inst.tag}> — valid: ${allowed.slice(0, 8).join(', ')}` }) } }
265
+ // variant/severity validated against the canonical registry enum (variantEnum/severityEnum); size/type
266
+ // against the prop enum. All match the rendered obs-* values, so no reflected-default false positives.
267
+ bad('variant', inst.variant, vv.variant)
268
+ bad('severity', inst.severity, vv.severity)
269
+ bad('size', inst.size, vv.size)
270
+ bad('type', inst.type, vv.type)
271
+ // icon-only interactive control (no text) should carry an aria-label
272
+ if ((inst.tag === 'obs-button' || inst.tag === 'obs-link') && !inst.hasText && !inst.ariaLabel) { checked++; invalid++; violations.component.push({ detail: `icon-only <${inst.tag}> has no aria-label (required state)` }) }
273
+ }
274
+ const presence = (m.dsInteractive + rawN) === 0 ? 1 : m.dsInteractive / (m.dsInteractive + rawN)
275
+ const validity = checked === 0 ? 1 : (checked - invalid) / checked
276
+ const componentScore = Math.round(presence * validity * 100)
277
+
230
278
  // Component fidelity is weighted heavily — the point of the check is DS components, not just DS colours.
231
279
  const overall = Math.round(tokenScore * 0.35 + componentScore * 0.30 + philosophyScore * 0.20 + layoutScore * 0.15)
232
280
  const result = { target, theme, overall, dimensions: { token: tokenScore, component: componentScore, philosophy: philosophyScore, layout: layoutScore },
233
- measured: { colors: tTot, spacings: lTot, dsComponents: m.dsInteractive, rawControls: rawN, fabricatedChips: m.chips.length }, violations }
281
+ measured: { colors: tTot, spacings: lTot, dsComponents: m.dsInteractive, rawControls: rawN, fabricatedChips: m.chips.length, variantChecks: checked, invalidVariants: invalid }, violations }
234
282
 
235
283
  if (jsonOut) fs.writeFileSync(jsonOut, JSON.stringify(result, null, 2))
236
284
  if (!QUIET) {
237
285
  console.log(`\n=== DS conformance — ${target} (theme=${theme}) ===`)
238
286
  console.log(` OVERALL: ${overall}/100 · token ${tokenScore} component ${componentScore} philosophy ${philosophyScore} layout ${layoutScore}`)
239
- console.log(` measured: ${tTot} colours · ${lTot} spacings · ${m.dsInteractive} DS components · ${rawN} raw controls · ${m.chips.length} fabricated chip(s)`)
287
+ console.log(` measured: ${tTot} colours · ${lTot} spacings · ${m.dsInteractive} DS components · ${rawN} raw controls · ${m.chips.length} fabricated chip(s) · ${checked} variant checks (${invalid} invalid)`)
240
288
  if (violations.component.length) { console.log('\n COMPONENT fidelity (use DS components, not raw/look-alikes):'); for (const v of violations.component.slice(0, 10)) console.log(` ✗ ${v.detail}`) }
241
289
  if (violations.token.length) { console.log('\n off-token colours (top):'); for (const v of violations.token.slice(0, 8)) console.log(` ✗ ${v.value} (${v.prop}, ×${v.count}) → nearest DS token ${v.nearestToken} (${v.nearestVal}, Δ${v.delta})`) }
242
290
  if (violations.layout.length) { console.log('\n off-scale spacing/radii:'); for (const v of violations.layout.slice(0, 6)) console.log(` ~ ${v.value} (×${v.count})`) }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mtdt/observeops-ds-spec",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Machine-readable spec and AI operating contract for the Motadata ObserveOps design system \u2014 components, tokens, page recipes, layout structure, and the rules an AI tool follows to build ObserveOps UI faithfully.",
5
5
  "license": "UNLICENSED",
6
6
  "main": "index.js",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mtdt/observeops-ds-spec",
3
- "version": "0.1.6",
4
- "generated": "2026-07-05T06:45:18.241Z",
3
+ "version": "0.1.8",
4
+ "generated": "2026-07-05T07:20:59.202Z",
5
5
  "entry": "llms.txt",
6
6
  "contract": "AGENTS.md",
7
7
  "index": "components/index.json",
@@ -50,8 +50,8 @@
50
50
  },
51
51
  {
52
52
  "path": "components/registry/button.json",
53
- "bytes": 13613,
54
- "sha256": "4b98033fc2df599f6e81dfcfb4ce24b5ebe869b336cd7fad99c6cf12c714506b"
53
+ "bytes": 13857,
54
+ "sha256": "b54bb602317cacf0eafb72211b9c63bc27bb7ecef1a4319c24b8174d628c2c1e"
55
55
  },
56
56
  {
57
57
  "path": "components/registry/checkbox.json",
@@ -130,8 +130,8 @@
130
130
  },
131
131
  {
132
132
  "path": "components/registry/link.json",
133
- "bytes": 6575,
134
- "sha256": "b3362ab4305d631e24c83149482d46723f964f4f9c62d516aaeec14bbf2e6ff4"
133
+ "bytes": 6818,
134
+ "sha256": "0502a87ed580f4899c0e0c4cea1b29303021ecdf3458049bc2e33da40a821d10"
135
135
  },
136
136
  {
137
137
  "path": "components/registry/loose-tags.json",
@@ -180,8 +180,8 @@
180
180
  },
181
181
  {
182
182
  "path": "components/registry/severity.json",
183
- "bytes": 7118,
184
- "sha256": "9aec8f6752f98d825bcd78e001284a98b737682b0da9125de6dd38c1fee1ff7b"
183
+ "bytes": 7318,
184
+ "sha256": "ecf038986b75313361bf7f38c57aedbc2e9c01280af37c1ba93f6c5f755ca82b"
185
185
  },
186
186
  {
187
187
  "path": "components/registry/switch.json",
@@ -200,8 +200,8 @@
200
200
  },
201
201
  {
202
202
  "path": "components/registry/tag.json",
203
- "bytes": 11493,
204
- "sha256": "245aef496865fb8ba8ddd065bba02c1f1b4863faa7b9b837bfd2bb09dae8738f"
203
+ "bytes": 11640,
204
+ "sha256": "8f50f7486f130d07f47484aaf5d1ef1aa772d5ea8fe13b2a90d45a6a4c35d6f2"
205
205
  },
206
206
  {
207
207
  "path": "components/registry/tags-list.json",
@@ -360,8 +360,8 @@
360
360
  },
361
361
  {
362
362
  "path": "conformance/ds-conformance.mjs",
363
- "bytes": 17517,
364
- "sha256": "2ab84ecc68575abf122b85beb3f9a9d4eccde9ffd629f71ea1cae2af50cfd3ac"
363
+ "bytes": 21068,
364
+ "sha256": "e710fd4aed8629bfdd95b5c3ced0135099d4b57fd36aafa827f05fc00e90082e"
365
365
  },
366
366
  {
367
367
  "path": "foundation/README.md",
@@ -411,7 +411,7 @@
411
411
  {
412
412
  "path": "package.json",
413
413
  "bytes": 1072,
414
- "sha256": "3e5a5e13ada2e15ca887ec4d40f280a0fac033f4dbaba8ac601d6efd46011d2f"
414
+ "sha256": "1d9d5850f9aa6ee23a7f8b071b596d37ebb013fc07262629a0bfd6bcedca03a3"
415
415
  },
416
416
  {
417
417
  "path": "tokens/README.md",