Credentials Bundle — Widget Vocabulary Extensions¶
Bundle id: credentials
Vocab spec base: STORYTANGL_WIDGET_VOCAB.md v1.5
Status: draft v0.2 · aligned to v1.5 core vocabulary
Genre: narrative inspection / verification, Papers Please-inspired
Audience: authors writing credentials-style bundles; port implementers covering the credentials profile suite
This document is a Tier P3 genre extension (per main spec §8). It introduces no new top-level vocabulary; it codifies conventions on top of v1.5 for inspection / verification gameplay.
The credentials-specific enrichments are mostly naming and emphasis conventions. A generic client that ignores packet styling, finding severity, and disposition color still conforms when it renders the underlying zones, pieces, key/value findings, blockers, and choices.
0 · Genre summary¶
Credentials bundles model verification under uncertainty: a player inspects packets of documents against a published rule-set, applies mediation moves to gather information, and disposes of each candidate (allow / deny / arrest / escalate) under time and accuracy pressure.
Three orthogonal patterns the credentials genre exercises:
Pattern |
Main spec mechanism |
Genre layer adds |
|---|---|---|
Document packet inspection |
|
|
Severity-coded findings |
|
conventional |
Mediation / disposition tree |
|
conventional |
On authorship vs. rendering. The credentials engine architecture
(CREDENTIALS_LOOP_DESIGN.md) describes how the bundle authors
candidates, restriction maps, indications, and outcome hierarchies.
This document describes how the client renders the engine’s emitted
fragments. Keep the two separated: the engine-side Indication,
RestrictionLevel, Outcome, and Move enums are bundle authoring
vocabulary; this doc is rendering contract.
1 · Domain vocabulary mapped to v1.5¶
Credentials concept |
v1.5 surface |
|---|---|
Candidate |
|
Credential packet |
|
Document (permit, id_card, ticket, asylum_form, etc.) |
|
Restriction map / current rules |
|
Inspection move |
|
Finding |
|
Mediation move |
|
Bribe offer |
|
Disposition |
|
Shift summary |
|
Score / accuracy meter |
|
2 · Packet zone conventions¶
The credential packet is a zone holding the candidate’s documents. Recommended layout:
{
uid: "z-packet",
fragment_type: "group",
group_type: "zone",
member_ids: ["pc-permit", "pc-id-card", "pc-ticket"],
layout_hints: { orientation: "row", reveal: "all" },
hints: { label_text: "Credentials packet", style_tags: ["packet"] }
}
Document pieces live inside the packet zone:
{
uid: "pc-permit",
fragment_type: "piece",
piece_id: "permit-9472",
kind: "permit",
zone_ref: "z-packet",
properties: {
seal: "Imperial",
holder: "Anya Volkov",
issue_date: "2026-04-12",
expiry: "2027-04-12",
purpose: "merchant",
photo_url: "..."
},
hints: { label_text: "Permit (Imperial)" }
}
The CLI port renders the packet as:
[Credentials packet]
- Permit (Imperial) holder=Anya Volkov expires 2027-04-12 purpose=merchant
- ID card holder=Anya Volkov issued 2025-11-03 photo=present
- Travel ticket origin=Kalden destination=here issued 2026-05-19
3 · Findings as annotated KvRows¶
A finding is a row in a KvFragment (scene-bound) or a ProjectedSection
(if persistent for the shift). It uses KvRow.emphasis for severity and
extra="allow" for the bundle-specific fields.
// scene-bound finding fragment after the player inspects the permit
{
uid: "f-finding-permit-expiry",
fragment_type: "kv",
content: [
{
key: "permit_seal",
value: "Imperial",
emphasis: "ok",
code: "seal_valid",
target: "pc-permit",
state: "verified"
},
{
key: "permit_purpose",
value: "merchant",
emphasis: "warn",
code: "purpose_mismatch_declared",
target: "pc-permit",
state: "flag"
},
{
key: "permit_expiry",
value: "2027-04-12",
emphasis: "ok",
code: "permit_current",
target: "pc-permit",
state: "verified"
}
],
hints: { style_tags: ["findings", "inline"] }
}
Severity convention:
|
Meaning |
CLI rendering |
|---|---|---|
|
Verified clean |
|
|
Informational; not actionable |
no prefix |
|
Mitigatable infraction; mediation move available |
|
|
Crime; arrest justified |
|
The code field is author-stable for downstream predicate evaluation
and test fixtures. target is the UID of the piece the finding
relates to. state is one of verified, flag, unverified,
disputed.
4 · Mediation moves¶
Mediation moves are choices the player invokes between initial inspection and final disposition. They typically reveal additional findings or unlock new options.
[
{
uid: "f-choice-verify-id",
fragment_type: "choice",
edge_id: "e-verify-id",
text: "Verify the ID card against the registry.",
accepts: { kind: "pick" },
ui_hints: { hotkey: "1", emphasis: "subtle", cost_previews: [{ ledger_key: "time", delta: -1 }] }
},
{
uid: "f-choice-request-permit",
fragment_type: "choice",
edge_id: "e-request-permit",
text: "Request the missing permit.",
accepts: {
kind: "pieces",
min: 1, max: 1,
constraints: { target_kind: ["permit"] }
},
available: false,
unavailable_reason: "No permit shown.",
blockers: [
{ code: "no_permit_in_packet", message: "Candidate has not provided a permit.", refs: ["z-packet"] }
],
ui_hints: { hotkey: "2" }
},
{
uid: "f-choice-search",
fragment_type: "choice",
edge_id: "e-search",
text: "Search the candidate's belongings.",
accepts: { kind: "pick" },
ui_hints: { hotkey: "3", emphasis: "warning", cost_previews: [{ ledger_key: "time", delta: -2 }] }
}
]
Mediation move catalog (conventional, non-normative):
Move kind |
accepts |
Typical effect |
|---|---|---|
|
|
Backend rolls validity against a registry; emits new findings |
|
|
Candidate produces the doc (new piece appears in packet) or refuses (Blocker on disposition) |
|
|
Backend reveals previously-hidden contents or contraband |
|
|
Backend evaluates consistency between two pieces; new finding |
|
|
Bundle-authored response from candidate; may seed Phase C bribery / threats |
Bundles MAY introduce additional mediation kinds; the rendering is
the same shape (a ChoiceFragment with appropriate accepts.kind).
ui_hints.validity_check optional preview¶
Most credentials checks should remain opaque until the backend returns a finding. When a bundle wants to advertise that a mediation move consumes time or checks a published rule, it MAY add a genre-specific hint:
class ValidityCheckHint(BaseModel):
label: str # "Registry lookup"
target_ref: str | None = None # document or candidate UID
published_rule: str | None = None # player-visible rule label
risk_text: str | None = None # "Costs 1 time"
This follows the same pattern as carwars/training stat_check: the hint
is advisory and legibility-focused. The backend still performs the lookup
and emits authoritative KvRow findings.
5 · Disposition severity¶
Disposition is the terminal commit per candidate. Conventional severity mapping:
[
{ uid: "f-disp-allow", fragment_type: "choice", edge_id: "e-allow",
text: "Allow passage.", accepts: { kind: "pick" },
ui_hints: { hotkey: "a", emphasis: "primary" } },
{ uid: "f-disp-deny", fragment_type: "choice", edge_id: "e-deny",
text: "Deny passage.", accepts: { kind: "pick" },
ui_hints: { hotkey: "d", emphasis: "warning" } },
{ uid: "f-disp-arrest", fragment_type: "choice", edge_id: "e-arrest",
text: "Arrest.", accepts: { kind: "pick" },
available: false,
unavailable_reason: "Insufficient evidence for arrest.",
blockers: [{ code: "no_arrestable_findings",
message: "No finding with emphasis=danger present.",
refs: [] }],
ui_hints: { hotkey: "x", emphasis: "danger" } }
]
The CLI port renders disposition as:
a) Allow passage.
d) Deny passage.
x) Arrest. (locked: Insufficient evidence for arrest.)
6 · Restriction map projection¶
The current shift’s restriction map renders as a ProjectedState
section the player can consult at any time:
{
section_id: "restrictions",
title: "Shift directives",
kind: "restrictions",
value: {
value_type: "kv_list",
items: [
{ key: "Imperial citizens", value: "allowed", emphasis: "ok",
hint: "tag" },
{ key: "Kaldenese refugees", value: "allowed with permit",
emphasis: "warn", hint: "tag" },
{ key: "Eastern merchants", value: "denied — embargo",
emphasis: "danger", hint: "tag" },
{ key: "Diplomatic envoys", value: "allowed — privileged",
emphasis: "subtle", hint: "tag" }
]
},
hints: { style_tags: ["sidebar"] }
}
Per §5.1 Decision Legibility, every restriction that could gate a
disposition’s blocker MUST appear in this projection. The
unavailable_reason on disposition choices references it
("Eastern origin under embargo — see Shift directives").
7 · Shift summary¶
End-of-shift summary is a ProjectedState table:
{
section_id: "shift_summary",
title: "Shift summary",
kind: "shift_summary",
value: {
value_type: "table",
columns: ["Candidate", "Decision", "Correct", "Findings"],
rows: [
["Anya Volkov", "Allowed", "✓", "purpose mismatch flagged"],
["Bek Tarsus", "Denied", "✓", "permit expired"],
["Kavel Ren", "Allowed", "✗", "missed embargo origin"]
]
}
}
8 · Worked example — one candidate, three turns¶
Turn 1 — candidate arrives¶
fragments: [
{ uid: "f-prose-1", fragment_type: "content",
content: "A merchant in worn furs approaches the booth. He sets down a
folded packet and waits, breath misting in the cold." },
// Candidate piece
{ uid: "pc-candidate-bek", fragment_type: "piece",
piece_id: "bek-tarsus", kind: "candidate",
properties: {
name: "Bek Tarsus",
declared_purpose: "merchant",
declared_origin: "Kalden",
photo_url: "..."
},
hints: { label_text: "Bek Tarsus (declared merchant)" } },
// Packet zone
{ uid: "z-packet", fragment_type: "group", group_type: "zone",
member_ids: ["pc-permit", "pc-id-card", "pc-ticket"],
layout_hints: { orientation: "row" },
hints: { label_text: "Credentials packet" } },
// Document pieces (abbreviated)
{ uid: "pc-permit", fragment_type: "piece",
piece_id: "permit-9472", kind: "permit", zone_ref: "z-packet",
properties: { seal: "Imperial", holder: "Bek Tarsus",
expiry: "2026-03-01", purpose: "merchant" },
hints: { label_text: "Permit (Imperial)" } },
{ uid: "pc-id-card", fragment_type: "piece",
piece_id: "id-3382", kind: "id_card", zone_ref: "z-packet",
properties: { holder: "Bek Tarsus", origin: "Kalden" },
hints: { label_text: "ID card" } },
{ uid: "pc-ticket", fragment_type: "piece",
piece_id: "ticket-117", kind: "ticket", zone_ref: "z-packet",
properties: { origin: "Kalden", destination: "Imperial Gate",
issued: "2026-05-19" },
hints: { label_text: "Travel ticket" } },
// Inspect choice
{ uid: "f-choice-inspect", fragment_type: "choice",
edge_id: "e-inspect",
text: "Inspect a document.",
accepts: {
kind: "pieces",
min: 1, max: 1,
constraints: { target_zone_ref: "z-packet" }
},
ui_hints: { hotkey: "1" } },
// Disposition options (initially gated)
{ uid: "f-disp-allow", fragment_type: "choice", edge_id: "e-allow",
text: "Allow passage.", accepts: { kind: "pick" },
available: false,
unavailable_reason: "Inspect documents first.",
ui_hints: { hotkey: "a", emphasis: "primary" } },
{ uid: "f-disp-deny", fragment_type: "choice", edge_id: "e-deny",
text: "Deny passage.", accepts: { kind: "pick" },
ui_hints: { hotkey: "d", emphasis: "warning" } }
]
Turn 2 — player inspects permit¶
After committing e-inspect with payload: {piece_ids: ["pc-permit"]}:
fragments: [
{ uid: "f-prose-2", fragment_type: "content",
content: "You unfold the permit. The Imperial seal is sound, but the
date stamp shows expiry months past." },
// Findings emitted by the backend
{ uid: "f-finding-permit", fragment_type: "kv",
content: [
{ key: "permit_seal", value: "Imperial", emphasis: "ok",
code: "seal_valid", target: "pc-permit", state: "verified" },
{ key: "permit_expiry", value: "2026-03-01", emphasis: "danger",
code: "permit_expired", target: "pc-permit", state: "flag" }
] },
// Allow now gated by the expired permit
{ uid: "f-disp-allow", fragment_type: "control",
ref_type: "fragment", ref_id: "f-disp-allow",
payload: {
available: false,
unavailable_reason: "Permit expired (see findings).",
blockers: [{ code: "permit_expired",
message: "Permit expired 2026-03-01.",
refs: ["pc-permit"] }]
} }
]
Same packet, same disposition slots — only an annotated finding fragment and a control mutation. The CLI port re-renders:
You unfold the permit. The Imperial seal is sound, but the date stamp
shows expiry months past.
[findings]
✓ permit_seal = Imperial
!! permit_expiry = 2026-03-01
1) Inspect a document.
a) Allow passage. (locked: Permit expired (see findings).)
d) Deny passage.
Turn 3 — disposition¶
Player commits e-deny. The backend returns the next candidate and
appends a row to the shift summary.
9 · Time pressure¶
Per main spec §0.3 and §0.6, time pressure is backend territory.
The bundle MAY emit periodic envelopes that decrement a visible time
counter via update control fragments. When time reaches zero, the
backend emits a control fragment marking all open dispositions
available=false with unavailable_reason="Shift ended.".
No client-side timer primitive. Per §0.2 CLI Floor Rule, the CLI port
renders the projected time_remaining row exactly as the web port
does — both update only on backend tick.
10 · Port parity addendum¶
Widget |
Web (Vue) |
CLI |
tkinter |
Hypothetical Godot |
|---|---|---|---|---|
Candidate piece |
photo + declared-purpose chip |
line: |
|
NPC 3D portrait |
Packet zone |
row of document tiles |
|
|
spatial array on counter |
Finding row ( |
check icon + muted text |
|
green text |
green chip |
Finding row ( |
warning chip + amber text |
|
amber text |
amber chip |
Finding row ( |
danger chip + red text + pulse |
|
red text |
red chip + sound |
Disposition ( |
green button |
|
green |
green panel |
Disposition ( |
amber button |
|
amber |
amber panel |
Disposition ( |
red button with confirm |
|
red |
red panel + confirm |
Disposition (locked) |
grayed + reason tooltip |
|
disabled + label |
grayed + tooltip |
Restriction map |
sidebar |
|
|
corkboard |
Shift summary |
table |
aligned columns |
|
scroll table |
Appendix — Prior art¶
Lucas Pope’s Papers, Please (2013) is the immediate inspiration for this genre profile. Pope’s design innovation is decoupling the procedural (verify documents against shifting rules) from the moral (every disposition is a small ethical choice). This bundle profile honors both halves: the verification mechanics are contract-rendered cleanly; the moral weight lives in bundle-authored prose around dispositions.
The credentials genre also overlaps with judicial process simulators (Phoenix Wright-style logical-discrepancy hunting), bureaucratic fiction (Kafka’s The Trial, Mieville’s Embassytown), and contemporary procedural games (This War of Mine’s ethical-triage loops). The vocabulary lifts cleanly to all of them.
The credentials engine architecture is documented separately in
CREDENTIALS_LOOP_DESIGN.md. The engine-side Outcome hierarchy,
Move enum, RestrictionLevel ordering, and indication-generation
doctrine are authoring concerns; this document is rendering contract.
End of credentials EXTENSIONS v0.2.