← Back to /admin

2026-09-18-pillar-convergence-plan.md

audits/2026-09-18-pillar-convergence-plan.md · 9,606 bytes

Pillar Convergence Plan — craig-converge-pillar-scorers-kw8

Authored by Plan-mode subagent, 2026-09-18, from read-only inspection of scripts/, blog/metadata.json, substack/metadata.json, wisdom/insights-cleaned.json.

Findings

There are FIVE pillar-scoring implementations, not three

# File Pillars Style Multi-tag S15 gate Overrides Callers/output
1 scripts/blog-categorise.py 10 regex, word boundaries 1–3 (40% thresh, min 3.0) yes MANUAL_TAG_ADD/REMOVE blog/metadata.json (795 posts), blog/by-topic/*.md
2 scripts/substack-categorise.py 10 regex, word boundaries 1–3 (60% thresh, min 3.0) no none substack/metadata.json (324 posts), substack/by-topic/*.md
3 scripts/wisdom-extract-v2.py::determine_pillar 8 (missing mathematics, enterprise) plain in lower substring 1 (argmax) no none wisdom/insights.json (paragraph-level pillar)
4 scripts/wisdom-extract.py::determine_pillar (v1) 8 substring 1 no none stale — v1 kept beside v2
5 scripts/substack-wisdom-extract.py::{determine_pillar, determine_pillar_for_post} 8 substring 1 (paragraph, with post-level fallback if <2 kw hits) no none wisdom/substack-insights.json

Plus scripts/wisdom-cleanup.py::reassign_pillar (uses TOPIC_TO_PILLAR and reads blog.topics[0]), which produced wisdom/insights-cleaned.json — this is why that file carries enterprise (60) and no mathematics even though v2's PILLARS omits both.

Divergence details vs. blog-categorise.py (post-S15)

substack-categorise.py

wisdom-extract-v2.py::determine_pillar

substack-wisdom-extract.py

The canonical pillar list

scripts/audit-sample.py (lines 34–36) is the ground truth: bitcoin-protocol, identity-history, law-governance, economics, security-forensics, philosophy, mathematics, enterprise, computation, information-theory. Wisdom scripts silently dropped two.

Proposed shared module

File: scripts/pillars.py.

Public surface


PILLAR_IDS = [                       # canonical order — matches audit-sample.PILLARS
    "bitcoin-protocol", "identity-history", "law-governance", "economics",
    "security-forensics", "philosophy", "mathematics", "enterprise",
    "computation", "information-theory",
]

PILLARS = { ... }                    # merged catalogue: blog's regex + S15 gate
                                     #   + useful additions absorbed from
                                     #   substack-wisdom (philosophy: beauty/
                                     #   aesthetics/virtue/hubris; law:
                                     #   bailment/custody/lien; economics:
                                     #   trade/tariff/settlement)

MANUAL_TAG_ADD = { ... }             # from blog-categorise.py
MANUAL_TAG_REMOVE = { ... }          # from blog-categorise.py

def score_text(text, *, title="", category=None, word_count=None,
               apply_structural_gate=True, density_sample_words=None) -> dict:
    """dict[pillar_id] -> (score: float, matches: list[str]). Only positive."""

def top_pillar(text, *, title="", **kw) -> str:
    """Single-argmax convenience for wisdom paragraph case. Fallback: 'bitcoin-protocol'."""

def tag_post(text, *, title, category=None, word_count=0, subtitle=None,
             secondary_threshold=0.40, min_absolute=3.0,
             max_tags=3, min_tags=1,
             fallback_pillar="identity-history",
             category_fallback_map=None,
             short_post_word_limit=100) -> list[str]:
    """Multi-tag convenience for blog and substack."""

def apply_manual_overrides(slug, tags) -> list[str]:
    """Idempotent MANUAL_TAG_ADD/REMOVE from the merged tables."""

Key design choices:

Migration deltas per caller (5 lines each, mechanical)

See agent output for exact deltas — omitted here for brevity. Every caller reduces to from pillars import ... + a thin shim that preserves its distinctive input/output shape.

Frozen 60-post hand-labelled eval set

Path: evals/pillar-gold-60.json (new evals/ directory).

Risks (with numbers)

Caller Corpus Reclassification pressure Mitigation
blog-categorise.py 795 posts Near-zero — shared module IS blog's current logic. ≤2 shift expected. Diff old vs new metadata; block ≥5 shifts.
substack-categorise.py 324 posts High. S15 gate + regex changes will move dozens. ~30–50 substack posts change tags. 57 currently ["philosophy"]-only reroute. Diff old vs new; hand-review; 20 substack cases in eval set.
wisdom-extract-v2.py 9,808 insights (10,770 in bead) Very high. 48% of bitcoin-protocol insights lack a strong bitcoin term. Adding mathematics/enterprise back re-homes many. Shadow file insights.v3.json; diff by id and by source slug; hand-review top-50 diffs.
wisdom-cleanup.py Same Moderate. Optional relaxation of TOPIC_TO_PILLAR["mathematics"] = "computation" collapse. Explicit policy decision.
substack-wisdom-extract.py wisdom/substack-insights.json (17 MB) High (same reason). Shadow-file diff pattern.

Secondary risks: downstream consumers (topics/.md, blog/by-topic/.md, wisdom/tier-*.md, study-guide.md) all regenerate — verify no external URLs depend on churn-free labels.

Recommended order

1. Extract scripts/pillars.py + scripts/tests/test_pillars.py (golden loader, S15 regression cases, MANUAL_TAG_* round-trip). No caller changes yet.

2. Freeze eval: produce evals/pillar-gold-60.json (sampling + hand-review by Adam). Gate — nothing else lands until agreed.

3. Migrate blog (lowest risk): swap; require ≤5 shifts.

4. Migrate wisdom-extract-v2 in shadow mode: shadow file, hand-review top-50, promote.

5. Migrate substack-categorise: diff, require eval pass, publish audit note.

6. Migrate substack-wisdom-extract, wisdom-extract v1, fix-info-theory-tags, wisdom-cleanup, audit-sample: mechanical single PR.

7. Retire dead code: v1 → sys.exit("use v2"). Pre-commit greps for PILLARS = { outside pillars.py.

8. Regenerate all derived views in ONE commit.


Report path: /home/a/Projects/craig/audits/2026-09-18-pillar-convergence-plan.md (this file). Original agent transcript at /tmp/claude-1001/.../tasks/aec7ad0996f0dc915.output (session-only).