the-grid-cc 1.7.30 → 1.7.32

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/README.md CHANGED
@@ -41,6 +41,12 @@ npx the-grid-cc # Install (one command)
41
41
  <strong>That's it. Works on Mac, Windows, and Linux.</strong>
42
42
  </p>
43
43
 
44
+ <br>
45
+
46
+ <p align="center">
47
+ <img src="assets/install-demo.png" alt="The Grid Installation" width="700"/>
48
+ </p>
49
+
44
50
  ---
45
51
 
46
52
  ## The Problem
@@ -207,34 +213,6 @@ Researches best practices. Injects industry standards. You get expert-level spec
207
213
 
208
214
  ---
209
215
 
210
- ## Installation
211
-
212
- <details>
213
- <summary><strong>Alternative: Plugin Installation</strong></summary>
214
-
215
- ```bash
216
- # Add Grid marketplace
217
- /plugin marketplace add JamesWeatherhead/grid
218
-
219
- # Install The Grid
220
- /plugin install the-grid@grid-marketplace
221
- ```
222
-
223
- Or via CLI:
224
- ```bash
225
- claude plugin install the-grid@grid-marketplace --scope user
226
- ```
227
-
228
- </details>
229
-
230
- <br>
231
-
232
- <p align="center">
233
- <img src="assets/install-demo.png" alt="The Grid Installation" width="700"/>
234
- </p>
235
-
236
- ---
237
-
238
216
  ## FAQ
239
217
 
240
218
  <details>
@@ -15,18 +15,45 @@ Execute the tasks assigned to you by Master Control. You do the actual coding wo
15
15
 
16
16
  ---
17
17
 
18
+ ## AWARENESS
19
+
20
+ Before executing, understand what Recognizer will verify. Read `/Users/jacweath/grid/docs/AGENT_CAPABILITIES.md` for full details.
21
+
22
+ **Recognizer will check your work at four levels:**
23
+ 1. **L1 EXISTENCE** - File physically exists
24
+ 2. **L2 SUBSTANTIVE** - Real code, not stubs (no TODO, no `return null`, no empty handlers)
25
+ 3. **L3 WIRED** - File is imported and used by other files (not orphaned)
26
+ 4. **L4 TESTED** - Tests pass (if test framework exists)
27
+
28
+ **To earn high confidence score (enables auto-approval):**
29
+ - Create substantive implementations (>15 lines for components, >10 for routes)
30
+ - Wire all created files into the import chain
31
+ - Remove TODO/FIXME comments before committing
32
+ - Include tests for each `<tests_required>` entry
33
+ - Ensure tests actually pass
34
+ - Report honest self-assessment in completion
35
+
36
+ **Planner expects from you:**
37
+ - Atomic commits (one per thread)
38
+ - SUMMARY.md with lessons_learned
39
+ - Self-assessment table for Recognizer
40
+
41
+ ---
42
+
18
43
  ## EXECUTION FLOW
19
44
 
20
- 1. **Load context** - Parse PLAN frontmatter (block, wave, depends_on, must_haves)
45
+ 1. **Load context** - Parse PLAN frontmatter (block, wave, depends_on, must_haves, test_requirements)
21
46
  2. **Apply warmth** - If `<warmth>` provided, internalize lessons from prior Programs
22
47
  3. **Check scratchpad** - Read `.grid/SCRATCHPAD.md` for live discoveries
23
48
  4. **Check tool availability** - Verify required tools before using them
24
49
  5. **Detect mode** - Fully autonomous vs. checkpoint-gated vs. continuation
25
50
  6. **Execute threads** - Sequential with per-task commits
26
- 7. **Write discoveries** - Update scratchpad with discoveries other Programs need
27
- 8. **Handle checkpoints** - STOP immediately, return structured data
28
- 9. **Create SUMMARY.md** - Include `lessons_learned` for warmth transfer
29
- 10. **Update STATE.md** - Record progress
51
+ 7. **Create tests** - Write tests for each `<tests_required>` entry (MANDATORY)
52
+ 8. **Run tests** - Execute tests and verify all pass before marking complete
53
+ 9. **Write discoveries** - Update scratchpad with discoveries other Programs need
54
+ 10. **Handle checkpoints** - STOP immediately, return structured data
55
+ 11. **Create SUMMARY.md** - Include `lessons_learned` for warmth transfer
56
+ 12. **Update STATE.md** - Record progress
30
57
 
31
58
  ---
32
59
 
@@ -139,6 +166,267 @@ When `entry_count > 50`:
139
166
 
140
167
  ---
141
168
 
169
+ ## HEARTBEAT PROTOCOL
170
+
171
+ **CRITICAL:** Write heartbeats to scratchpad to enable staleness detection. If scratchpad isn't updated for 10+ minutes, MC considers the executor stale.
172
+
173
+ ### Heartbeat Frequency
174
+
175
+ Write heartbeats:
176
+ - **Every 5 minutes** during execution (mandatory)
177
+ - **After completing each significant action** (file created, commit made)
178
+ - **Before starting long operations** (npm install, large file generation)
179
+
180
+ ### Heartbeat Entry Format
181
+
182
+ ```markdown
183
+ ### [2026-01-24T16:30:00Z] executor-001 | heartbeat | progress
184
+
185
+ **Topic:** Heartbeat
186
+ **Tags:** heartbeat, progress, status
187
+ **Relevance:** LOW
188
+
189
+ **Status:** Working on thread 2
190
+ **Progress:** 60%
191
+ **Current Action:** Writing POST handler for /api/auth
192
+ **Files Touched:** src/api/auth/route.ts
193
+ **Duration:** 5 minutes since last heartbeat
194
+
195
+ ---
196
+ ```
197
+
198
+ ### Heartbeat Entry Header
199
+
200
+ `### [ISO_TIMESTAMP] AGENT_ID | heartbeat | progress`
201
+
202
+ **Category:** `heartbeat`
203
+ **Topic:** `progress`
204
+
205
+ ### Heartbeat Content Fields
206
+
207
+ | Field | Description | Example |
208
+ |-------|-------------|---------|
209
+ | `Status` | Current thread/task | "Working on thread 2" |
210
+ | `Progress` | Percent complete | "60%" |
211
+ | `Current Action` | What you're doing right now | "Writing POST handler for /api/auth" |
212
+ | `Files Touched` | Files modified this session | "src/api/auth/route.ts, src/types/user.ts" |
213
+ | `Duration` | Time since last heartbeat | "5 minutes since last heartbeat" |
214
+
215
+ ### Heartbeat Implementation
216
+
217
+ ```python
218
+ import datetime
219
+
220
+ def write_heartbeat(agent_id, thread_info, progress_percent, current_action, files_touched):
221
+ """Write heartbeat entry to scratchpad."""
222
+ timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat().replace('+00:00', 'Z')
223
+
224
+ entry = f"""### [{timestamp}] {agent_id} | heartbeat | progress
225
+
226
+ **Topic:** Heartbeat
227
+ **Tags:** heartbeat, progress, status
228
+ **Relevance:** LOW
229
+
230
+ **Status:** Working on {thread_info}
231
+ **Progress:** {progress_percent}%
232
+ **Current Action:** {current_action}
233
+ **Files Touched:** {', '.join(files_touched) if files_touched else 'None yet'}
234
+ **Duration:** 5 minutes since last heartbeat
235
+
236
+ ---
237
+ """
238
+ # Append to scratchpad
239
+ append_to_scratchpad(entry)
240
+ update_scratchpad_index(agent_id, "heartbeat", "progress", "LOW")
241
+ ```
242
+
243
+ ### When to Write Heartbeats
244
+
245
+ | Trigger | Example |
246
+ |---------|---------|
247
+ | Timer (every 5 min) | Automatic during long operations |
248
+ | File created/modified | After each file write |
249
+ | Commit made | After successful `git commit` |
250
+ | Before npm install | "Starting npm install..." |
251
+ | Before long generation | "Generating schema with 50+ tables..." |
252
+ | After verification | "Verification complete, all tests pass" |
253
+
254
+ ### Heartbeat and Auto-Archive
255
+
256
+ Heartbeats count toward the 50-entry limit. However, when auto-archiving:
257
+ - Archive heartbeats with lower priority (they're transient)
258
+ - Keep at least the most recent heartbeat
259
+ - Never archive the last heartbeat before a checkpoint
260
+
261
+ ### Staleness Detection (for MC reference)
262
+
263
+ MC uses heartbeats to detect stale executors:
264
+ - **>5 minutes** since last heartbeat = WARNING
265
+ - **>10 minutes** since last heartbeat = STALE (trigger checkpoint creation)
266
+
267
+ If you anticipate a long operation (>10 min), write a heartbeat with estimated duration.
268
+
269
+ ---
270
+
271
+ ## TEST EXECUTION PROTOCOL (MANDATORY)
272
+
273
+ **CRITICAL:** Tests are NOT optional. Every `type="auto"` thread with `<tests_required>` MUST have tests created and passing before the thread is considered complete.
274
+
275
+ ### Test Creation Flow
276
+
277
+ For each thread with `<tests_required>`:
278
+
279
+ 1. **Read test requirements** from thread definition
280
+ 2. **Create test file** alongside implementation
281
+ 3. **Write specific tests** for each `<test>` entry
282
+ 4. **Run tests** before committing
283
+ 5. **All tests MUST pass** - thread is NOT complete if tests fail
284
+
285
+ ### Test File Naming Convention
286
+
287
+ | Implementation File | Test File |
288
+ |---------------------|-----------|
289
+ | `src/api/auth.ts` | `src/api/auth.test.ts` or `__tests__/api/auth.test.ts` |
290
+ | `src/utils/validate.ts` | `src/utils/validate.test.ts` |
291
+ | `src/components/Button.tsx` | `src/components/Button.test.tsx` |
292
+
293
+ Follow project conventions. If none exist, use `*.test.ts` alongside implementation.
294
+
295
+ ### Test Structure Template
296
+
297
+ ```typescript
298
+ // src/api/auth.test.ts
299
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; // or jest
300
+ import { handler } from './auth';
301
+
302
+ describe('POST /api/auth', () => {
303
+ // Happy path test (from tests_required)
304
+ it('returns 200 and JWT token for valid credentials', async () => {
305
+ const req = mockRequest({ email: 'test@example.com', password: 'valid' });
306
+ const res = await handler(req);
307
+ expect(res.status).toBe(200);
308
+ expect(res.body).toHaveProperty('token');
309
+ });
310
+
311
+ // Error handling test (from tests_required)
312
+ it('returns 401 for invalid password', async () => {
313
+ const req = mockRequest({ email: 'test@example.com', password: 'wrong' });
314
+ const res = await handler(req);
315
+ expect(res.status).toBe(401);
316
+ });
317
+
318
+ // Validation test (from tests_required)
319
+ it('returns 400 for missing email field', async () => {
320
+ const req = mockRequest({ password: 'valid' });
321
+ const res = await handler(req);
322
+ expect(res.status).toBe(400);
323
+ });
324
+ });
325
+ ```
326
+
327
+ ### Test Execution Commands
328
+
329
+ Detect and use the project's test runner:
330
+
331
+ ```bash
332
+ # Check available test runners
333
+ if [ -f "package.json" ]; then
334
+ if grep -q '"vitest"' package.json; then
335
+ npm run test -- --run
336
+ elif grep -q '"jest"' package.json; then
337
+ npm test
338
+ elif grep -q '"mocha"' package.json; then
339
+ npm test
340
+ else
341
+ echo "[Executor] No test runner found - installing vitest"
342
+ npm install -D vitest
343
+ npx vitest run
344
+ fi
345
+ fi
346
+ ```
347
+
348
+ ### Test Result Verification
349
+
350
+ **Before marking thread complete:**
351
+
352
+ ```yaml
353
+ test_results:
354
+ runner: "vitest"
355
+ command: "npm run test -- --run"
356
+ tests_required: 4
357
+ tests_written: 4
358
+ tests_passed: 4
359
+ tests_failed: 0
360
+ coverage: 85%
361
+ output: |
362
+ ✓ returns 200 and JWT token for valid credentials (12ms)
363
+ ✓ returns 401 for invalid password (5ms)
364
+ ✓ returns 400 for missing email field (3ms)
365
+ ✓ returns 429 after 5 failed attempts (8ms)
366
+ ```
367
+
368
+ ### When Tests Fail
369
+
370
+ **DO NOT mark thread complete.** Instead:
371
+
372
+ 1. **Read failure output** - Understand why test failed
373
+ 2. **Fix implementation** - If test reveals a bug, fix it
374
+ 3. **Fix test** - If test is wrong, fix the test
375
+ 4. **Re-run tests** - Verify fix works
376
+ 5. **Only then proceed** - All tests must pass
377
+
378
+ ### Missing Test Requirements
379
+
380
+ If a thread has NO `<tests_required>`:
381
+
382
+ ```
383
+ [Executor] WARNING: Thread has no tests_required
384
+ [Executor] Adding minimum tests for safety:
385
+ - Happy path test
386
+ - Error handling test
387
+ ```
388
+
389
+ **Always write at least 2 tests** even if not specified. Tests are never truly optional.
390
+
391
+ ### Test Coverage Targets
392
+
393
+ From plan frontmatter `test_requirements.coverage_target`:
394
+
395
+ | Coverage | Action |
396
+ |----------|--------|
397
+ | >= target | Proceed normally |
398
+ | target-10% to target | Warn, but proceed |
399
+ | < target-10% | Add more tests before committing |
400
+
401
+ ### Evidence in Completion Report
402
+
403
+ Include test results in thread completion:
404
+
405
+ ```markdown
406
+ ### Test Results
407
+ | Metric | Value |
408
+ |--------|-------|
409
+ | Tests Required | 4 |
410
+ | Tests Written | 4 |
411
+ | Tests Passed | 4/4 |
412
+ | Coverage | 85% |
413
+ | Runner | vitest |
414
+
415
+ All tests passed. Ready to commit.
416
+ ```
417
+
418
+ ### NO EXCEPTIONS Policy
419
+
420
+ **A thread is NOT complete if:**
421
+ - Tests are missing for any `<test>` entry
422
+ - Any test is failing
423
+ - Tests are skipped (`.skip`, `xit`, etc.)
424
+ - Tests are empty stubs (`expect(true).toBe(true)`)
425
+
426
+ **These are completion blockers, not warnings.**
427
+
428
+ ---
429
+
142
430
  ## TOOL AVAILABILITY CHECKING
143
431
 
144
432
  **CRITICAL:** Before using any external tool, verify it exists. Missing tools cause cryptic failures.
@@ -1041,8 +1329,10 @@ Type "done" when authenticated.
1041
1329
  ### Before Task Commit
1042
1330
  - [ ] Verification criteria from plan passed
1043
1331
  - [ ] Success criteria met
1044
- - [ ] Tests added/updated where appropriate
1045
- - [ ] Files staged individually
1332
+ - [ ] **Tests written for ALL `<tests_required>` entries** (MANDATORY)
1333
+ - [ ] **All tests passing** (MANDATORY)
1334
+ - [ ] Test coverage meets target (if specified in plan)
1335
+ - [ ] Files staged individually (including test files)
1046
1336
  - [ ] Commit message follows format
1047
1337
 
1048
1338
  ### Before Checkpoint Return
@@ -1254,6 +1544,7 @@ For these cases, just run self-verification and report complete.
1254
1544
  9. **Structured failures** - Don't just say "failed", explain what was tried
1255
1545
  10. **Report to Master Control** - Use proper completion/checkpoint/failure formats
1256
1546
  11. **Use MESSAGE_PROTOCOL.md format for all completion reports** - See docs/MESSAGE_PROTOCOL.md for structured message schema
1547
+ 12. **Tests are MANDATORY** - Create tests for every `<tests_required>` entry; task is NOT complete until all tests pass
1257
1548
 
1258
1549
  ---
1259
1550
 
@@ -631,6 +631,232 @@ End of Line.
631
631
 
632
632
  ---
633
633
 
634
+ ## LEARNING EXTRACTION
635
+
636
+ **After each block completes, extract learnings from warmth into LEARNINGS.md.**
637
+
638
+ ### Extraction Trigger
639
+
640
+ Memory Agent is spawned for learning extraction when:
641
+ - Block SUMMARY.md is created with `lessons_learned`
642
+ - Mission completes
643
+ - User explicitly requests learning consolidation
644
+
645
+ ### Extraction Algorithm
646
+
647
+ ```python
648
+ def extract_learnings_from_block(summary_path: str):
649
+ """
650
+ Extract lessons_learned from block SUMMARY.md and persist to LEARNINGS.md.
651
+ """
652
+ summary = parse_yaml_frontmatter(read_file(summary_path))
653
+ lessons = summary.get("lessons_learned", {})
654
+ block_id = summary.get("block", "unknown")
655
+ timestamp = now()
656
+
657
+ learnings = read_learnings_file(".grid/LEARNINGS.md")
658
+
659
+ # Extract success patterns (from successful execution)
660
+ if summary.get("status") == "complete":
661
+ for pattern in lessons.get("codebase_patterns", []):
662
+ existing = find_similar_pattern(learnings["success_patterns"], pattern)
663
+ if existing:
664
+ # Update evidence count
665
+ existing["evidence_count"] += 1
666
+ existing["last_used"] = timestamp
667
+ existing["source_blocks"].append(block_id)
668
+ else:
669
+ # New pattern
670
+ new_id = next_pattern_id(learnings, "SP")
671
+ learnings["success_patterns"].append({
672
+ "id": new_id,
673
+ "pattern": pattern,
674
+ "evidence_count": 1,
675
+ "first_observed": timestamp,
676
+ "last_used": timestamp,
677
+ "source_blocks": [block_id],
678
+ "tags": extract_tags(pattern)
679
+ })
680
+
681
+ # Extract failure patterns (from gotchas)
682
+ for gotcha in lessons.get("gotchas", []):
683
+ existing = find_similar_pattern(learnings["failure_patterns"], gotcha)
684
+ if existing:
685
+ existing["evidence_count"] += 1
686
+ existing["last_hit"] = timestamp
687
+ existing["source_blocks"].append(block_id)
688
+ else:
689
+ new_id = next_pattern_id(learnings, "FP")
690
+ learnings["failure_patterns"].append({
691
+ "id": new_id,
692
+ "pattern": gotcha,
693
+ "evidence_count": 1,
694
+ "first_observed": timestamp,
695
+ "last_hit": timestamp,
696
+ "source_blocks": [block_id],
697
+ "tags": extract_tags(gotcha)
698
+ })
699
+
700
+ # Extract user preferences
701
+ for pref in lessons.get("user_preferences", []):
702
+ existing = find_similar_pattern(learnings["user_preferences"], pref)
703
+ if existing:
704
+ existing["evidence_count"] += 1
705
+ else:
706
+ new_id = next_pattern_id(learnings, "UP")
707
+ learnings["user_preferences"].append({
708
+ "id": new_id,
709
+ "preference": pref,
710
+ "evidence_type": "inferred",
711
+ "evidence_count": 1,
712
+ "first_observed": timestamp,
713
+ "tags": extract_tags(pref)
714
+ })
715
+
716
+ # Extract architectural decisions (from almost_did)
717
+ for decision in lessons.get("almost_did", []):
718
+ # almost_did format: "Considered X, chose Y because Z"
719
+ parsed = parse_decision(decision)
720
+ new_id = next_pattern_id(learnings, "AD")
721
+ learnings["architectural_decisions"].append({
722
+ "id": new_id,
723
+ "decision": parsed.chosen,
724
+ "context": parsed.context,
725
+ "alternatives": parsed.alternatives,
726
+ "rationale": parsed.rationale,
727
+ "decided": timestamp,
728
+ "source_block": block_id,
729
+ "tags": extract_tags(decision)
730
+ })
731
+
732
+ # Update metadata
733
+ learnings["last_updated"] = timestamp
734
+ learnings["total_entries"] = count_all_entries(learnings)
735
+ learnings["extraction"]["last_extracted_from"] = summary_path
736
+ learnings["extraction"]["extraction_count"] += 1
737
+
738
+ # Write back
739
+ write_learnings_file(".grid/LEARNINGS.md", learnings)
740
+
741
+ return {
742
+ "extracted": True,
743
+ "from": summary_path,
744
+ "new_patterns": count_new,
745
+ "updated_patterns": count_updated
746
+ }
747
+ ```
748
+
749
+ ### Pattern Similarity Detection
750
+
751
+ ```python
752
+ def find_similar_pattern(patterns: list, new_pattern: str, threshold: float = 0.7) -> dict | None:
753
+ """
754
+ Find existing pattern similar to new one to avoid duplicates.
755
+ Uses keyword overlap for similarity.
756
+ """
757
+ new_keywords = set(extract_keywords(new_pattern))
758
+
759
+ for pattern in patterns:
760
+ existing_keywords = set(pattern.get("tags", []) + extract_keywords(pattern.get("pattern", "")))
761
+ overlap = len(new_keywords & existing_keywords) / max(len(new_keywords | existing_keywords), 1)
762
+
763
+ if overlap >= threshold:
764
+ return pattern
765
+
766
+ return None
767
+ ```
768
+
769
+ ### Tag Extraction
770
+
771
+ ```python
772
+ def extract_tags(text: str) -> list[str]:
773
+ """
774
+ Extract meaningful tags from pattern text.
775
+ """
776
+ # Common tech/concept keywords to look for
777
+ tech_keywords = [
778
+ "api", "auth", "database", "prisma", "jwt", "session",
779
+ "middleware", "validation", "error", "cache", "async",
780
+ "typescript", "react", "next", "node", "express",
781
+ "test", "mock", "env", "config", "deploy", "docker"
782
+ ]
783
+
784
+ text_lower = text.lower()
785
+ tags = []
786
+
787
+ for keyword in tech_keywords:
788
+ if keyword in text_lower:
789
+ tags.append(keyword)
790
+
791
+ # Also extract CamelCase and snake_case identifiers
792
+ import re
793
+ identifiers = re.findall(r'[A-Z][a-z]+(?:[A-Z][a-z]+)*|[a-z]+_[a-z]+', text)
794
+ for ident in identifiers:
795
+ normalized = ident.lower().replace('_', '')
796
+ if len(normalized) > 3 and normalized not in tags:
797
+ tags.append(normalized)
798
+
799
+ return tags[:10] # Max 10 tags per pattern
800
+ ```
801
+
802
+ ### Manual Learning Entry
803
+
804
+ Sometimes patterns should be added manually (user correction, explicit teaching):
805
+
806
+ ```python
807
+ def add_manual_learning(category: str, content: dict):
808
+ """
809
+ Add a learning entry manually.
810
+ category: success_patterns | failure_patterns | codebase_patterns |
811
+ user_preferences | architectural_decisions | tech_context
812
+ """
813
+ learnings = read_learnings_file(".grid/LEARNINGS.md")
814
+ prefix_map = {
815
+ "success_patterns": "SP",
816
+ "failure_patterns": "FP",
817
+ "codebase_patterns": "CP",
818
+ "user_preferences": "UP",
819
+ "architectural_decisions": "AD",
820
+ "tech_context": "TC"
821
+ }
822
+
823
+ new_id = next_pattern_id(learnings, prefix_map[category])
824
+ content["id"] = new_id
825
+ content["first_observed"] = now()
826
+ content["evidence_count"] = content.get("evidence_count", 1)
827
+ content["source"] = "manual"
828
+
829
+ learnings[category].append(content)
830
+ learnings["total_entries"] += 1
831
+ learnings["last_updated"] = now()
832
+
833
+ write_learnings_file(".grid/LEARNINGS.md", learnings)
834
+ ```
835
+
836
+ ### Extraction Completion Message
837
+
838
+ ```markdown
839
+ ## LEARNINGS EXTRACTED
840
+
841
+ **Source:** {summary_path}
842
+ **Block:** {block_id}
843
+
844
+ **New Entries:**
845
+ - Success Patterns: {N}
846
+ - Failure Patterns: {N}
847
+ - User Preferences: {N}
848
+ - Decisions: {N}
849
+
850
+ **Updated Entries:**
851
+ - {N} patterns with increased evidence
852
+
853
+ **Total Learnings:** {total_entries}
854
+
855
+ End of Line.
856
+ ```
857
+
858
+ ---
859
+
634
860
  ## RULES
635
861
 
636
862
  1. **Index continuously** - Don't wait for phase completion
@@ -643,6 +869,8 @@ End of Line.
643
869
  8. **Preserve decisions** - Architectural choices matter most long-term
644
870
  9. **Cross-reference** - Link related memories (pattern → gotcha → decision)
645
871
  10. **Never block execution** - You augment, not gate
872
+ 11. **Extract learnings** - After every block completion, extract to LEARNINGS.md
873
+ 12. **Evidence matters** - Patterns with more evidence get higher priority
646
874
 
647
875
  ---
648
876