cadence-skill-installer 0.2.21 → 0.2.22
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.
package/package.json
CHANGED
|
@@ -653,8 +653,20 @@ def normalize_ideation_research(
|
|
|
653
653
|
continue
|
|
654
654
|
alias_lookup.setdefault(normalized_alias, set()).add(entity["entity_id"])
|
|
655
655
|
|
|
656
|
+
# Track block usage from explicit topic references before alias inference.
|
|
657
|
+
entity_blocks: dict[str, set[str]] = {}
|
|
658
|
+
entity_topic_refs: dict[str, list[tuple[str, str]]] = {}
|
|
659
|
+
for topic_ref in flat_topics:
|
|
660
|
+
block_id = topic_ref["block_id"]
|
|
661
|
+
topic_id = _string(topic_ref["topic"].get("topic_id"))
|
|
662
|
+
for entity_id in topic_ref["topic"]["related_entities"]:
|
|
663
|
+
entity_blocks.setdefault(entity_id, set()).add(block_id)
|
|
664
|
+
entity_topic_refs.setdefault(entity_id, []).append((block_id, topic_id))
|
|
665
|
+
|
|
656
666
|
for topic_ref in flat_topics:
|
|
657
667
|
topic = topic_ref["topic"]
|
|
668
|
+
topic_block_id = topic_ref["block_id"]
|
|
669
|
+
topic_id = _string(topic.get("topic_id"))
|
|
658
670
|
haystack = " ".join(
|
|
659
671
|
[
|
|
660
672
|
_string(topic.get("title")),
|
|
@@ -675,14 +687,24 @@ def normalize_ideation_research(
|
|
|
675
687
|
detected_ids.append(entity_id)
|
|
676
688
|
|
|
677
689
|
for entity_id in detected_ids:
|
|
678
|
-
if entity_id
|
|
679
|
-
|
|
690
|
+
if entity_id in topic["related_entities"]:
|
|
691
|
+
continue
|
|
680
692
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
693
|
+
entity = entity_index.get(entity_id)
|
|
694
|
+
if entity is None:
|
|
695
|
+
continue
|
|
696
|
+
|
|
697
|
+
owner_block_id = _string(entity.get("owner_block_id"))
|
|
698
|
+
referenced_blocks = entity_blocks.get(entity_id, set())
|
|
699
|
+
# Alias inference is advisory only and must never introduce cross-block links.
|
|
700
|
+
if owner_block_id and owner_block_id != topic_block_id:
|
|
701
|
+
continue
|
|
702
|
+
if referenced_blocks and topic_block_id not in referenced_blocks:
|
|
703
|
+
continue
|
|
704
|
+
|
|
705
|
+
topic["related_entities"].append(entity_id)
|
|
706
|
+
entity_blocks.setdefault(entity_id, set()).add(topic_block_id)
|
|
707
|
+
entity_topic_refs.setdefault(entity_id, []).append((topic_block_id, topic_id))
|
|
686
708
|
|
|
687
709
|
for entity in normalized_entities:
|
|
688
710
|
entity_id = entity["entity_id"]
|
|
@@ -690,9 +712,17 @@ def normalize_ideation_research(
|
|
|
690
712
|
owner_block_id = _string(entity.get("owner_block_id"))
|
|
691
713
|
|
|
692
714
|
if len(referenced_blocks) > 1:
|
|
693
|
-
|
|
715
|
+
refs = ", ".join(
|
|
716
|
+
sorted(
|
|
717
|
+
f"{topic_id}@{block_id}"
|
|
718
|
+
for block_id, topic_id in entity_topic_refs.get(entity_id, [])
|
|
719
|
+
if block_id and topic_id
|
|
720
|
+
)
|
|
721
|
+
)
|
|
694
722
|
raise ResearchAgendaValidationError(
|
|
695
|
-
f"ENTITY_BLOCK_CONFLICT: entity '{entity_id}' is referenced across multiple blocks
|
|
723
|
+
f"ENTITY_BLOCK_CONFLICT: entity '{entity_id}' is referenced across multiple blocks "
|
|
724
|
+
f"({', '.join(sorted(referenced_blocks))})."
|
|
725
|
+
f"{f' References: {refs}.' if refs else ''}"
|
|
696
726
|
)
|
|
697
727
|
|
|
698
728
|
if owner_block_id:
|
|
@@ -702,9 +732,17 @@ def normalize_ideation_research(
|
|
|
702
732
|
)
|
|
703
733
|
if referenced_blocks and owner_block_id not in referenced_blocks:
|
|
704
734
|
block_label = _ordered_block_choice(referenced_blocks, block_order)
|
|
735
|
+
refs = ", ".join(
|
|
736
|
+
sorted(
|
|
737
|
+
f"{topic_id}@{block_id}"
|
|
738
|
+
for block_id, topic_id in entity_topic_refs.get(entity_id, [])
|
|
739
|
+
if block_id and topic_id
|
|
740
|
+
)
|
|
741
|
+
)
|
|
705
742
|
raise ResearchAgendaValidationError(
|
|
706
743
|
f"ENTITY_OWNER_MISMATCH: entity '{entity_id}' owner block '{owner_block_id}' "
|
|
707
744
|
f"does not match referenced block '{block_label}'."
|
|
745
|
+
f"{f' References: {refs}.' if refs else ''}"
|
|
708
746
|
)
|
|
709
747
|
elif referenced_blocks:
|
|
710
748
|
entity["owner_block_id"] = _ordered_block_choice(referenced_blocks, block_order)
|
|
Binary file
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
SCRIPTS_DIR = Path(__file__).resolve().parents[1] / "scripts"
|
|
7
|
+
if str(SCRIPTS_DIR) not in sys.path:
|
|
8
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
9
|
+
|
|
10
|
+
from ideation_research import ResearchAgendaValidationError, normalize_ideation_research
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def base_payload() -> dict:
|
|
14
|
+
return {
|
|
15
|
+
"objective": "test",
|
|
16
|
+
"research_agenda": {
|
|
17
|
+
"blocks": [
|
|
18
|
+
{
|
|
19
|
+
"block_id": "block-a",
|
|
20
|
+
"title": "Block A",
|
|
21
|
+
"rationale": "",
|
|
22
|
+
"tags": [],
|
|
23
|
+
"topics": [
|
|
24
|
+
{
|
|
25
|
+
"topic_id": "topic-a1",
|
|
26
|
+
"title": "Topic A1",
|
|
27
|
+
"category": "general",
|
|
28
|
+
"priority": "high",
|
|
29
|
+
"why_it_matters": "",
|
|
30
|
+
"research_questions": ["q"],
|
|
31
|
+
"keywords": [],
|
|
32
|
+
"tags": [],
|
|
33
|
+
"related_entities": [],
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"block_id": "block-b",
|
|
39
|
+
"title": "Block B",
|
|
40
|
+
"rationale": "",
|
|
41
|
+
"tags": [],
|
|
42
|
+
"topics": [
|
|
43
|
+
{
|
|
44
|
+
"topic_id": "topic-b1",
|
|
45
|
+
"title": "Topic B1",
|
|
46
|
+
"category": "general",
|
|
47
|
+
"priority": "high",
|
|
48
|
+
"why_it_matters": "",
|
|
49
|
+
"research_questions": ["q"],
|
|
50
|
+
"keywords": [],
|
|
51
|
+
"tags": [],
|
|
52
|
+
"related_entities": [],
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
"entity_registry": [],
|
|
58
|
+
"topic_index": {},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class IdeationResearchNormalizationTests(unittest.TestCase):
|
|
64
|
+
def test_alias_inference_does_not_cross_owner_block(self) -> None:
|
|
65
|
+
payload = base_payload()
|
|
66
|
+
payload["research_agenda"]["blocks"][0]["topics"][0]["related_entities"] = ["entity-marketplace-fees"]
|
|
67
|
+
payload["research_agenda"]["blocks"][1]["topics"][0]["keywords"] = ["royalties"]
|
|
68
|
+
payload["research_agenda"]["entity_registry"] = [
|
|
69
|
+
{
|
|
70
|
+
"entity_id": "entity-marketplace-fees",
|
|
71
|
+
"label": "Marketplace fee system",
|
|
72
|
+
"kind": "economy-system",
|
|
73
|
+
"aliases": ["royalties", "platform fees"],
|
|
74
|
+
"owner_block_id": "block-a",
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
normalized = normalize_ideation_research(copy.deepcopy(payload), require_topics=True)
|
|
79
|
+
topics = normalized["research_agenda"]["blocks"]
|
|
80
|
+
|
|
81
|
+
self.assertEqual(
|
|
82
|
+
topics[0]["topics"][0]["related_entities"],
|
|
83
|
+
["entity-marketplace-fees"],
|
|
84
|
+
)
|
|
85
|
+
self.assertEqual(topics[1]["topics"][0]["related_entities"], [])
|
|
86
|
+
|
|
87
|
+
def test_alias_inference_does_not_cross_existing_ownerless_block_reference(self) -> None:
|
|
88
|
+
payload = base_payload()
|
|
89
|
+
payload["research_agenda"]["blocks"][0]["topics"][0]["related_entities"] = ["entity-royalties"]
|
|
90
|
+
payload["research_agenda"]["blocks"][1]["topics"][0]["keywords"] = ["royalties"]
|
|
91
|
+
payload["research_agenda"]["entity_registry"] = [
|
|
92
|
+
{
|
|
93
|
+
"entity_id": "entity-royalties",
|
|
94
|
+
"label": "Royalties",
|
|
95
|
+
"kind": "economy-system",
|
|
96
|
+
"aliases": ["royalties"],
|
|
97
|
+
"owner_block_id": "",
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
normalized = normalize_ideation_research(copy.deepcopy(payload), require_topics=True)
|
|
102
|
+
topics = normalized["research_agenda"]["blocks"]
|
|
103
|
+
entity = normalized["research_agenda"]["entity_registry"][0]
|
|
104
|
+
|
|
105
|
+
self.assertEqual(topics[1]["topics"][0]["related_entities"], [])
|
|
106
|
+
self.assertEqual(entity["owner_block_id"], "block-a")
|
|
107
|
+
|
|
108
|
+
def test_explicit_cross_block_reference_still_fails(self) -> None:
|
|
109
|
+
payload = base_payload()
|
|
110
|
+
payload["research_agenda"]["blocks"][1]["topics"][0]["related_entities"] = ["entity-marketplace-fees"]
|
|
111
|
+
payload["research_agenda"]["entity_registry"] = [
|
|
112
|
+
{
|
|
113
|
+
"entity_id": "entity-marketplace-fees",
|
|
114
|
+
"label": "Marketplace fee system",
|
|
115
|
+
"kind": "economy-system",
|
|
116
|
+
"aliases": ["royalties"],
|
|
117
|
+
"owner_block_id": "block-a",
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
with self.assertRaises(ResearchAgendaValidationError) as ctx:
|
|
122
|
+
normalize_ideation_research(copy.deepcopy(payload), require_topics=True)
|
|
123
|
+
|
|
124
|
+
self.assertIn("ENTITY_OWNER_MISMATCH", str(ctx.exception))
|
|
125
|
+
|
|
126
|
+
def test_same_block_alias_inference_still_links_entity(self) -> None:
|
|
127
|
+
payload = base_payload()
|
|
128
|
+
payload["research_agenda"]["blocks"][1]["topics"][0]["keywords"] = ["royalties"]
|
|
129
|
+
payload["research_agenda"]["entity_registry"] = [
|
|
130
|
+
{
|
|
131
|
+
"entity_id": "entity-marketplace-fees",
|
|
132
|
+
"label": "Marketplace fee system",
|
|
133
|
+
"kind": "economy-system",
|
|
134
|
+
"aliases": ["royalties"],
|
|
135
|
+
"owner_block_id": "block-b",
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
normalized = normalize_ideation_research(copy.deepcopy(payload), require_topics=True)
|
|
140
|
+
block_b_topic = normalized["research_agenda"]["blocks"][1]["topics"][0]
|
|
141
|
+
|
|
142
|
+
self.assertEqual(block_b_topic["related_entities"], ["entity-marketplace-fees"])
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
unittest.main()
|