A standing canon of methodology principles validated through Loomworks development work. Each principle here has been earned — surfaced during real execution, validated across multiple instances, and named as a stable pattern worth carrying forward.
The canon is not exhaustive. It captures principles that have been validated through evidence, not aspirational discipline. Principles that don't yet meet the evidence threshold remain methodology candidates in development assessment notes or implementation notes; they're lifted to the canon only when execution evidence supports stability.
This v0.1 captures the four principles consolidated from the substrate hygiene cleanup arc (Phases 61, 62, 63). Future consolidation passes may extend the canon as additional principles earn their place.
Principles are stable. Once a principle is in the canon, it stays unless a substantive new principle replaces it or evidence accumulates that contradicts it. New versions of the canon (v0.2, v0.3) absorb additions, refinements, or — rarely — corrections.
Principles must be actionable. A principle that doesn't shape concrete decisions during work doesn't earn a canon entry. Each principle here is paired with practical guidance — what to do, what not to do, when it applies.
Principles must be evidenced. Each entry cites the specific instances that validated it. A principle without evidence is a candidate, not canon.
Principles are not prescriptive everywhere. Each entry includes boundary conditions — when the principle applies, when it doesn't, what edge cases it doesn't cover. Cargo-culting a principle outside its boundary conditions is its own anti-pattern.
When a code path is being prepared for future external integration, the reserved-location marker lives at each business-event site, not centralized in helper writers.
When Loomworks code writes a record that will eventually need to emit an external-system notification (FORAY anchoring, OVA authorization check, future integrations), the emit happens at the call site that represents the business event — not in a centralized helper function that all writes flow through.
A business event might correspond to one code site, three code sites, or twelve code sites. The number doesn't matter; the principle is that each business event gets its own emit. Two business events sharing a code path through a centralized helper would emit twice from the same site, which is wrong; two business events at distinct call sites get distinct emits, which is right.
append_event is the single write path), 1 reserved-emit call. Design Decision 4 confirmed the discipline scales both directions — fewer sites still get the per-call-site pattern.flows.py (11) and proposal_applier.py (1), 12 reserved-emit calls. Validates the per-call-site discipline at scale (the largest sibling CR's surface).Centralizing the reserved emit in a helper writer because it's "DRY" or "simpler." This loses the business-event-vs-persistence distinction. A future engineer reading the helper sees one emit; the helper's three callers each represent different business events that should — when the external integration opens — emit differently. Centralization papers over the distinction.
When a test's filename framing references a cleaned-up origin, the filename should be updated to reflect what the test actually tests today — or the file deleted entirely if the underlying functionality is also gone.
Test filenames often carry phase-number prefixes, feature names, or origin context from when they were written. After substrate cleanup or refactoring, the filename may reference work that no longer exists in the codebase. This creates ambient confusion: a reader looking for tests that verify current behavior sees filenames that name what used to exist.
Two responses, depending on what changed:
Rename when the logic the test verifies is still present but the framing is stale. Example: test_phase_25_commit_attestation.py becomes test_commit_webauthn_attestation.py when Phase 25's wiring is cleaned up but commit attestation as a Memory concern survives (renamed).
Delete when both the framing AND the underlying functionality are gone. Example: test_phase_25_content_hash_and_foray.py deleted when content_hash is dropped and the FORAY namespace injection is removed.
The principle isn't about phase numbers specifically — it's about test filenames as documentation. Test files are read; their names participate in what the codebase communicates. Stale names corrupt that signal.
test_voice_tune_setting_foray_audit.py → test_voice_tune_setting_audit.py (the FORAY framing was removed from audit substrate; the file still tests audit-side voice-tune behavior).test_phase_25_commit_attestation.py → test_commit_webauthn_attestation.py (logic preserved with new column name)test_phase_25_content_hash_and_foray.py (all three tests covered removed functionality)test_foray_namespace_injected in test_phase_53, test_phase_54, test_phase_31) testing the removed namespace injectiontest_phase_47_credit_substrate.py, test_phase_48_*, test_phase_49_*, test_phase_50_*) were named after their phase-origin rather than after the FORAY framing being removed. The principle correctly predicted “no renames needed here” because the filenames still accurately describe what the tests cover today. This validates the principle in its negative form: leave alone when the existing name still describes what the file tests. A principle that predicts both positive actions (rename / delete) and negative actions (leave alone) is more useful than one that predicts only positive — the four-instance evidence base now covers both directions.Leaving test files with stale framing because "tests still pass, so the filename doesn't matter." Filenames are part of the codebase's communication. Stale filenames make new readers spend cognitive effort figuring out what a file actually tests. The cost compounds as the codebase ages.
Choosing between in-place rename and drop-and-recreate for a table or column migration depends on what downstream objects bind to it. Bindings to the underlying identity (table OID, column position) follow a rename transparently; objects independent of the renamed thing can be cleanly recreated.
When cleaning up a database object — table, column, index, function — two migration strategies exist:
Rename (in-place) — keep the underlying object identity; change its name. Downstream bindings (triggers on tables, FKs referencing tables, indexes on columns) follow the rename automatically because they bind to the underlying identity, not the surface name.
Recreate (drop-and-add) — drop the object; create a new one with the new shape. Downstream bindings need to be re-established explicitly. Data needs to be migrated.
The choice is determined by what binds to the object:
When all downstream bindings will follow a rename, rename is preferred (no data motion, no constraint re-establishment, no time window where the new shape exists without its dependent objects).
When downstream bindings are independent (no triggers, no FKs, clean integration boundary), recreate is acceptable — the data migration is the only cost.
audit.foray_events had no triggers, no FKs from other tables, no downstream readers. Strategy worked cleanly.attestation → commit_webauthn_attestation) via op.alter_column(... new_column_name=...). The one existing non-null row (event_id 0673cfd7-354b-4e2a-8a33-9d7355ebf180) survived with data intact. Indexes followed the rename transparently.credit.update_balance_on_flow references column names CR-C does not change (NEW.id, NEW.to_party, NEW.from_party, NEW.asset_id, NEW.quantity) and binds to the table OID. The intra-credit FK also follows the OID. No DDL touches the trigger during migration; it continues firing on inserts to the renamed table. CC's design surfaced this insight during grep work — what was anticipated as the hardest part of CR-C turned out to be a non-question.Defaulting to drop-and-recreate "because it's cleaner" when rename would preserve downstream bindings transparently. Recreate has costs: data migration, constraint re-establishment, brief windows where the new table exists without its dependents. When rename works, it's strictly less risky. The decision to recreate should be justified by what requires recreating (e.g., schema changes that don't fit alter_column), not by default preference.
Inventories enumerate references but undercount cognitive density per reference. Design steps should grep for assertion patterns and reader sites directly, not inherit inventory counts; per-test-function enumeration is more reliable than per-file enumeration.
When investigating a code change's surface area, two questions sound similar but produce different answers:
An inventory that produces the first count helps locate the work; it doesn't predict the work's cost. Inventories tend to undercount cognitive density per reference because:
ForayActionFlow, foray_action_flows). Test helpers and some production paths use raw SQL (INSERT INTO credit.foray_action_flows ...) that grep on ORM names misses.The corrective practice: during design (not just inventory), grep for the patterns that matter for the change — assertion patterns (payload["_foray"], references to dropped column names, references to removed helper functions), reader query patterns (JSONB-extract sites), raw-SQL INSERT/UPDATE/DELETE on the affected tables. Enumerate per-test-function and per-call-site, not per-file.
test_foray_namespace_injected in three phase test files, plus an assertion block inside another)_ANCHOR_PRIORITY needing updates (rather than relying on a backward-compatible re-export)compute_content_hash import removals (only 1 anticipated; two others lived inside the deleted test_foray_namespace_injected functions)test_phase_48_reconciliation_evaluator.py:128 writing the dropped content_hash column — failed all 6 reconciliation-evaluator tests until updatedInheriting an inventory's file count as the work estimate. Inventories produce the locator information; designs produce the work estimate. Skipping the design-time grep because "the inventory already covered it" causes reserved-slot consumption from execution-time discovery of references the inventory didn't surface. The cost of design-time grep is small; the cost of execution-time discovery is reserved-slot consumption or worse.
The four principles consolidated here aren't independent; they share a structure worth naming.
Each principle corrects a tendency toward false simplicity:
Each principle also names what appears simpler initially (centralization, leaving names alone, drop-and-recreate, inventory inheritance) and what actually requires more care (per-call-site, alignment work, rename evaluation, grep-at-design-time). The pattern is: surface-simplicity has a hidden cost; the principles name what's worth paying attention to.
This is v0.1. Future versions absorb:
The canon is meant to be useful, not encyclopedic. A principle that doesn't shape concrete decisions during work doesn't belong here; a principle that shapes work but isn't documented elsewhere does belong here. Future passes should keep this calibration in mind.