UI First-Principles
Reasoning chain: <scenario> → JTBD → 5 IA/cognitive principles → field decisions → anti-patterns → gap report → validated handoff doc.
Output:handoff-ui-first-principles.md(default<cwd>/handoff-ui-first-principles.md, override with--output). Downstream/frontend-designreads §5 Information Hierarchy directly.
Non-Negotiable Rules
SKILL.md is the normative source. Files under the references/ directory elaborate but do not override.
| # | Rule | Violation = | |---|------|-------------| | 1 | Phase 1 (redact.js) must run before any LLM phase. Raw input never enters Phases 3–6. | Skill invalid (PII risk) | | 2 | Phase 7 critical violation (pii_leak_fingerprint / pii_leak_regex / missing_decision) → 1 retry with violation context → still critical → emit ⚠️ Need Human (no warn-only fallback). | Retry policy breach | | 3 | Principle Anchor column must hold one ID from JTBD \| CognitiveLoadTheory \| HicksLaw \| MillersLaw \| ProgressiveDisclosure. Multi-principle prose is fine in rationale. | invalid_anchor soft violation | | 4 | Priority column must hold one of primary \| secondary \| on_demand \| hidden. | invalid_priority soft violation | | 5 | Anti-Pattern Pattern column IDs must belong to the v1 whitelist in references/anti-patterns.md. Use literal (none detected) when no anti-patterns apply. | invalid_anti_pattern_id soft violation | | 6 | Output must end with ✅ Ready (clean) or ⚠️ Soft warnings (soft only) or ⚠️ Need Human (post-retry critical). Hook + behavior layer parses these. | Auto-loop cannot parse |
Trigger
- Keywords: UI first principles, IA design, 資訊架構, 欄位優先級, scenario-driven UI, JTBD UI, anti-pattern audit, ui-first-principles
- Slash form:
/ui-first-principles <scenario>
When NOT to Use
| Intent | Use instead | |--------|-------------| | Visual layout / colour / Tailwind work | /frontend-design | | Post-build evaluation of existing UI | /critique | | Simplifying an already-shipped flow | /distill | | Pure feasibility on a design idea | /feasibility-study | | Tech-spec for an IA decision | /tech-spec (use this skill's output as input) |
Arguments
| Arg | Required | Default | Purpose | |-----|----------|---------|---------| | <scenario> | Yes | — | Free-text scenario name (e.g. transaction confirmation, NFT detail page). Drives JTBD. | | --api <path> | No | — | JSON sample file (single object literal). Phase 2 uses top-level keys as field set. | | --manual <path> | Deferred to v2 | — | Manual field-list file (fieldName: type (description) per line). Not supported in v1. Reason: redact.js masks via the KV-pair fallback parser, which treats field: type as field=type and masks the type literal — address: string becomes address: <redacted:address>. The masked line then fails normalize-input.js's MANUAL_LINE_RE (the type token must start with a letter or quote, not <), so the field is silently dropped from bundle.fields and Phase 7 Rule 2 cannot require a decision for it. Always use --api in v1. Manual-list support requires a redactor change tracked in the v2 backlog. | | --domain crypto | No | none | Phase 1 + 7 desensitization for 0x... addresses/hashes. | | --output <path> | No | <cwd>/handoff-ui-first-principles.md | Override report path. |
v1 invocation contract:--apiis required in v1 (--manualis deferred to v2 — see Arguments table for why). Phase 0 rejects missing input or any combination that supplies--manual. The tech-spec §3.3 LLM-fallback path (running Phases 3–6 with no real input) is also deferred to v2 —redact.jscannot mask what does not exist, so a no-input run would publish an empty bundle and skip Rule 1 fingerprint coverage and Rule 2 field coverage. Rule 1b (regex rescan over the report) would still execute, but it cannot compensate for missing input — it only catches new PII the LLM hallucinates, not values that should have been redacted upstream.
Workflow
Phase 0 preflight → Phase 1 redact → Phase 2 normalize → Phase 3 JTBD → Phase 4 principles → Phase 5 field table → Phase 5b anti-patterns → Phase 6 gap → Phase 7 validate → Emit
↑__________________________ retry-on-critical (×1) ____________________________|
Phase 0 — Preflight (Bash)
- Verify
--api <path>was provided. v1 only accepts--api; reject--manual(deferred to v2 — see Arguments table). Reject and exit non-zero with the canonical usage banner:
`` ⚠️ Need Human: ui-first-principles preflight error Reason: <missing_input | unsupported_input_v1 | input_unreadable> Usage: /ui-first-principles "<scenario>" --api <path> [--domain crypto] [--output <path>] Detail: <one-line context — e.g. "--manual is deferred to v2; only --api is supported"> ``
- Verify the file at
--apiexists and is readable; on failure useReason: input_unreadablewith the offending path inDetail:. TMPDIR=$(mktemp -d /tmp/ui-fp.XXXXXX). Pass to all later phases. Install the cleanup trap before any later phase runs:
``bash set -Eeuo pipefail cleanup() { rm -rf "${TMPDIR:-}" 2>/dev/null || true; } trap cleanup EXIT trap 'cleanup; trap - INT; kill -INT $$' INT trap 'cleanup; trap - TERM; kill -TERM $$' TERM ``
This purges $TMPDIR (masked text + fingerprints) on normal exit, on a set -e failure, and on Ctrl-C / SIGTERM (the INT/TERM traps re-raise the signal so the caller observes the correct exit status). Caveats: a kill -9 (SIGKILL) cannot be trapped — $TMPDIR survives a hard kill, so do not rely on trap for security guarantees beyond a polite shutdown. set -Eeuo pipefail propagates ERR into functions (else trap ... ERR is silently dropped). Because Phase 0 uses Bash builtins (set, trap, function definitions), execute it through bash -c and keep Bash(bash:) in allowed-tools; the Bash(node:) | Bash(mktemp:) | Bash(rm:) prefixes alone do not authorize the preflight wrapper.
Phase 1 — Redact (Bash → JSON file)
node scripts/skills/ui-first-principles/redact.js \
--input "${API_PATH:-$MANUAL_PATH}" \
--inputFormat "$INPUT_FORMAT" \
--domain "${DOMAIN:-}" \
--output "$TMPDIR/phase1.json"
INPUT_FORMATmust bejson_samplein v1 (the only supported value, since--apiis the only supported input mode — see Arguments table). Omitting--inputFormatletsredact.jsdefault tojson_sample, which is correct for--api; onJSON.parsefailure the redactor still falls back tofallbackStringMode(KV-pair masking) rather than producing an empty result, but the orchestrator should always pass--inputFormatexplicitly so Phase 2 can cross-check the format with--inputFormat(Phase 2 normalization branches on it). Once--manualships in v2 the contract becomes "pass exactly what Phase 0 selected."
Output schema (consumed by Phase 2 — exactly what redact.js --output writes):
{
"maskedText": "<input with PII replaced by <redacted:type> placeholders>",
"forbiddenFingerprints": ["sha256:abc...", ...],
"fieldDecisions": [{
"path": "<dot.path>",
"fieldName": "<key>",
"action": "keep" | "mask" | "crypto_allow",
"piiClass": "<present only when action=mask> — email | phone | address | account_id | national_id | credential",
"fingerprint": "<present only when action=mask> — sha256:..."
}],
"redactionSummary": {
"totalMasks": N,
"maskedClasses": ["email", "address", ...],
"cryptoAllowlistHits": M,
"baseRedactHits": K
}
}
The forbiddenFingerprints array is the Phase 7 input that catches LLM-fabricated leaks of original sensitive values. CLI exit codes: 0 on success; 2 on cli_args / unreadable_input / high_confidence_secret / redact_failed / write_failed (each emits a structured `{ ok: false, error, detai