RETE working memory redesign — typed property indexes (the hard risk)

Cluster cProfile (combined_40_ings, v0.5.421) flagged dict.get on
_facts as the #1 hot spot: 5.85M calls / 1.26s tottime. The cost
was per-call 3-tuple allocation + hashing in the canonical
`(uid, fact_type, key) -> WorkingMemoryFact` store, plus Fact
wrapper unwrapping at every read.

This release adds typed sub-indexes that mirror the property_value
and property_type slices of _facts:

  _property_values: dict[node_uid, dict[name, value]]
  _property_types : dict[node_uid, dict[name, type_name]]

Hot-path reads skip tuple construction and Fact wrapping:

  has_property(uid, name)        — 2x dict.get + `in`
  get_property_value(uid, name)  — 2x dict.get
  get_property_type(uid, name)   — 2x dict.get

Maintained alongside _facts in assert_fact, retract_fact,
retract_all_for_node, and clear. Contract test (18 cases) pins:
- read-side semantics (incl. falsy-value preservation: 0/''/False)
- mirror consistency across assert / retract / retract_all / clear
- has_property MUST distinguish property_value from property_type
- get_fact() compat for non-property fact_types

This is the "hard risk" optimization step — touches the core
storage contract of WM. Validated by 532/532 green tests including
CO2 invariant on test_benchmark_two_origins.

Local impact invisible (two_origins volume too low). Expected
cluster impact 0.4-0.8s on the 22.4s combined_40_ings baseline.
Cluster cProfile after deploy will tell us whether this also
reduces the cumtime for upstream callers (alpha_network.evaluate
3.46s, conditions.check 1.82s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>