docs(#1867): ui-consideration-probe reference + docs-parity test (ADPT-02)
Reference doc mirrors edge-probe.md structure but links rather than re-argues; states the MIXED-axis boundary (closed compiled shape-rooted subset here; open UX subset - real-time/offline, a11y depth, i18n/RTL - prose-owned in domain-probes.md). Docs-parity test pins doc taxonomy ids == code UI_TAXONOMY ids and asserts disjointness from domain-probes.md topics (ADR-456 runtime-contract exemption, see #1867).
This commit is contained in:
73
gsd-core/references/ui-consideration-probe.md
Normal file
73
gsd-core/references/ui-consideration-probe.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# UI-Consideration Probe — Spec-Completeness Reference
|
||||
|
||||
The **third** adapter of the shared `probe-core` resolution model (ADR-550 Decision 7), on
|
||||
the **UI element/state axis**. It surfaces the shape-rooted UI *state* considerations a
|
||||
UI-SPEC must resolve before a dimension may PASS — the visual analog of the requirement-side
|
||||
[edge-probe](./edge-probe.md), reusing its exact lifecycle, validators, and plan-phase lift
|
||||
(see edge-probe.md for the shared status×verification model — this doc does not re-argue it).
|
||||
|
||||
**Axis boundary (this is a MIXED axis).** This compiled taxonomy covers ONLY the finite,
|
||||
project-independent shape-rooted content/robustness states. The **open**, domain/UX-dependent
|
||||
considerations — real-time/offline/optimistic-UI, deep accessibility (WCAG breadth),
|
||||
internationalization / RTL depth, and emerging interaction paradigms — are open-ended and are
|
||||
prose-owned in the companion [domain-probes.md](./domain-probes.md) technology/UX bank, NOT
|
||||
here. Forcing them into a closed compiled taxonomy is the wrong model.
|
||||
|
||||
## Inputs
|
||||
|
||||
A list of UI elements, each a `{ id, text, elements? }` record where `text` is the
|
||||
researcher-authored description and `elements` is an optional author-supplied override of the
|
||||
element classification. The six element kinds are: `form`, `list-collection`, `nav`, `media`,
|
||||
`interactive-control`, `static-content`. When `elements` is absent, a heuristic classifier
|
||||
proposes kinds from the prose (propose-then-confirm) — the author may correct the kind.
|
||||
|
||||
## Taxonomy (8 categories)
|
||||
|
||||
Closed and small by design: the finite, project-independent content/robustness states every
|
||||
UI surface must account for. Growth toward open UX topics happens in `domain-probes.md`, not by
|
||||
bloating this closed core.
|
||||
|
||||
| id | name | applies to element kinds | consideration question |
|
||||
|----|------|--------------------------|------------------------|
|
||||
| empty | Empty / no data | form, list-collection, media | What is shown when there is no data — zero items, an unfilled form, or absent media? |
|
||||
| loading | Loading / in-flight | form, list-collection, media, nav | What is shown while data or content is still loading (skeleton, spinner, progressive reveal)? |
|
||||
| error | Error / failure | form, list-collection, media, nav | What is shown when the load or submit fails (message, retry affordance, partial fallback)? |
|
||||
| populated | Populated / happy path | list-collection, media | What does the normal populated (happy-path) state look like at a typical volume of content? |
|
||||
| partial | Partial / incomplete | form, list-collection | What is shown for partial or incomplete data — some fields or rows present, others missing? |
|
||||
| overflow | Overflow / truncation | list-collection, nav, static-content | What happens when content exceeds its container — scroll, clip, wrap, or truncate? |
|
||||
| zero-one-many | Zero / one / many | list-collection | How does the layout read at zero, one, and many items (singular vs plural copy, spacing)? |
|
||||
| long-text | Long text | form, static-content, interactive-control, nav | What happens with unusually long text — truncation, wrapping, ellipsis, or reflow? |
|
||||
|
||||
## Relevance filter + resolution states
|
||||
|
||||
The probe reuses the edge-probe rails verbatim (ADR-550 Decision 7 — see
|
||||
[edge-probe.md](./edge-probe.md#relevance-filter--resolution-states) for the full model):
|
||||
|
||||
1. **Relevance filter first.** Classify each element's kind(s), then raise only the categories
|
||||
whose `applies to element kinds` intersect. A static label is never asked about loading or
|
||||
empty state — that is what makes an unresolved consideration meaningful.
|
||||
2. **Dismissal requires a reason string.** Silence is not a resolution; the reason is the audit
|
||||
trail.
|
||||
3. **Zero-classification surfaces one `unclassified` candidate (#1110).** An element whose prose
|
||||
matched no kind cue yields exactly one soft `unclassified — review manually` item
|
||||
(`category: "unclassified"`, `status: "unresolved"`) — never a silent drop, never a guessed
|
||||
kind. `unclassified` is a review signal, **not** a ninth taxonomy category; an explicit
|
||||
`elements: []` opt-out stays silent.
|
||||
|
||||
Each raised consideration carries the shared two orthogonal axes — `status`
|
||||
(`resolved | dismissed | unresolved`) and, when resolved, a `verification` tier
|
||||
(`explicit | backstop`). A `backstop` consideration lifts into `must_haves.truths` and, at
|
||||
verify time, is confirmed only by explicit evidence (a wired held-out/property test) or routes
|
||||
to `insufficient_spec → human_needed` — never a silent pass (the honest-verifier disposition,
|
||||
#1154). See [honest-verifier.md](./honest-verifier.md).
|
||||
|
||||
## Closed / open boundary
|
||||
|
||||
The **8 ids above are the closed, compiled subset** — finite and project-independent, so a
|
||||
compiled taxonomy is legitimate (the same property that makes edge-probe's data-shape taxonomy
|
||||
closed). The **open subset is prose-owned in [domain-probes.md](./domain-probes.md)**:
|
||||
real-time/offline/optimistic-UI, deep accessibility (WCAG breadth), i18n / RTL depth, and
|
||||
emerging interaction paradigms (gesture/voice/reduced-motion/print) are open-ended and
|
||||
cue-triggered — they do not belong in this closed taxonomy. This probe **complements** the
|
||||
`gsd-ui-checker` six quality dimensions (it adds a state-coverage axis); it does not change the
|
||||
BLOCK/FLAG/PASS enum or the dimensions themselves.
|
||||
57
tests/ui-consideration-probe-docs-fixtures.test.cjs
Normal file
57
tests/ui-consideration-probe-docs-fixtures.test.cjs
Normal file
@@ -0,0 +1,57 @@
|
||||
// allow-test-rule: runtime-contract-is-the-product (see #1867) — the rendered reference doc's taxonomy table IS the runtime contract; this pins its bijection to the code (docs-parity, ADR-456 exception matrix)
|
||||
// Asserts gsd-core/references/ui-consideration-probe.md keeps its taxonomy id column in
|
||||
// sync with the source-of-truth UI_TAXONOMY (built .cjs), and that the closed compiled
|
||||
// taxonomy stays DISJOINT from the open-prose domain-probes.md bank (the mixed-axis boundary,
|
||||
// ADPT-02). The comparison is on PARSED table ids and PARSED `##` headings, never a raw
|
||||
// full-text substring match — a reformat that preserves the data does not fail; semantic drift does.
|
||||
'use strict';
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const uc = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'ui-consideration-probe.cjs'));
|
||||
const docPath = path.join(__dirname, '..', 'gsd-core', 'references', 'ui-consideration-probe.md');
|
||||
const domainPath = path.join(__dirname, '..', 'gsd-core', 'references', 'domain-probes.md');
|
||||
|
||||
// Extract the first-column ids from the `## Taxonomy` markdown table (skips the `id` header
|
||||
// row and the `|----|` separator; an id is a lowercase-hyphen token).
|
||||
function docTaxonomyIds(md) {
|
||||
const section = md.split(/^## Taxonomy/m)[1].split(/^## /m)[0];
|
||||
return section.split('\n')
|
||||
.filter((l) => l.trim().startsWith('|'))
|
||||
.map((l) => l.split('|')[1].trim())
|
||||
.filter((c) => /^[a-z][a-z0-9-]*$/.test(c) && c !== 'id');
|
||||
}
|
||||
|
||||
// The `##` topic headings of the open-prose bank (lower-cased).
|
||||
function domainTopics(md) {
|
||||
return md.split('\n')
|
||||
.filter((l) => /^## /.test(l))
|
||||
.map((l) => l.replace(/^## /, '').trim().toLowerCase());
|
||||
}
|
||||
|
||||
describe('ui-consideration-probe doc/code parity (ADPT-02)', () => {
|
||||
test('reference doc exists', () => {
|
||||
assert.ok(fs.existsSync(docPath), `${docPath} must exist`);
|
||||
});
|
||||
|
||||
test('doc taxonomy ids deep-equal the code UI_TAXONOMY ids, in order (doc == code)', () => {
|
||||
const md = fs.readFileSync(docPath, 'utf8');
|
||||
assert.deepEqual(docTaxonomyIds(md), uc.UI_TAXONOMY.map((c) => c.id));
|
||||
});
|
||||
|
||||
test('no taxonomy id overlaps a domain-probes.md open-prose topic (closed/open disjointness)', () => {
|
||||
const topics = domainTopics(fs.readFileSync(domainPath, 'utf8'));
|
||||
for (const id of uc.UI_TAXONOMY.map((c) => c.id)) {
|
||||
assert.ok(!topics.includes(id), `taxonomy id "${id}" must not overlap a domain-probes.md topic`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the doc names domain-probes.md as the companion open-prose bank (links, does not duplicate)', () => {
|
||||
const md = fs.readFileSync(docPath, 'utf8');
|
||||
assert.match(md, /domain-probes\.md/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user