| name | ui-first-principles |
| description | First-principles UI/IA reasoning: turns a `<scenario>` + API field set into JTBD analysis, principle-anchored field-priority decisions, anti-pattern findings, and a bidirectional UI↔API gap report. Trigger: UI、UX、資訊架構、IA、scenario-driven UI、欄位優先級、information hierarchy. Not for: visual/CSS work (use `/frontend-design`), post-build critique (use `/critique`), or simplifying existing layouts (use `/distill`). |
| allowed-tools | Read, Grep, Glob, Write, Bash(bash:*), Bash(node:*), Bash(mktemp:*), Bash(rm:*) |
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-design reads §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: --api is required in v1 (--manual is 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.js cannot 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 --api exists and is readable; on failure use Reason: input_unreadable with the offending path in Detail:.
-
TMPDIR=$(mktemp -d /tmp/ui-fp.XXXXXX). Pass to all later phases. Install the cleanup trap before any later phase runs:
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_FORMAT must be json_sample in v1 (the only supported value, since --api is the only supported input mode — see Arguments table). Omitting --inputFormat lets redact.js default to json_sample, which is correct for --api; on JSON.parse failure the redactor still falls back to fallbackStringMode (KV-pair masking) rather than producing an empty result, but the orchestrator should always pass --inputFormat explicitly so Phase 2 can cross-check the format with --inputFormat (Phase 2 normalization branches on it). Once --manual ships 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, detail } JSON line on stdout).
PII Class Reference
Phase 1 and Phase 7 use different detection sets — keep them straight or you will misread Phase 7 violations. The validator file (scripts/skills/ui-first-principles/validate-report.js) is the source of truth for Phase 7; this table is a quick reference.
Phase 1 (redact.js) — masking authority. Combines regex content scan + field-name heuristics. Classes emitted as <redacted:{class}> placeholders + added to forbiddenFingerprints are exactly: email, phone, address, account_id, national_id, credential. Independent of these classes, scripts/security-redact.js runs as a base layer: high-confidence matches (PEM private keys, AWS AKIA…, OpenAI sk-…, GitHub ghp_… / github_pat_…, Slack xox*, Google AIza…) abort with high_confidence_secret; medium-confidence matches (password=, token:/api_key=/secret= assignments, JWT-like eyJ…, ≥32-char hex non-SHA1) are masked as [REDACTED] and counted as baseRedactHits. Base matches are not PII classes — they do not appear in <redacted:{class}> form.
Phase 7 (validate-report.js) — leak rescan. Two complementary checks:
| Check | Purpose | Source of truth |
|---|
pii_leak_fingerprint (Rule 1) | Catches tokenized re-leak of values whose SHA-256 prefix Phase 1 emitted into forbiddenFingerprints. The validator splits the report on whitespace + markdown/JSON delimiters and hashes each token, the punctuation-stripped variant, and the assignment RHS — so pwd:supersecret, supersecret., =supersecret all hit the same fingerprint as supersecret. It is not substring/window scanning: multi-token values (e.g. 123 Main St postal addresses) only match if the original full token appears intact. | forbiddenFingerprints Set carried via bundle.json |
pii_leak_regex (Rule 1b) | Catches LLM hallucination of plausible-looking PII that Phase 1 never saw. Smaller class set than Phase 1. | The 6 regex constants in validate-report.js (5 distinct labels — SSN and Taiwan ID share national_id) |
The actual Rule 1b regex set:
| Phase 7 regex label | Pattern source (validate-report.js) | --domain crypto behaviour |
|---|
email | EMAIL_PATTERN = /[\w.+-]+@[\w-]+\.[\w.-]+/g | always flagged |
national_id (US SSN) | SSN_PATTERN = /\b\d{3}-\d{2}-\d{4}\b/g | always flagged |
national_id (Taiwan ID) | TAIWAN_ID_PATTERN = /\b[A-Z][12]\d{8}\b/g | always flagged |
phone (E.164) | E164_PATTERN = /(?<![+\dA-Za-z_])\+\d{8,15}(?![\dA-Za-z_])/g | always flagged |
eth_address | ETH_ADDR_PATTERN = /\b0x[0-9a-fA-F]{40}\b/g | flagged only when --domain is NOT crypto |
eth_hash | ETH_HASH_PATTERN = /\b0x[0-9a-fA-F]{64}\b/g | flagged only when --domain is NOT crypto |
Classes Rule 1b does NOT regex-detect (rely on Rule 1 fingerprint instead): domestic non-E.164 phones, postal addresses, generic account IDs. These are caught only when Phase 1 actually masks or fingerprints them through value patterns, field-name heuristics (address_*, account_id, etc.), or the security-redact.js base layer. Coverage v1 does NOT regex-detect, AND redact.js has no value pattern for: IBAN, SWIFT, BIC, mnemonic / seed / recovery phrases, generic sk_… style API keys (note: only sk-… with hyphen is high-confidence). Such values must be removed from the input before invocation, or redact.js must be extended before claiming coverage.
Crypto domain semantics (current v1 implementation): --domain crypto simply suppresses Rule 1b's eth_address / eth_hash checks. It does not verify that a 0x... token in LLM prose originated from the input — fabricated hex strings under crypto domain are not regex-flagged but will still trip Rule 1 if their fingerprint matches an input value, or pass silently if they do not. Origin-aware crypto enforcement is on the v2 backlog.
Phase 2 — Normalize (Bash → JSON file)
node scripts/skills/ui-first-principles/normalize-input.js \
--phase1 "$TMPDIR/phase1.json" \
--scenario "$SCENARIO" \
--inputFormat "$INPUT_FORMAT" \
--output "$TMPDIR/bundle.json"
Output: ScenarioBundle JSON file with this schema. The LLM (Phases 3–6) consumes scenario, fields, inputFormat, and redactionSummary. The validator (Phase 7) consumes fields[].name (Rule 2 missing_decision), forbiddenFingerprints (Rule 1), and the three allowlists (Rules 3 / 3b / 5):
{
"scenario": "<free text>",
"fields": [{ "name": "...", "type": "...", "sampleValue": "...", "description": "...", "source": "json_sample" | "manual" }],
"inputFormat": "json_sample" | "manual_list",
"redactionSummary": { "totalMasks": N, "maskedClasses": [...], "cryptoAllowlistHits": M },
"forbiddenFingerprints": ["sha256:...", ...],
"allowedPrinciples": ["JTBD", "CognitiveLoadTheory", "HicksLaw", "MillersLaw", "ProgressiveDisclosure"],
"allowedPriorities": ["primary", "secondary", "on_demand", "hidden"],
"allowedAntiPatterns": ["too_many_primary", "scenario_field_mismatch", "pure_aesthetic_over_utility", "hidden_critical_info", "redundant_fields"]
}
Phases 3–6 read only this file plus the four reference docs — they never see raw input. Phase 7 reads the same file and uses fields[].name for Rule 2 (missing_decision), forbiddenFingerprints for Rule 1, and the three allowlists for Rules 3 / 3b / 5. CLI exit codes: 0 on success; 2 on cli_args / unreadable_phase1 / normalize_failed / write_failed.
Phase 3 — JTBD Analysis (LLM)
- Read
references/jtbd-framework.md (functional / emotional / social elicitation rules; Web3 specialization).
- Read
$TMPDIR/bundle.json.
- Produce
## 1. JTBD Analysis (3 subsections; empty dimensions explicitly say none in this scenario).
Phase 4 — Principles Briefing (LLM)
- Read
references/principle-anchors.md (5 principles + reasoning-chain order + mask semantics).
- Internalize the closed enum
JTBD | CognitiveLoadTheory | HicksLaw | MillersLaw | ProgressiveDisclosure — every Phase 5 row cites exactly one.
Phase 5 — Field Decision Table (LLM)
For each field in bundle.fields, decide:
| Column | Source |
|---|
Field | Exact name from bundle.fields[].name (no rename) |
Priority | One of primary | secondary | on_demand | hidden |
Principle Anchor | One ID from the principles whitelist |
Rationale | 1–2 sentences traced back to a Phase 3 job or quantitative threshold; never echoes the raw redacted value |
The validator's Rule 2 (missing_decision, critical) checks every field appears as a row.
Phase 5b — Anti-Pattern Findings (LLM)
-
Read references/anti-patterns.md (5 IDs + triggers + severity rubric).
-
Emit ## 3. Anti-Pattern Findings as a markdown table (preferred — matches references/output-template.md schema).
Acceptable fallback: a bullet list whose every line matches the validator's strict grammar (parseAntiPatterns regex /^\s*[-*]\s+`([a-z][a-z0-9_]+)`/gm):
- `too_many_primary`: rationale text…
- `redundant_fields`: rationale text…
Forms the validator REJECTS as anti_pattern_unstructured: bullets without a leading backticked ID (e.g. - Pattern: too_many_primary — rationale), nested or indented sub-bullets without an ID at the top, and prose paragraphs that mention IDs inline.
-
If no anti-patterns apply → emit a single-row table: (none detected) | — | info | All fields pass anti-pattern checks. (parens + space — see references/anti-patterns.md § Detection Discipline).
Phase 6 — Gap Report (LLM)
Bidirectional gap analysis (validator Rule 4 requires both directions):
## 4. Gap Report
**UI needs but API missing**: <field1, field2 — or `none`>
**API provides but UI ignores**: <field3, field4 — or `none`>
Phase 7 — Validate (Bash → JSON, then act)
node scripts/skills/ui-first-principles/validate-report.js \
--report "$DRAFT_PATH" \
--bundle "$TMPDIR/bundle.json" \
--domain "${DOMAIN:-}" \
> "$TMPDIR/validation.json"
Decision table:
| Result | Action |
|---|
ok=true, no soft violations | Write $OUTPUT, emit ✅ Ready, end |
ok=true, soft only | Prepend > ⚠️ Warnings: <list> block, write $OUTPUT, emit ⚠️ Soft warnings, end |
ok=false, critical, retry not yet attempted | Re-enter Phases 3–6 with violation context appended; retry counter = 1 |
ok=false, critical, retry already attempted | Discard draft, emit ⚠️ Need Human with violation summary; do not write report |
Rule 1 / 1b leak details are surfaced as <redacted len=N> previews — never the raw value (re-leaking would defeat the discipline).
Emit
The report file ($OUTPUT) must follow references/output-template.md exactly — first line is # UI First-Principles Analysis: <scenario>, followed by the metadata blockquote and §1–§5. The block below is the operator-facing wrapper the skill prints to the conversation (path + run metadata + sentinel) — it is not what gets written to disk.
## UI First-Principles Analysis (run summary)
> Path: <output>
> Scenario: <scenario> Domain: <crypto|none> Input: <json_sample|manual_list>
<final markdown report rendered from $OUTPUT — header must be `# UI First-Principles Analysis: <scenario>` per references/output-template.md>
<gate sentinel>
Performance Budget
Tech-spec §4 → NFR-5 Time Budget Breakdown sets the run target at p95 ≤ 120s (≈ 92s sum + 28s margin). When a phase exceeds its share:
| Trigger | First action | Escalation |
|---|
| Single phase exceeds its share by < 20% | Continue — margin absorbs | Log only |
| Single phase exceeds its share by 20–50% | Compress LLM prompt for that phase (drop optional examples; keep contracts) | Re-run; if still over, log to validation summary |
| Aggregate run exceeds 120s | Skip optional reference re-loads on retry; reuse cached bundle.json | If still over after one retry → emit ⚠️ Need Human: performance budget exceeded with per-phase timings |
Never relax validator strictness to recover budget — soft warnings still emit, critical violations still retry. Compress prompts, not gates.
Reference Loading Order
Reference files are progressive context — loaded only when needed:
| Phase | Reference | Why |
|---|
| 3 | references/jtbd-framework.md | Three-dimension elicitation guide; FR-3 contract |
| 4–5 | references/principle-anchors.md | 5 principle definitions, anchor whitelist, mask semantics |
| 5b | references/anti-patterns.md | 5 anti-pattern IDs + detection triggers |
| Final pre-emit | references/output-template.md | Markdown schema the validator expects |
A skilled reader can cite all four in a single read; on retry, only the file relevant to the violation needs reloading.
Output Schema (authoritative)
See references/output-template.md for the full markdown contract: header metadata → ## 1. JTBD Analysis → ## 2. Field Decision Table → ## 3. Anti-Pattern Findings → ## 4. Gap Report → ## 5. Information Hierarchy (Primary / Secondary / On-Demand / Hidden zones).
Examples
/ui-first-principles "transaction confirmation" --api fixtures/tx-confirm.json --domain crypto
/ui-first-principles "NFT 詳情" --api fixtures/nft.json --domain crypto --output docs/handoffs/nft-fp.md
Output
| Artifact | Default location |
|---|
| Handoff report | <cwd>/handoff-ui-first-principles.md (override with --output) |
| Validation log | $TMPDIR/validation.json (cleaned on exit) |
| Phase 1 / 2 intermediate files | $TMPDIR/*.json (cleaned on exit) |
Verification Checklist
Cross-References
- Tech spec:
docs/features/ui-first-principles/2-tech-spec.md §3 (orchestration), §3.4 (per-phase contracts)
- Requirements:
docs/features/ui-first-principles/1-requirements.md §FR-1 / §FR-3 / §NFR-7 / §NFR-8
- Validator:
scripts/skills/ui-first-principles/validate-report.js (rules 1–5)
- Redactor:
scripts/skills/ui-first-principles/redact.js
- Normalizer:
scripts/skills/ui-first-principles/normalize-input.js