draft-protocol 1.0.0__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/PKG-INFO +1 -1
  2. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/pyproject.toml +1 -1
  3. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/src/draft_protocol/__init__.py +1 -1
  4. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/src/draft_protocol/engine.py +89 -1
  5. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/src/draft_protocol/storage.py +19 -0
  6. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/tests/test_draft_protocol.py +198 -0
  7. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/.dockerignore +0 -0
  8. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/.editorconfig +0 -0
  9. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/.github/CODEOWNERS +0 -0
  10. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  11. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  12. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/.github/dependabot.yml +0 -0
  14. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/.github/workflows/ci.yml +0 -0
  15. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/.github/workflows/release.yml +0 -0
  16. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/.gitignore +0 -0
  17. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/.pre-commit-config.yaml +0 -0
  18. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/AGENTS.md +0 -0
  19. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/BENCHMARKS.md +0 -0
  20. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/CHANGELOG.md +0 -0
  21. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/CODE_OF_CONDUCT.md +0 -0
  22. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/CONFORMANCE.md +0 -0
  23. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/CONTRIBUTING.md +0 -0
  24. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/Dockerfile +0 -0
  25. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/INTEGRATIONS.md +0 -0
  26. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/LICENSE +0 -0
  27. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/METHODOLOGY.md +0 -0
  28. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/Makefile +0 -0
  29. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/README.md +0 -0
  30. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/RELEASING.md +0 -0
  31. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/ROADMAP.md +0 -0
  32. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/RULES.md +0 -0
  33. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/SECURITY.md +0 -0
  34. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/STRUCTURE.md +0 -0
  35. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/THREAT_MODEL.md +0 -0
  36. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/docker-compose.example.yml +0 -0
  37. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/docs/README.md +0 -0
  38. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/docs/api.md +0 -0
  39. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/docs/architecture.md +0 -0
  40. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/examples/README.md +0 -0
  41. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/examples/basic_usage.py +0 -0
  42. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/extension/background.js +0 -0
  43. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/extension/content.css +0 -0
  44. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/extension/content.js +0 -0
  45. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/extension/icons/icon128.png +0 -0
  46. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/extension/icons/icon16.png +0 -0
  47. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/extension/icons/icon48.png +0 -0
  48. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/extension/manifest.json +0 -0
  49. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/extension/popup.html +0 -0
  50. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/extension/popup.js +0 -0
  51. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/extension/sidepanel.html +0 -0
  52. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/extension/sidepanel.js +0 -0
  53. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/src/draft_protocol/__main__.py +0 -0
  54. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/src/draft_protocol/config.py +0 -0
  55. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/src/draft_protocol/providers.py +0 -0
  56. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/src/draft_protocol/py.typed +0 -0
  57. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/src/draft_protocol/rest.py +0 -0
  58. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/src/draft_protocol/server.py +0 -0
  59. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/tests/conftest.py +0 -0
  60. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/tests/test_rest.py +0 -0
  61. {draft_protocol-1.0.0 → draft_protocol-1.1.0}/tests/test_security.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draft-protocol
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: DRAFT Protocol — Intake governance for AI tool calls. Ensures AI understands human intent before execution begins.
5
5
  Project-URL: Homepage, https://github.com/manifold-vectors/draft-protocol
6
6
  Project-URL: Documentation, https://github.com/manifold-vectors/draft-protocol#readme
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draft-protocol"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  description = "DRAFT Protocol — Intake governance for AI tool calls. Ensures AI understands human intent before execution begins."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -13,7 +13,7 @@ Usage:
13
13
  from draft_protocol.providers import llm_available, embed_available
14
14
  """
15
15
 
16
- __version__ = "1.0.0"
16
+ __version__ = "1.1.0"
17
17
 
18
18
  # Public API — importable from `draft_protocol` directly
19
19
  from draft_protocol.engine import (
@@ -6,6 +6,9 @@ Features:
6
6
  3. Context-aware suggestions — optional LLM scaffolds or static templates
7
7
  4. Classification confidence scoring — 0.0-1.0 on all assessments
8
8
  5. Graceful degradation — works without any LLM, better with one
9
+ 6. Closed session guards — all operations reject closed sessions (M1.3)
10
+ 7. Tier enum validation — rejects invalid tier strings (M1.4)
11
+ 8. Context enrichment — gate PASS returns full dimensional mapping (M1.5)
9
12
 
10
13
  Supports any LLM provider via DRAFT_LLM_PROVIDER env var:
11
14
  ollama, openai (+ compatible APIs), anthropic, or none (keyword-only).
@@ -23,6 +26,18 @@ from draft_protocol.config import (
23
26
  STANDARD_TRIGGERS,
24
27
  )
25
28
 
29
+ # ── M1.3: Closed Session Guard ───────────────────────────
30
+
31
+ _CLOSED_SESSION_ERROR = "Session {sid} is closed. Start new session with draft_intake."
32
+
33
+
34
+ def _check_open(session_id: str) -> dict | None:
35
+ """Return error dict if session is closed or missing, else None."""
36
+ if storage.is_session_closed(session_id):
37
+ return {"error": _CLOSED_SESSION_ERROR.format(sid=session_id)}
38
+ return None
39
+
40
+
26
41
  # ── LLM Schemas ───────────────────────────────────────────
27
42
 
28
43
  TIER_SCHEMA = {
@@ -218,6 +233,11 @@ def _field_enrichment(field_key: str) -> str:
218
233
 
219
234
  def map_dimensions(session_id: str, context: str) -> dict:
220
235
  """Map DRAFT dimensions against user context using LLM or heuristics."""
236
+ # M1.3: Closed session guard
237
+ closed = _check_open(session_id)
238
+ if closed:
239
+ return closed
240
+
221
241
  session = storage.get_session(session_id)
222
242
  if not session:
223
243
  return {"error": f"Session {session_id} not found"}
@@ -388,6 +408,11 @@ def _context_suggests_applicable(dim_key: str, context: str) -> bool:
388
408
 
389
409
  def generate_elicitation(session_id: str) -> list[dict]:
390
410
  """Generate targeted questions for MISSING/AMBIGUOUS fields."""
411
+ # M1.3: Closed session guard
412
+ closed = _check_open(session_id)
413
+ if closed:
414
+ return [closed]
415
+
391
416
  session = storage.get_session(session_id)
392
417
  if not session:
393
418
  return [{"error": f"Session {session_id} not found"}]
@@ -465,6 +490,11 @@ def _suggest_answer(field_key: str, intent: str) -> str | None:
465
490
 
466
491
  def generate_assumptions(session_id: str) -> list[dict]:
467
492
  """Surface key assumptions as falsifiable claims."""
493
+ # M1.3: Closed session guard
494
+ closed = _check_open(session_id)
495
+ if closed:
496
+ return [closed]
497
+
468
498
  session = storage.get_session(session_id)
469
499
  if not session:
470
500
  return [{"error": f"Session {session_id} not found"}]
@@ -508,6 +538,11 @@ def generate_assumptions(session_id: str) -> list[dict]:
508
538
 
509
539
  def check_gate(session_id: str) -> dict:
510
540
  """Check whether all applicable fields are confirmed."""
541
+ # M1.3: Closed session guard
542
+ closed = _check_open(session_id)
543
+ if closed:
544
+ return {"passed": False, "blockers": [closed["error"]], "summary": "ERROR"}
545
+
511
546
  session = storage.get_session(session_id)
512
547
  if not session:
513
548
  return {"passed": False, "blockers": ["Session not found"], "summary": "ERROR"}
@@ -554,7 +589,7 @@ def check_gate(session_id: str) -> dict:
554
589
 
555
590
  storage.log_audit(session_id, "draft_gate", "gate_check", f"{'PASS' if passed else 'FAIL'}: {confirmed}/{total}")
556
591
 
557
- return {
592
+ result = {
558
593
  "passed": passed,
559
594
  "confirmed": confirmed,
560
595
  "total": total,
@@ -562,12 +597,40 @@ def check_gate(session_id: str) -> dict:
562
597
  "summary": f"{'[PASS]' if passed else '[BLOCKED]'}: {confirmed}/{total}",
563
598
  }
564
599
 
600
+ # M1.5: Context enrichment — compliant agents get rich context for free
601
+ if passed:
602
+ enrichment = {}
603
+ for dim_key in ["D", "R", "A", "F", "T"]:
604
+ dim_fields = dims.get(dim_key, {})
605
+ if isinstance(dim_fields, dict) and dim_fields.get("_screened"):
606
+ enrichment[dim_key] = {"_screened": True, "_reason": dim_fields.get("_reason", "N/A")}
607
+ continue
608
+ dim_data = {}
609
+ for fk, info in dim_fields.items():
610
+ if fk.startswith("_") or not isinstance(info, dict):
611
+ continue
612
+ dim_data[fk] = {
613
+ "value": info.get("extracted", ""),
614
+ "question": info.get("question", ""),
615
+ "status": info.get("status", "UNKNOWN"),
616
+ }
617
+ enrichment[dim_key] = dim_data
618
+ result["context_enrichment"] = enrichment
619
+ result["tier"] = session.get("tier", "UNKNOWN")
620
+
621
+ return result
622
+
565
623
 
566
624
  # ── Field Operations ──────────────────────────────────────
567
625
 
568
626
 
569
627
  def confirm_field(session_id: str, field_key: str, value: str) -> dict:
570
628
  """Confirm a DRAFT field with a human-provided answer."""
629
+ # M1.3: Closed session guard
630
+ closed = _check_open(session_id)
631
+ if closed:
632
+ return closed
633
+
571
634
  session = storage.get_session(session_id)
572
635
  if not session:
573
636
  return {"error": "Session not found"}
@@ -611,6 +674,11 @@ def confirm_field(session_id: str, field_key: str, value: str) -> dict:
611
674
 
612
675
  def unscreen_dimension(session_id: str, dimension_key: str) -> dict:
613
676
  """Reverse screening on a dimension incorrectly marked N/A."""
677
+ # M1.3: Closed session guard
678
+ closed = _check_open(session_id)
679
+ if closed:
680
+ return closed
681
+
614
682
  session = storage.get_session(session_id)
615
683
  if not session:
616
684
  return {"error": "Session not found"}
@@ -637,6 +705,11 @@ def unscreen_dimension(session_id: str, dimension_key: str) -> dict:
637
705
 
638
706
  def add_assumption(session_id: str, claim: str, source: str = "manual", falsifier: str = "") -> dict:
639
707
  """Add a manually authored assumption."""
708
+ # M1.3: Closed session guard
709
+ closed = _check_open(session_id)
710
+ if closed:
711
+ return closed
712
+
640
713
  session = storage.get_session(session_id)
641
714
  if not session:
642
715
  return {"error": "Session not found"}
@@ -658,6 +731,11 @@ def add_assumption(session_id: str, claim: str, source: str = "manual", falsifie
658
731
 
659
732
  def override_gate(session_id: str, reason: str) -> dict:
660
733
  """Override a blocked gate with logged reason (authorized override)."""
734
+ # M1.3: Closed session guard
735
+ closed = _check_open(session_id)
736
+ if closed:
737
+ return closed
738
+
661
739
  session = storage.get_session(session_id)
662
740
  if not session:
663
741
  return {"error": "Session not found"}
@@ -677,6 +755,11 @@ def override_gate(session_id: str, reason: str) -> dict:
677
755
 
678
756
  def verify_assumption(session_id: str, index: int, verified: bool, note: str = "") -> dict:
679
757
  """Verify or reject an assumption."""
758
+ # M1.3: Closed session guard
759
+ closed = _check_open(session_id)
760
+ if closed:
761
+ return closed
762
+
680
763
  session = storage.get_session(session_id)
681
764
  if not session:
682
765
  return {"error": "Session not found"}
@@ -698,6 +781,11 @@ def verify_assumption(session_id: str, index: int, verified: bool, note: str = "
698
781
 
699
782
  def elicitation_review(session_id: str) -> dict:
700
783
  """Self-assessment of elicitation quality."""
784
+ # M1.3: Closed session guard
785
+ closed = _check_open(session_id)
786
+ if closed:
787
+ return closed
788
+
701
789
  session = storage.get_session(session_id)
702
790
  if not session:
703
791
  return {"error": "Session not found"}
@@ -7,6 +7,9 @@ from datetime import datetime, timezone
7
7
 
8
8
  from draft_protocol.config import DB_PATH
9
9
 
10
+ # M1.4: Valid tier enum — reject anything not in this set
11
+ VALID_TIERS = {"CASUAL", "STANDARD", "CONSEQUENTIAL"}
12
+
10
13
 
11
14
  def get_db() -> sqlite3.Connection:
12
15
  conn = sqlite3.connect(str(DB_PATH))
@@ -57,6 +60,9 @@ def _now() -> str:
57
60
 
58
61
  def create_session(tier: str, intent: str) -> str:
59
62
  """Create a new DRAFT session. Returns session_id."""
63
+ # M1.4: Validate tier enum
64
+ if tier not in VALID_TIERS:
65
+ raise ValueError(f"Invalid tier '{tier}'. Must be one of: {', '.join(sorted(VALID_TIERS))}")
60
66
  sid = str(uuid.uuid4())[:12]
61
67
  conn = get_db()
62
68
  now = _now()
@@ -83,6 +89,16 @@ def get_session(session_id: str) -> dict | None:
83
89
  return d
84
90
 
85
91
 
92
+ def is_session_closed(session_id: str) -> bool:
93
+ """Check if a session is closed. M1.3: Closed session guard."""
94
+ conn = get_db()
95
+ row = conn.execute("SELECT closed_at FROM sessions WHERE id = ?", (session_id,)).fetchone()
96
+ conn.close()
97
+ if not row:
98
+ return True # Nonexistent sessions treated as closed
99
+ return row["closed_at"] is not None
100
+
101
+
86
102
  def get_active_session() -> dict | None:
87
103
  """Get the most recent unclosed session."""
88
104
  conn = get_db()
@@ -98,6 +114,9 @@ def get_active_session() -> dict | None:
98
114
 
99
115
  def update_session(session_id: str, **kwargs):
100
116
  """Update session fields. JSON fields auto-serialized."""
117
+ # M1.4: Validate tier if being updated
118
+ if "tier" in kwargs and kwargs["tier"] not in VALID_TIERS:
119
+ raise ValueError(f"Invalid tier '{kwargs['tier']}'. Must be one of: {', '.join(sorted(VALID_TIERS))}")
101
120
  conn = get_db()
102
121
  sets = ["updated_at = ?"]
103
122
  vals = [_now()]
@@ -21,10 +21,12 @@ from draft_protocol.engine import ( # noqa: E402
21
21
  verify_assumption,
22
22
  )
23
23
  from draft_protocol.storage import ( # noqa: E402
24
+ VALID_TIERS,
24
25
  close_session,
25
26
  create_session,
26
27
  get_active_session,
27
28
  get_session,
29
+ is_session_closed,
28
30
  log_audit,
29
31
  )
30
32
 
@@ -377,3 +379,199 @@ class TestProviderConfig:
377
379
  assert "ollama" in _EMBED_PROVIDERS
378
380
  assert "openai" in _EMBED_PROVIDERS
379
381
  assert "anthropic" in _EMBED_PROVIDERS
382
+
383
+
384
+ # ── M1.3: Closed Session Guards ───────────────────────────
385
+
386
+
387
+ class TestClosedSessionGuard:
388
+ """M1.3: All operations on closed sessions must return error."""
389
+
390
+ def _make_closed_session(self):
391
+ sid = create_session("STANDARD", "test")
392
+ map_dimensions(sid, "Build a tool for processing")
393
+ close_session(sid)
394
+ return sid
395
+
396
+ def test_is_session_closed(self):
397
+ sid = create_session("STANDARD", "test")
398
+ assert not is_session_closed(sid)
399
+ close_session(sid)
400
+ assert is_session_closed(sid)
401
+
402
+ def test_nonexistent_treated_as_closed(self):
403
+ assert is_session_closed("nonexistent_id_xyz")
404
+
405
+ def test_map_dimensions_blocked(self):
406
+ sid = self._make_closed_session()
407
+ result = map_dimensions(sid, "some context")
408
+ assert "error" in result
409
+ assert "closed" in result["error"].lower()
410
+
411
+ def test_generate_elicitation_blocked(self):
412
+ sid = self._make_closed_session()
413
+ result = generate_elicitation(sid)
414
+ assert len(result) == 1
415
+ assert "error" in result[0]
416
+ assert "closed" in result[0]["error"].lower()
417
+
418
+ def test_confirm_field_blocked(self):
419
+ sid = self._make_closed_session()
420
+ result = confirm_field(sid, "D1", "Some value here")
421
+ assert "error" in result
422
+ assert "closed" in result["error"].lower()
423
+
424
+ def test_check_gate_blocked(self):
425
+ sid = self._make_closed_session()
426
+ result = check_gate(sid)
427
+ assert not result["passed"]
428
+ assert any("closed" in b.lower() for b in result["blockers"])
429
+
430
+ def test_generate_assumptions_blocked(self):
431
+ sid = self._make_closed_session()
432
+ result = generate_assumptions(sid)
433
+ assert len(result) == 1
434
+ assert "error" in result[0]
435
+
436
+ def test_verify_assumption_blocked(self):
437
+ sid = self._make_closed_session()
438
+ result = verify_assumption(sid, 0, True)
439
+ assert "error" in result
440
+ assert "closed" in result["error"].lower()
441
+
442
+ def test_add_assumption_blocked(self):
443
+ sid = self._make_closed_session()
444
+ result = add_assumption(sid, "Test claim")
445
+ assert "error" in result
446
+ assert "closed" in result["error"].lower()
447
+
448
+ def test_override_gate_blocked(self):
449
+ sid = self._make_closed_session()
450
+ result = override_gate(sid, "Override reason")
451
+ assert "error" in result
452
+ assert "closed" in result["error"].lower()
453
+
454
+ def test_unscreen_dimension_blocked(self):
455
+ sid = self._make_closed_session()
456
+ result = unscreen_dimension(sid, "R")
457
+ assert "error" in result
458
+ assert "closed" in result["error"].lower()
459
+
460
+ def test_elicitation_review_blocked(self):
461
+ sid = self._make_closed_session()
462
+ result = elicitation_review(sid)
463
+ assert "error" in result
464
+ assert "closed" in result["error"].lower()
465
+
466
+ def test_error_message_includes_session_id(self):
467
+ sid = self._make_closed_session()
468
+ result = confirm_field(sid, "D1", "Some value")
469
+ assert sid in result["error"]
470
+
471
+
472
+ # ── M1.4: Tier Enum Validation ────────────────────────────
473
+
474
+
475
+ class TestTierEnumValidation:
476
+ """M1.4: Invalid tier strings must be rejected."""
477
+
478
+ def test_valid_tiers_constant_exists(self):
479
+ assert {"CASUAL", "STANDARD", "CONSEQUENTIAL"} == VALID_TIERS
480
+
481
+ def test_create_session_valid_tiers(self):
482
+ for tier in VALID_TIERS:
483
+ sid = create_session(tier, "test")
484
+ session = get_session(sid)
485
+ assert session["tier"] == tier
486
+
487
+ def test_create_session_invalid_tier_rejected(self):
488
+ import pytest
489
+ with pytest.raises(ValueError, match="Invalid tier"):
490
+ create_session("SUPER_HIGH", "test")
491
+
492
+ def test_create_session_lowercase_rejected(self):
493
+ import pytest
494
+ with pytest.raises(ValueError, match="Invalid tier"):
495
+ create_session("casual", "test")
496
+
497
+ def test_create_session_empty_tier_rejected(self):
498
+ import pytest
499
+ with pytest.raises(ValueError, match="Invalid tier"):
500
+ create_session("", "test")
501
+
502
+ def test_update_session_invalid_tier_rejected(self):
503
+ import pytest
504
+
505
+ from draft_protocol.storage import update_session
506
+ sid = create_session("CASUAL", "test")
507
+ with pytest.raises(ValueError, match="Invalid tier"):
508
+ update_session(sid, tier="INVALID")
509
+
510
+
511
+ # ── M1.5: Context Enrichment Return ──────────────────────
512
+
513
+
514
+ class TestContextEnrichment:
515
+ """M1.5: Gate PASS returns full dimensional mapping as structured context."""
516
+
517
+ def _make_passed_session(self):
518
+ sid = create_session("STANDARD", "test")
519
+ map_dimensions(sid, "Build a tool for processing data")
520
+ session = get_session(sid)
521
+ dims = session["dimensions"]
522
+ for _dk, fields in dims.items():
523
+ if isinstance(fields, dict) and fields.get("_screened"):
524
+ continue
525
+ for fk in fields:
526
+ if not fk.startswith("_"):
527
+ confirm_field(sid, fk, f"Confirmed value for {fk} with enough content")
528
+ return sid
529
+
530
+ def test_gate_pass_includes_enrichment(self):
531
+ sid = self._make_passed_session()
532
+ gate = check_gate(sid)
533
+ assert gate["passed"]
534
+ assert "context_enrichment" in gate
535
+
536
+ def test_enrichment_has_all_dimensions(self):
537
+ sid = self._make_passed_session()
538
+ gate = check_gate(sid)
539
+ enrichment = gate["context_enrichment"]
540
+ for dim_key in ["D", "R", "A", "F", "T"]:
541
+ assert dim_key in enrichment
542
+
543
+ def test_enrichment_fields_have_value_and_question(self):
544
+ sid = self._make_passed_session()
545
+ gate = check_gate(sid)
546
+ enrichment = gate["context_enrichment"]
547
+ for _dim_key, dim_data in enrichment.items():
548
+ if dim_data.get("_screened"):
549
+ continue
550
+ for fk, info in dim_data.items():
551
+ if fk.startswith("_"):
552
+ continue
553
+ assert "value" in info
554
+ assert "question" in info
555
+ assert "status" in info
556
+
557
+ def test_enrichment_includes_tier(self):
558
+ sid = self._make_passed_session()
559
+ gate = check_gate(sid)
560
+ assert "tier" in gate
561
+ assert gate["tier"] == "STANDARD"
562
+
563
+ def test_gate_fail_no_enrichment(self):
564
+ sid = create_session("STANDARD", "test")
565
+ map_dimensions(sid, "Build a tool")
566
+ gate = check_gate(sid)
567
+ assert not gate["passed"]
568
+ assert "context_enrichment" not in gate
569
+
570
+ def test_screened_dimensions_marked_in_enrichment(self):
571
+ sid = self._make_passed_session()
572
+ gate = check_gate(sid)
573
+ if gate["passed"]:
574
+ enrichment = gate["context_enrichment"]
575
+ for _dim_key, dim_data in enrichment.items():
576
+ if dim_data.get("_screened"):
577
+ assert "_reason" in dim_data
File without changes
File without changes
File without changes
File without changes
File without changes