Skip to content

Commit 1ce81cc

Browse files
committed
Phase 6: persona arbitration simplification (inventory, authority metadata, routing, event compression, deprecations, docs, tests)
1 parent 046c8d3 commit 1ce81cc

13 files changed

Lines changed: 355 additions & 1 deletion

docs/governance/INVARIANTS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,13 @@ This file declares the governance anchor; enforcement logic will be implemented
180180

181181
- Semantic Outcome Lock: The canonical semantics (primary outcome derivation, item roles, and invariants) are locked under Phase 5 and may not be altered except via an explicit governance phase update. Changes to semantic meaning require a recorded governance decision and a corresponding implementation phase.
182182

183+
## Persona Arbitration Invariant (Phase 6)
184+
185+
- Statement: Persona authority and routing MUST be explicit, deterministic, and auditable. Persona outputs are evidence and may be compressed into a single primary persona cause for audit, but persona outputs MUST NOT arbitrarily change canonical checklist semantics without a governance decision.
186+
- Rules:
187+
- Personas MAY declare an optional `authority` of one of: `DECISIVE`, `ADVISORY`, `ANNOTATIVE` (metadata only in Phase 6).
188+
- Routing of persona invocation MUST be static and derived from the explicit routing table in `src/shieldcraft/persona/routing.py` (if configured); otherwise persona discovery falls back to `scope` rules.
189+
- Persona events are recorded atomically and deterministically in `artifacts/persona_events_v1.json` and hashed for integrity.
190+
- Persona outputs are compressed into a `checklist.persona_summary` structure for deterministic auditability; compression does not change primary checklist outcome semantics.
191+
- Enforcement: These invariants are enforced by documentation, deterministic routing, persona metadata, and persona event compression implemented in `finalize_checklist` (Phase 6).
192+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Persona Authority Model (Authoritative)
2+
3+
This file defines the Phase 6 **Persona Authority Model** (classification and metadata only).
4+
5+
Purpose: introduce a single, explicit authority class for personas to make decision authority auditable and deterministic.
6+
7+
## Authority classes
8+
9+
- `DECISIVE`
10+
- May cause REFUSAL outcomes by emitting vetoes (via `emit_veto`).
11+
- Decision-impacting; use sparingly and require explicit rationale in persona definitions.
12+
13+
- `ADVISORY`
14+
- May emit non-blocking constraints and BLOCKER-class signals (via `allowed_actions`), but cannot directly cause REFUSAL.
15+
16+
- `ANNOTATIVE`
17+
- Emit annotations and decisions recorded as evidence only. Non-authoritative for pipeline control.
18+
19+
## Implementation note (Phase 6)
20+
21+
- Personas may include an optional top-level field `authority` in their persona JSON to declare one of the above classes. This is metadata-only and does not change runtime behavior by itself.
22+
- The codebase now supports an optional `authority` attribute on `Persona` and `PersonaContext` dataclasses to carry this metadata deterministically.
23+
- Enforcement (mapping DECISIVE->veto permission, etc.) will be introduced in a follow-up Phase 6 runtime step after governance review.
24+
25+
## Auditability
26+
27+
- Persona authority MUST be recorded in PersonaEvents and engine-side persona decision records when decisions are emitted so that audits can reconstruct which persona class made the decision.
28+
29+
***
30+
31+
This document is authoritative for Phase 6 classification metadata and is intentionally declarative.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Persona Decision Surface (Inventory)
2+
3+
This document enumerates, facts-only, the observable persona decision points, veto paths, and annotation APIs in the codebase. It is an inventory for Phase 6 work and contains no proposals.
4+
5+
## Key APIs
6+
7+
- `shieldcraft.persona.emit_veto(engine, persona_ctx, phase, code, explanation, severity)`
8+
- Capability: **veto** (records a veto in engine state, emits persona events/annotations)
9+
- Audit: writes to `engine._persona_vetoes` and emits PersonaEvent/PersonaAnnotation via observability
10+
- Usage: called by persona evaluation code and tests
11+
12+
- `shieldcraft.persona.emit_annotation(engine, persona_ctx, phase, message, severity)`
13+
- Capability: **annotate** (non-authoritative; emits annotations and PersonaEvent)
14+
- Audit: appends to engine `_persona_annotations` and emits PersonaEvent
15+
16+
- `shieldcraft.persona.decision_record.record_decision(engine, persona_id, phase, decision)`
17+
- Capability: **decision** (records non-blocking persona decisions as PersonaEvents)
18+
- Audit: appends to engine `_persona_decisions` and emits PersonaEvent
19+
20+
## Evaluation & Enforcement Points
21+
22+
- `shieldcraft.persona.persona_evaluator.evaluate_personas(engine, personas, items, phase="checklist")`
23+
- Scope: **Generator** (ChecklistGenerator.build calls this before persona-influenced checks)
24+
- Decision types: **veto** (emit_veto), **constraint** (non-mutating instruction recorded via `record_decision`)
25+
- Determinism: iterates personas deterministically by `persona.name`
26+
27+
- `shieldcraft.services.validator.persona_gate.enforce_persona_veto(engine)`
28+
- Scope: **Validator / Generator / Readiness evaluator** (called from ChecklistGenerator and readiness evaluator)
29+
- Decision type: **veto enforcement** (raises `RuntimeError("persona_veto:...")` when vetoes present)
30+
- Determinism: selects single veto deterministically (sort by severity then persona_id)
31+
32+
- `Engine.preflight` persona veto check
33+
- Scope: **Engine preflight** (top-level preflight gate checks `engine._persona_vetoes`)
34+
- Decision type: **REFUSAL** (records `G7_PERSONA_VETO` event and raises `RuntimeError("persona_veto:...")`)
35+
36+
## Persona Loading & Discovery
37+
38+
- `shieldcraft.persona.load_persona(path)`
39+
- Scope: **persona discovery / engine self-host** (persona files loaded when persona feature enabled)
40+
- Decision types: **load-time errors** (raises `PersonaError` on invalid persona files, `SyncError` on repo sync failures)
41+
42+
- `shieldcraft.persona.persona_registry` (register/list/find_personas_for_phase)
43+
- Scope: **in-memory registry for deterministic test/runtime persona evaluation**
44+
- Behavior: `find_personas_for_phase(phase)` filters by persona `scope` (phase or `all`)
45+
46+
## Observability / Audit
47+
48+
- `shieldcraft.observability.emit_persona_event(engine, persona_id, capability, phase, payload_ref, severity)`
49+
- Records PersonaEvent payloads (`persona_events_v1.json`) deterministically and emits a companion hash
50+
51+
- `shieldcraft.observability.emit_persona_annotation(engine, persona_id, phase, message, severity)`
52+
- Records persona annotations (`persona_annotations_v1.json`) deterministically
53+
54+
## Tests and Instrumentation (examples)
55+
56+
- Tests call `emit_veto` and `emit_annotation` directly to simulate persona-driven vetoes and annotations
57+
- `persona_evaluator.evaluate_personas` is exercised in generator tests (ensures constraints are recorded and applied)
58+
59+
## Observations (facts-only)
60+
61+
- Persona APIs are intentionally conservative and deterministic (sorted iteration, deterministic discovery rules).
62+
- Persona decisions are recorded via PersonaEvent/Annotation and stored on the `engine` object for deterministic inspection.
63+
- Veto enforcement is a deterministic, centralized check (`enforce_persona_veto`) used in multiple phases.
64+
65+
This completes the factual inventory of persona decision points in the current codebase.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Persona Protocol Deprecations (Phase 6)
2+
3+
This file records **deprecated** or **archived** persona protocol mechanisms identified during Phase 6 so they can be audited and retired in a future phase. No deletions are performed in this phase — this document is *informational* and *non-authoritative* about policy.
4+
5+
Deprecated / Archived items (facts-only):
6+
7+
- **Legacy persona profiles** (`docs/persona/legacy/*`)
8+
- Rationale: these persona profiles are subordinate to the canonical `PERSONA_PROTOCOL.md` and are retained for historical/contextual reference only.
9+
10+
- **Ad-hoc persona routing by phase or discovery heuristics**
11+
- Rationale: implicit routing is superseded by the explicit routing table implemented in `src/shieldcraft/persona/routing.py` (Phase 6 introduces a deterministic, static routing table).
12+
13+
- **Implicit authority inference from persona role or phase**
14+
- Rationale: Phase 6 introduces an explicit `authority` classification (DECISIVE|ADVISORY|ANNOTATIVE); implicit inference from role/phase is deprecated and will be considered for removal in a later phase after governance review.
15+
16+
Notes:
17+
18+
- This document is intentionally conservative: it does not remove or change any code. It only flags mechanisms that are considered for future deprecation and records the rationale and candidate replacements introduced in Phase 6.

docs/governance/decision_log.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,14 @@ Authoritative decisions made on 2025-12-13 (Phase 13 kickoff). All decisions rec
118118
- Semantic stability assertion added to ensure deterministic, byte-stable output for identical inputs.
119119
- No persona, schema, or gate-ID changes were made; all logic was centralized in `finalize_checklist`.
120120
- Rationale: Lock semantic meanings to prevent regressions and ensure deterministic artifact semantics; changes to semantics require governance approval and a new implementation phase.
121+
122+
123+
## Phase 6 Closed — Persona Arbitration Simplified
124+
- Decision: RECORD
125+
- Summary (facts-only): Phase 6 (Persona Arbitration Simplification) is complete. Persona authority, routing, and event compression have been introduced as defensive, deterministic, metadata-only changes and documentation. Persona outputs remain auditable evidence and do not, by themselves, alter canonical checklist semantics.
126+
- Explicit state:
127+
- A Persona Authority Model (DECISIVE|ADVISORY|ANNOTATIVE) has been introduced as metadata; persona dataclasses now carry an optional `authority` attribute.
128+
- Deterministic, static persona routing (configurable) is available via `src/shieldcraft/persona/routing.py`; discovery falls back to `scope` when routing is not configured.
129+
- Persona events are compressed into a deterministic `checklist.persona_summary` structure for auditability; primary persona cause is selected deterministically and traceably.
130+
- Marked legacy persona protocol artifacts for deprecation and archived legacy docs as informational only.
131+
- Rationale: Simplify persona arbitration while preserving auditability, determinism, and decision authority; future runtime enforcement of authority classes will be gated by governance decisions.

src/shieldcraft/engine.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,53 @@ def _item_event_index(it):
183183
# Enforce semantic invariants now that roles are assigned
184184
_assert_semantic_invariants(checklist, primary_outcome, gate_outcomes)
185185

186+
# Persona event compression: summarize persona outputs deterministically
187+
try:
188+
persona_events = list(getattr(engine, '_persona_events', []) or [])
189+
if persona_events:
190+
# Map persona capability to nominal outcome for selection precedence
191+
def _capability_to_outcome(cap):
192+
if cap == 'veto':
193+
return 'REFUSAL'
194+
# annotations/decisions are diagnostics/evidence
195+
return 'DIAGNOSTIC'
196+
197+
# Build list with derived outcomes and preserve order
198+
pes = []
199+
for idx, pe in enumerate(persona_events):
200+
outcome = _capability_to_outcome(pe.get('capability'))
201+
pes.append({
202+
'persona_id': pe.get('persona_id'),
203+
'capability': pe.get('capability'),
204+
'phase': pe.get('phase'),
205+
'payload_ref': pe.get('payload_ref'),
206+
'severity': pe.get('severity'),
207+
'derived_outcome': outcome,
208+
'index': idx,
209+
})
210+
211+
# Select primary persona cause by precedence and deterministic tie-break
212+
prim = None
213+
if any(p.get('derived_outcome') == 'REFUSAL' for p in pes):
214+
candidates = [p for p in pes if p.get('derived_outcome') == 'REFUSAL']
215+
elif any(p.get('derived_outcome') == 'DIAGNOSTIC' for p in pes):
216+
candidates = [p for p in pes if p.get('derived_outcome') == 'DIAGNOSTIC']
217+
else:
218+
candidates = pes
219+
220+
if candidates:
221+
# deterministic selection: lowest persona_id, then earliest index
222+
prim = sorted(candidates, key=lambda p: (p.get('persona_id') or '', p.get('index')))[0]
223+
224+
checklist['persona_summary'] = {
225+
'primary_persona': prim.get('persona_id') if prim else None,
226+
'primary_capability': prim.get('capability') if prim else None,
227+
'events': pes,
228+
}
229+
except Exception:
230+
# Persona auditing must not interfere with finalization
231+
pass
232+
186233
# Expose primary outcome at the top-level result as required by the
187234
# Checklist Semantics Contract (Phase 5).
188235
result_primary_outcome = primary_outcome

src/shieldcraft/persona/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ class Persona:
7272
scope: List[str] = field(default_factory=list)
7373
allowed_actions: List[str] = field(default_factory=list)
7474
constraints: Dict[str, Any] = field(default_factory=dict)
75+
# Optional authority classification for Phase 6 (DECISIVE|ADVISORY|ANNOTATIVE)
76+
authority: Optional[str] = None
7577

7678

7779
@dataclass(frozen=True)
@@ -82,6 +84,7 @@ class PersonaContext:
8284
scope: List[str]
8385
allowed_actions: List[str]
8486
constraints: Dict[str, Any]
87+
authority: Optional[str] = None
8588

8689
def to_dict(self) -> Dict[str, Any]:
8790
return {
@@ -91,6 +94,7 @@ def to_dict(self) -> Dict[str, Any]:
9194
"scope": list(self.scope) if self.scope is not None else [],
9295
"allowed_actions": list(self.allowed_actions) if self.allowed_actions is not None else [],
9396
"constraints": dict(self.constraints) if self.constraints is not None else {},
97+
"authority": self.authority,
9498
}
9599

96100
def to_canonical_json(self) -> str:
@@ -287,6 +291,7 @@ def load_persona(path: str) -> Persona:
287291
scope=data.get("scope", []),
288292
allowed_actions=data.get("allowed_actions", []),
289293
constraints=data.get("constraints", {}),
294+
authority=data.get("authority"),
290295
)
291296

292297

src/shieldcraft/persona/persona_registry.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,25 @@ def list_personas() -> List[Persona]:
2828

2929

3030
def find_personas_for_phase(phase: str) -> List[Persona]:
31+
"""Return personas applicable to a phase.
32+
33+
Behavior:
34+
- If a static routing table is configured (see `persona.routing`), filter
35+
by allowed persona names for the phase. Otherwise fall back to persona
36+
`scope` rules (existing behavior).
37+
- Deterministic ordering (sorted by persona.name) is preserved.
38+
"""
39+
from shieldcraft.persona.routing import get_allowed_persona_names_for_phase
40+
3141
res = []
3242
for p in list_personas():
3343
if not p.scope or 'all' in p.scope or phase in p.scope:
3444
res.append(p)
35-
return res
45+
46+
allowed = get_allowed_persona_names_for_phase(phase)
47+
if allowed is None:
48+
return res
49+
50+
# Filter by allowed names deterministically
51+
allowed_set = set(allowed)
52+
return [p for p in res if p.name in allowed_set]

src/shieldcraft/persona/routing.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Persona routing table: maps {phase} -> allowed persona names.
2+
3+
This module centralizes static, deterministic routing of personas to phases.
4+
By default, routing is empty and discovery falls back to persona `scope` rules.
5+
Routing is pure configuration and deterministic (lexicographic ordering applied where relevant).
6+
"""
7+
from typing import Dict, List, Optional
8+
9+
# Example shape: {"checklist": ["security_auditor", "licensor"], "preflight": ["governance"]}
10+
DEFAULT_ROUTING: Dict[str, List[str]] = {}
11+
12+
13+
def get_allowed_persona_names_for_phase(phase: str) -> Optional[List[str]]:
14+
"""Return list of allowed persona names for `phase` or None if not configured."""
15+
if not DEFAULT_ROUTING:
16+
return None
17+
return sorted(DEFAULT_ROUTING.get(phase, []))
18+
19+
20+
def set_routing(mapping: Dict[str, List[str]]) -> None:
21+
"""Set routing mapping for runtime/testing. Mapping should be deterministic (string lists).
22+
23+
WARNING: This mutates module-level routing state and is intended to be used in deterministic
24+
test and configuration flows only. Production usage should set `DEFAULT_ROUTING` via
25+
a well-reviewed configuration change.
26+
"""
27+
global DEFAULT_ROUTING
28+
DEFAULT_ROUTING = {k: sorted(v) for k, v in mapping.items()}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import json
2+
from shieldcraft.persona import Persona, PersonaContext, load_persona
3+
4+
5+
def test_persona_authority_field_roundtrip(tmp_path):
6+
p = Persona(name='x', role=None, display_name=None, scope=['preflight'], allowed_actions=['annotate'], constraints={}, authority='ANNOTATIVE')
7+
ctx = PersonaContext(name=p.name, role=p.role, display_name=p.display_name, scope=p.scope, allowed_actions=p.allowed_actions, constraints=p.constraints, authority=p.authority)
8+
d = ctx.to_dict()
9+
assert d.get('authority') == 'ANNOTATIVE'
10+
11+
# Now write a persona JSON and load via load_persona
12+
persona_file = tmp_path / 'persona.json'
13+
persona_file.write_text(json.dumps({'name': 'y', 'version': '1.0', 'persona_version': 'v1', 'authority': 'DECISIVE'}))
14+
# Monkeypatch repo/worktree checks so load_persona can run in test env
15+
import shieldcraft.services.sync as syncmod
16+
syncmod.verify_repo_sync = lambda root: None
17+
import shieldcraft.persona as pm
18+
pm._is_worktree_clean = lambda: True
19+
loaded = load_persona(str(persona_file))
20+
assert loaded.authority == 'DECISIVE'

0 commit comments

Comments
 (0)