|
11 | 11 | 7. Bidirectional consistency: concept.parameters ↔ parameter.parent_concept |
12 | 12 | 8. No orphaned parameters (not in any concept's parameters array) |
13 | 13 | 9. No unreachable entities (not in any concept's operates_on) |
| 14 | + 10. No dangling principles (in principles.json but not referenced by any node) |
14 | 15 |
|
15 | 16 | Usage: |
16 | 17 | python scripts/validate_concepts.py <path-to-concepts.json> |
@@ -108,6 +109,47 @@ def validate(path: Path) -> list[str]: |
108 | 109 | if not full_path.exists(): |
109 | 110 | errors.append(f"entity '{entity['name']}': source file not found: {full_path}") |
110 | 111 |
|
| 112 | + # Dangling principles: every active principle in principles.json must be |
| 113 | + # referenced by at least one entity, concept, or parameter in concepts.json |
| 114 | + principles_path = path.parent / "principles.json" |
| 115 | + if principles_path.exists(): |
| 116 | + try: |
| 117 | + with open(principles_path) as f: |
| 118 | + principles_data = json.load(f) |
| 119 | + except json.JSONDecodeError as e: |
| 120 | + errors.append(f"principles.json contains invalid JSON: {e}") |
| 121 | + principles_data = None |
| 122 | + except OSError as e: |
| 123 | + errors.append(f"principles.json could not be read: {e}") |
| 124 | + principles_data = None |
| 125 | + |
| 126 | + if principles_data is not None: |
| 127 | + if not isinstance(principles_data, dict): |
| 128 | + errors.append( |
| 129 | + f"principles.json has invalid structure: expected a JSON object, " |
| 130 | + f"got {type(principles_data).__name__}" |
| 131 | + ) |
| 132 | + else: |
| 133 | + all_principle_ids = { |
| 134 | + p["id"] |
| 135 | + for p in principles_data.get("principles", []) |
| 136 | + if isinstance(p, dict) and "id" in p and p.get("status", "active") == "active" |
| 137 | + } |
| 138 | + referenced_principle_ids = set() |
| 139 | + for entity in data.get("entities", []): |
| 140 | + referenced_principle_ids.update(entity.get("principles") or []) |
| 141 | + for concept in data.get("concepts", []): |
| 142 | + referenced_principle_ids.update(concept.get("principles") or []) |
| 143 | + for param in data.get("parameters", []): |
| 144 | + referenced_principle_ids.update(param.get("principles") or []) |
| 145 | + |
| 146 | + dangling = sorted(all_principle_ids - referenced_principle_ids) |
| 147 | + if dangling: |
| 148 | + errors.append( |
| 149 | + f"dangling principles (in principles.json but not referenced by any " |
| 150 | + f"entity/concept/parameter): {dangling}" |
| 151 | + ) |
| 152 | + |
111 | 153 | return errors |
112 | 154 |
|
113 | 155 |
|
|
0 commit comments