Skip to content

Commit 0f7be19

Browse files
susiejojoclaude
andauthored
feat(wiki): reject dangling principles in validation (#287)
Principles in principles.json that aren't referenced by any entity, concept, or parameter in concepts.json are now flagged as errors by validate_concepts.py. This prevents "dark knowledge" — principles that are stored but never surface through retrieval or visualization. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fe8873b commit 0f7be19

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

.claude/commands/post-campaign.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ Index a completed Nous campaign into the shared wiki and generate a visualizatio
222222
- Extract 5-15 concepts, 3-10 parameters, and 5-10 entities per campaign.
223223
- Only include parameters that were actively varied during the campaign.
224224
- Skip common industry terms (TTFT, LLM, GPU, p99, etc.). Focus on campaign-specific vocabulary.
225-
- Every active principle should be referenced by at least one concept, parameter, or entity.
225+
- Every active principle MUST be referenced by at least one concept, parameter, or entity. The validator will reject dangling principles (present in principles.json but not referenced by any node). If a principle has no natural home, either create a concept that captures the pattern it describes, or attach it to the most relevant existing entity/concept.
226226
- The `evolution` array for parameters should include every iteration where the parameter's value was meaningfully varied.
227227
228228
**Entity validation (mandatory):** After drafting the entity list, verify each entity is truly pre-existing — NOT something the campaign introduced. You cannot rely on git history (campaign code may not be committed). Instead, use these sources of truth:
@@ -262,6 +262,7 @@ Index a completed Nous campaign into the shared wiki and generate a visualizatio
262262
- "orphaned parameter" → add it to the owning concept's `parameters` array
263263
- "unreachable entity" → either add an `operates_on` reference from a concept, or remove the entity
264264
- "unknown entity/parameter/concept" → fix the spelling to match exactly
265+
- "dangling principles" → add the principle ID to the `principles` array of the most relevant entity, concept, or parameter. If no existing node fits, create a new concept that captures the pattern the principle describes.
265266
266267
Do NOT proceed to step 10b until `validate_concepts.py` exits 0.
267268

scripts/validate_concepts.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
7. Bidirectional consistency: concept.parameters ↔ parameter.parent_concept
1212
8. No orphaned parameters (not in any concept's parameters array)
1313
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)
1415
1516
Usage:
1617
python scripts/validate_concepts.py <path-to-concepts.json>
@@ -108,6 +109,47 @@ def validate(path: Path) -> list[str]:
108109
if not full_path.exists():
109110
errors.append(f"entity '{entity['name']}': source file not found: {full_path}")
110111

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+
111153
return errors
112154

113155

0 commit comments

Comments
 (0)