diff --git a/solstone/apps/entities/talent/entity_observer.md b/solstone/apps/entities/talent/entity_observer.md index 1b6da966e..95b51cba9 100644 --- a/solstone/apps/entities/talent/entity_observer.md +++ b/solstone/apps/entities/talent/entity_observer.md @@ -71,7 +71,7 @@ Use operations to maintain the numbered current observations shown in context: - `update`: revise an existing numbered observation when fresh source evidence improves, narrows, or corrects it. - `drop`: remove an existing numbered observation when it is duplicated, stale, or fails the durability litmus. - `add`: append a new durable fact that is not already covered by the current observations. -- `keep`: explicitly leave an existing numbered observation unchanged when that is the right decision. +- `keep`: deliberately re-affirm an existing numbered observation against fresh confirming or contradicting evidence. Rules: - Use the `entity_id` from context. @@ -82,8 +82,10 @@ Rules: - Use `add` only for facts that pass the durability litmus. - One fact per observation — no compound sentences. - `relation` is `null` unless the observation asserts a relationship. `target_name` is the other entity's NAME, never an id. `kind` must be one of `works-with`, `works-at`, `reports-to`, `family-of`, `knows`, `uses`, `created`, `other`. `note` explains the relationship and is required when `kind` is `"other"`. -- The `reasoning` field is for audit only. -- Empty operations are valid when no changes are needed for an entity. +- For `add`, `update`, and `drop`, `reasoning` is one short clause, well under the cap. +- For `keep`, set `reasoning` to `null`. +- Emit `keep` only when fresh evidence makes an explicit re-affirmation useful; otherwise omit the operation. +- An entity with no operations is valid and preferred when no changes or re-affirmations are needed. ## Output Format @@ -100,7 +102,7 @@ Respond with a JSON object in this exact format: "target_index": 0, "content": "The revised durable observation text", "target_quote": "short exact quote from the old observation", - "reasoning": "Why this update is warranted", + "reasoning": "Fresh evidence narrows it", "relation": null }, { @@ -108,7 +110,7 @@ Respond with a JSON object in this exact format: "target_index": null, "content": "A new durable observation text", "target_quote": null, - "reasoning": "Why this qualifies", + "reasoning": "Durable uncaptured expertise", "relation": { "kind": "works-with", "target_name": "Bob Lee", @@ -120,7 +122,7 @@ Respond with a JSON object in this exact format: "target_index": 2, "content": null, "target_quote": "short exact quote from the old observation", - "reasoning": "Why this should be removed", + "reasoning": "Stale duplicate", "relation": null }, { @@ -128,7 +130,7 @@ Respond with a JSON object in this exact format: "target_index": 3, "content": null, "target_quote": null, - "reasoning": "Why this should remain unchanged", + "reasoning": null, "relation": null } ] diff --git a/solstone/apps/entities/talent/entity_observer.schema.json b/solstone/apps/entities/talent/entity_observer.schema.json index 426f8710c..a0db4339f 100644 --- a/solstone/apps/entities/talent/entity_observer.schema.json +++ b/solstone/apps/entities/talent/entity_observer.schema.json @@ -66,8 +66,12 @@ "maxLength": 300 }, "reasoning": { - "type": "string", - "maxLength": 300 + "type": [ + "string", + "null" + ], + "maxLength": 300, + "x-truncate": true }, "relation": { "type": [ @@ -112,7 +116,8 @@ }, "summary": { "type": "string", - "maxLength": 500 + "maxLength": 500, + "x-truncate": true } } } diff --git a/tests/test_entity_observer_context.py b/tests/test_entity_observer_context.py index bdd5a6444..43b4cee7b 100644 --- a/tests/test_entity_observer_context.py +++ b/tests/test_entity_observer_context.py @@ -532,7 +532,7 @@ def test_post_process_applies_operations_and_writes_outcome(tmp_path, monkeypatc { "op": "keep", "target_index": 2, - "reasoning": "Still durable.", + "reasoning": None, }, { "op": "add", diff --git a/tests/test_entity_observer_schema.py b/tests/test_entity_observer_schema.py index 9a5ab5efa..b437b7393 100644 --- a/tests/test_entity_observer_schema.py +++ b/tests/test_entity_observer_schema.py @@ -10,6 +10,7 @@ from jsonschema import Draft202012Validator from solstone.talent.story import ALLOWED_RELATION_KINDS from solstone.think.schema_bounds import unbounded_nodes +from solstone.think.schema_prep import SCHEMA_TRUNCATE_KEY from solstone.think.talent import get_talent SCHEMA_PATH = ( @@ -91,6 +92,72 @@ def test_valid_operations_payload(): ) +def test_reasoning_null_validates_for_each_operation_type(): + validator = Draft202012Validator(_load_schema()) + + assert validator.is_valid( + { + "entities": [ + { + "entity_id": "alice_johnson", + "operations": [ + { + "op": "update", + "target_index": 0, + "content": "Updated durable observation text", + "target_quote": "old durable observation", + "reasoning": None, + "relation": None, + }, + { + "op": "add", + "target_index": None, + "content": "New durable observation text", + "target_quote": None, + "reasoning": None, + "relation": None, + }, + { + "op": "drop", + "target_index": 2, + "content": None, + "target_quote": "stale observation", + "reasoning": None, + "relation": None, + }, + { + "op": "keep", + "target_index": 3, + "content": None, + "target_quote": None, + "reasoning": None, + "relation": None, + }, + ], + } + ], + "summary": "null reasoning validates", + } + ) + + +def test_audit_fields_carry_truncation_annotation(): + schema = _load_schema() + operation = schema["properties"]["entities"]["items"]["properties"]["operations"][ + "items" + ]["properties"] + + reasoning = operation["reasoning"] + summary = schema["properties"]["summary"] + + assert reasoning["type"] == ["string", "null"] + assert reasoning["maxLength"] == 300 + assert reasoning[SCHEMA_TRUNCATE_KEY] is True + assert summary["type"] == "string" + assert summary["maxLength"] == 500 + assert summary[SCHEMA_TRUNCATE_KEY] is True + + def test_invalid_missing_top_required(): validator = Draft202012Validator(_load_schema())