@mtdt/observeops-ds-spec 0.1.5 → 0.1.7
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/conformance/ds-conformance.mjs +82 -15
- package/package.json +1 -1
- package/spec.manifest.json +5 -5
|
@@ -99,6 +99,24 @@ 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
|
+
// Validate ONLY against a declared prop `enum` (the reliable accepted-values source). The registry's
|
|
114
|
+
// `variants` field is a doc concept whose names don't always match the prop values (e.g. link), so we
|
|
115
|
+
// do NOT validate against it — that produced false positives. Where a component has no enum, its variant
|
|
116
|
+
// isn't strictly validated here (a registry gap the ai-readiness/validate-registries pass should close).
|
|
117
|
+
return (_regCache[id] = { id, variant: enumOf('variant'), size: enumOf('size'), type: enumOf('type') })
|
|
118
|
+
}
|
|
119
|
+
|
|
102
120
|
// ── static server (reused from diff.mjs) ─────────────────────────────────────
|
|
103
121
|
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
122
|
function serve(root) {
|
|
@@ -124,9 +142,37 @@ const MEASURE = () => {
|
|
|
124
142
|
const v = parseFloat(c[p]); if (v > 0) spaces.push({ prop: p, val: Math.round(v * 10) / 10 })
|
|
125
143
|
}
|
|
126
144
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
145
|
+
// ── component fidelity ─────────────────────────────────────────────────────
|
|
146
|
+
// A DS component (obs-*) renders its real control in SHADOW DOM, so any RAW interactive element in the
|
|
147
|
+
// light DOM is a non-DS control that should be a DS component. And a styled <span>/<div> chip that isn't
|
|
148
|
+
// a DS component is a fabricated look-alike (should be obs-tag / obs-severity).
|
|
149
|
+
const DS = ['obs-button','obs-input','obs-select','obs-switch','obs-checkbox','obs-radio','obs-link','obs-tag','obs-severity','obs-tags','obs-tooltip','obs-date-time-picker','obs-filters','obs-selected-pills']
|
|
150
|
+
const SUGGEST = { button:'obs-button', input:'obs-input', textarea:'obs-input', select:'obs-select', a:'obs-link' }
|
|
151
|
+
const insideCE = (el) => { let n = el.parentElement; while (n) { if (n.tagName.includes('-')) return true; n = n.parentElement } return false }
|
|
152
|
+
const dsInteractive = document.querySelectorAll('obs-button,obs-input,obs-select,obs-switch,obs-checkbox,obs-radio,obs-link').length
|
|
153
|
+
const rawControls = [...document.querySelectorAll('button,input:not([type=hidden]),select,textarea,a[href],[role=button],[role=switch],[role=checkbox],[role=radio],[role=tab]')]
|
|
154
|
+
.filter((el) => !el.tagName.includes('-') && !insideCE(el))
|
|
155
|
+
.map((el) => { const t = el.tagName.toLowerCase(); const role = el.getAttribute('role'); return { tag: t, role, suggest: SUGGEST[t] || (role ? 'obs-' + role.replace('checkbox','checkbox').replace('button','button') : 'a DS component'), text: (el.textContent || el.getAttribute('placeholder') || '').trim().slice(0, 30) } })
|
|
156
|
+
// fabricated chips: light-DOM, non-custom element with a bg tint + radius + short text (a Tag/Badge look-alike)
|
|
157
|
+
const chips = [...document.querySelectorAll('span,div,i')].filter((el) => {
|
|
158
|
+
if (el.tagName.includes('-') || insideCE(el)) return false
|
|
159
|
+
const c = getComputedStyle(el); const r = el.getBoundingClientRect()
|
|
160
|
+
const bg = c.backgroundColor; const hasBg = bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent'
|
|
161
|
+
const rad = parseFloat(c.borderTopLeftRadius) || 0
|
|
162
|
+
const txt = (el.textContent || '').trim()
|
|
163
|
+
return hasBg && rad >= 4 && r.width > 0 && r.width < 160 && r.height < 40 && txt.length >= 1 && txt.length <= 24 && el.children.length <= 1
|
|
164
|
+
}).map((el) => (el.textContent || '').trim().slice(0, 24))
|
|
165
|
+
// capture each DS component instance + its variant/size/type/state attrs (validated vs the registry node-side)
|
|
166
|
+
const dsInstances = [...document.querySelectorAll(DS.join(','))].map((el) => ({
|
|
167
|
+
tag: el.tagName.toLowerCase(),
|
|
168
|
+
variant: el.getAttribute('variant'),
|
|
169
|
+
size: el.getAttribute('size'),
|
|
170
|
+
type: el.getAttribute('type'),
|
|
171
|
+
severity: el.getAttribute('severity'),
|
|
172
|
+
ariaLabel: el.getAttribute('aria-label') || el.getAttribute('aria-labelledby'),
|
|
173
|
+
hasText: (el.textContent || '').trim().length > 0,
|
|
174
|
+
}))
|
|
175
|
+
return { colors, spaces, dsInteractive, rawControls, chips: [...new Set(chips)], dsInstances, bodyBg: getComputedStyle(document.body).backgroundColor }
|
|
130
176
|
}
|
|
131
177
|
|
|
132
178
|
;(async () => {
|
|
@@ -200,26 +246,47 @@ const MEASURE = () => {
|
|
|
200
246
|
if (brandMisuse.length) { philosophyScore -= 20; violations.philosophy.push({ rule: 'brand-navy', detail: `off-token saturated blue used (${brandMisuse.map((b) => b.value).join(', ')}) — brand must be --primary navy, not blue/cyan` }) }
|
|
201
247
|
philosophyScore = Math.max(0, philosophyScore)
|
|
202
248
|
|
|
203
|
-
// 4.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
if (m.
|
|
249
|
+
// 4. COMPONENT FIDELITY — every interactive control must be a real DS component (not a raw element),
|
|
250
|
+
// and no fabricated Tag/Badge look-alikes. A DS page renders controls via obs-* (shadow DOM), so any
|
|
251
|
+
// raw light-DOM control is a non-DS element. This is the "use DS components, not look-alikes" check.
|
|
252
|
+
const rawN = m.rawControls.length
|
|
253
|
+
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` })
|
|
254
|
+
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(', ')}` })
|
|
255
|
+
|
|
256
|
+
// variant/state fidelity — every variant/size/type used must be a REAL registry value (not invented),
|
|
257
|
+
// and required state must be present (icon-only control needs an aria-label).
|
|
258
|
+
let checked = 0, invalid = 0
|
|
259
|
+
for (const inst of (m.dsInstances || [])) {
|
|
260
|
+
const vv = validValues(inst.tag); if (!vv) continue
|
|
261
|
+
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(', ')}` }) } }
|
|
262
|
+
// Validate `size`/`type` against the prop enum (reliable). NOT `variant`: components reflect a default
|
|
263
|
+
// `variant` attr that often isn't in the registry's variant list (e.g. obs-link reflects "primary"),
|
|
264
|
+
// and the registry's variant names don't always match the component's accepted values — so validating
|
|
265
|
+
// it here produces false positives. Enabling variant validation needs a clean per-component variant
|
|
266
|
+
// enum in the registry (tracked as a spec-hardening TODO; see validate-registries / ai-readiness).
|
|
267
|
+
bad('size', inst.size, vv.size)
|
|
268
|
+
bad('type', inst.type, vv.type)
|
|
269
|
+
// icon-only interactive control (no text) should carry an aria-label
|
|
270
|
+
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)` }) }
|
|
271
|
+
}
|
|
272
|
+
const presence = (m.dsInteractive + rawN) === 0 ? 1 : m.dsInteractive / (m.dsInteractive + rawN)
|
|
273
|
+
const validity = checked === 0 ? 1 : (checked - invalid) / checked
|
|
274
|
+
const componentScore = Math.round(presence * validity * 100)
|
|
209
275
|
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
276
|
+
// Component fidelity is weighted heavily — the point of the check is DS components, not just DS colours.
|
|
277
|
+
const overall = Math.round(tokenScore * 0.35 + componentScore * 0.30 + philosophyScore * 0.20 + layoutScore * 0.15)
|
|
278
|
+
const result = { target, theme, overall, dimensions: { token: tokenScore, component: componentScore, philosophy: philosophyScore, layout: layoutScore },
|
|
279
|
+
measured: { colors: tTot, spacings: lTot, dsComponents: m.dsInteractive, rawControls: rawN, fabricatedChips: m.chips.length, variantChecks: checked, invalidVariants: invalid }, violations }
|
|
213
280
|
|
|
214
281
|
if (jsonOut) fs.writeFileSync(jsonOut, JSON.stringify(result, null, 2))
|
|
215
282
|
if (!QUIET) {
|
|
216
283
|
console.log(`\n=== DS conformance — ${target} (theme=${theme}) ===`)
|
|
217
|
-
console.log(` OVERALL: ${overall}/100 · token ${tokenScore}
|
|
218
|
-
console.log(` measured: ${tTot} colours · ${lTot} spacings · ${m.controls.length}
|
|
284
|
+
console.log(` OVERALL: ${overall}/100 · token ${tokenScore} component ${componentScore} philosophy ${philosophyScore} layout ${layoutScore}`)
|
|
285
|
+
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)`)
|
|
286
|
+
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}`) }
|
|
219
287
|
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})`) }
|
|
220
288
|
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})`) }
|
|
221
289
|
if (violations.philosophy.length) { console.log('\n philosophy:'); for (const v of violations.philosophy) console.log(` ✗ [${v.rule}] ${v.detail}`) }
|
|
222
|
-
if (violations.component.length) { console.log('\n component:'); for (const v of violations.component) console.log(` ~ ${v.detail}`) }
|
|
223
290
|
console.log('')
|
|
224
291
|
}
|
|
225
292
|
process.exit(overall >= 80 ? 0 : 1)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mtdt/observeops-ds-spec",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
package/spec.manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mtdt/observeops-ds-spec",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"generated": "2026-07-
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"generated": "2026-07-05T06:59:15.142Z",
|
|
5
5
|
"entry": "llms.txt",
|
|
6
6
|
"contract": "AGENTS.md",
|
|
7
7
|
"index": "components/index.json",
|
|
@@ -360,8 +360,8 @@
|
|
|
360
360
|
},
|
|
361
361
|
{
|
|
362
362
|
"path": "conformance/ds-conformance.mjs",
|
|
363
|
-
"bytes":
|
|
364
|
-
"sha256": "
|
|
363
|
+
"bytes": 21201,
|
|
364
|
+
"sha256": "625ab74a9f0b788a6721432cf8b8688a128c03e832fe001300f8a24ad176f9b2"
|
|
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": "
|
|
414
|
+
"sha256": "19f2e139d6c911d1801219acf6ecbd6f029ed596d8484e8bbf4adc4ae61c78b9"
|
|
415
415
|
},
|
|
416
416
|
{
|
|
417
417
|
"path": "tokens/README.md",
|