LisaOS Docs
Governance

Code Discipline Protocol

Last Modified: NaN


Code Discipline Protocol

Addendum to Phase 1 Architecture

This protocol applies across all modules as a mandatory methodological overlay. It governs how every line of code is written, generated, reviewed, and accepted throughout the curriculum.


Design Principle

TypeScript is not "JavaScript with optional types." It is a statically typed language that compiles to JavaScript. We treat it as such. Every exercise, every Claude Code generation, every Cursor session operates under TypeScript strict mode — the compiler is your first code reviewer, and it must pass before you even begin your human review.

This means M3 (JS/TS Fundamentals) is restructured: you learn JavaScript's runtime behaviour through TypeScript's type lens, not the other way around. You understand let x = 5 by understanding that TypeScript infers x: number — the dynamic behaviour is the output, not the mental model.


The Five Pillars

1. Strict Typing (Type Hinting → Full Type System)

What this means in practice:

  • tsconfig.json is configured with "strict": true from the first exercise in M3. This enables all strict checks simultaneously: noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, alwaysStrict.
  • Every function has explicit parameter types and return types. No inferred return types on exported functions — ever.
  • any is treated as a code smell. When Claude Code or Cursor generates any, your review must flag and replace it with a proper type, unknown (which forces safe narrowing), or a generic.
  • Union types and discriminated unions replace loose string | number patterns with explicit, exhaustive alternatives.
  • In Python (used in M12 for ML): type hints are mandatory on all function signatures, enforced by mypy --strict.

Review checklist item: "Could the compiler catch a bug here that a human might miss? If the types are loose enough that the answer is no, the types are wrong."

// ❌ What Claude Code might generate
function processMessage(data: any) {
  return data.content.trim();
}
 
// ✅ What your review transforms it into
interface InboundMessage {
  readonly id: string;
  readonly content: string;
  readonly timestamp: Date;
  readonly channel: 'whatsapp' | 'telegram' | 'discord';
}
 
function processMessage(message: InboundMessage): string {
  return message.content.trim();
}

2. Static Analysis Toolchain

The toolchain is non-negotiable and configured before the first line of code in M3:

ToolPurposeWhen It Runs
TypeScript compiler (tsc --strict)Type checkingEvery save (IDE integration) + pre-commit
ESLint (with @typescript-eslint)Code quality, convention enforcementEvery save + pre-commit
PrettierFormatting consistencyEvery save (format-on-save) + pre-commit
Husky + lint-stagedPre-commit gateEvery git commit (M2 onward)
mypy (--strict)Python type checking (M12 only)Every save + pre-commit
RuffPython linting (M12 only)Every save + pre-commit

ESLint rules enforced from day one (not gradually introduced):

// .eslintrc.json (key rules — full config provided in M3)
{
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/explicit-function-return-type": "error",
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/strict-boolean-expressions": "error",
    "@typescript-eslint/no-floating-promises": "error",
    "@typescript-eslint/no-unsafe-assignment": "error",
    "@typescript-eslint/no-unsafe-member-access": "error",
    "@typescript-eslint/no-unsafe-call": "error",
    "eqeqeq": ["error", "always"],
    "no-var": "error",
    "prefer-const": "error"
  }
}

Review workflow integration: When Claude Code or Cursor generates code, you run the full toolchain before beginning your manual review. If the toolchain passes, you review for logic, architecture, and security. If it fails, you fix the toolchain errors first — they represent the class of bugs you should never need to find manually.


3. Defensive Programming

Core principle: Code must handle the world as it is, not as it should be. Every function boundary is a trust boundary.

Practices enforced throughout:

  • Input validation at system boundaries: All external data (API responses, user input, file reads, environment variables) is validated with a runtime schema library before entering your type system. We use Zod for TypeScript — it bridges runtime validation and compile-time types:
import { z } from 'zod';
 
// Schema defines both runtime validation AND compile-time type
const MessageSchema = z.object({
  id: z.string().uuid(),
  content: z.string().min(1).max(10_000),
  timestamp: z.coerce.date(),
  channel: z.enum(['whatsapp', 'telegram', 'discord']),
});
 
// Type is derived from schema — single source of truth
type Message = z.infer<typeof MessageSchema>;
 
function handleInbound(raw: unknown): Message {
  // Throws ZodError with detailed path info if validation fails
  return MessageSchema.parse(raw);
}
  • Exhaustive switch statements: TypeScript's never type enforces that all union members are handled. If a new channel is added and you miss a case, the compiler catches it:
function routeMessage(channel: Message['channel']): string {
  switch (channel) {
    case 'whatsapp': return 'wa-handler';
    case 'telegram': return 'tg-handler';
    case 'discord':  return 'dc-handler';
    default: {
      const _exhaustive: never = channel;
      throw new Error(`Unhandled channel: ${String(_exhaustive)}`);
    }
  }
}
  • Explicit error handling: No bare try/catch that swallows errors. Errors are either typed (custom error classes), propagated with context, or handled with explicit recovery logic.
  • Null safety: strictNullChecks means the compiler forces you to handle null and undefined. Combined with optional chaining (?.) and nullish coalescing (??), this eliminates an entire class of runtime errors.
  • Assertions for invariants: Where the type system can't express a runtime guarantee, use assertion functions:
function assertDefined<T>(
  value: T | null | undefined,
  name: string
): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error(`Expected ${name} to be defined`);
  }
}

Review checklist item: "What happens when this function receives unexpected input? If the answer is 'undefined behaviour' or 'silent failure,' the code is incomplete."


4. Naming Conventions

Naming is type information for humans. The convention system below encodes semantic meaning into identifiers so that code reads as close to self-documenting prose as possible.

CategoryConventionExampleRationale
InterfacesPascalCase, noun/adjectiveSessionConfig, SendableDescribes shape or capability
Type aliasesPascalCaseChannelType, MessageIdDistinguishable from runtime values
EnumsPascalCase (enum), PascalCase (members)Channel.WhatsAppReads as a qualified constant
ClassesPascalCase, nounGatewayRouterDescribes the entity
Functions / methodscamelCase, verb-firstparseMessage(), isValid()Describes the action
Boolean variablescamelCase, is/has/should/can prefixisConnected, hasMemoryReads as a predicate
ConstantsUPPER_SNAKE_CASEMAX_RETRY_COUNTVisually distinct as immutable
Private memberscamelCase (no underscore prefix)private sessionCacheTypeScript's private keyword is the access control; _ prefix is redundant
Generic type paramsDescriptive single-word or T prefixTMessage, TConfig or Input, OutputReadable generics over opaque T, U, V
File nameskebab-case.tsgateway-router.tsFilesystem-safe, consistent
Test files*.test.tsgateway-router.test.tsCo-located, easily grep'd

Additional conventions:

  • No abbreviations unless universally understood (id, url, api). Write message, not msg. Write configuration, not cfg. Code is read 10x more than it is written.
  • Collection names are plural: sessions, messages, channels. Singular for individual items: session, message, channel.
  • Callback/handler naming: onMessageReceived, handleSessionExpiry — prefix communicates the trigger pattern.

Review checklist item: "Can I understand what this variable holds and what this function does without reading its implementation? If not, the name is wrong."


5. Interfaces and Abstract Contracts

Core principle: Program to interfaces, not implementations. Define the contract before writing the code.

Practices enforced throughout:

  • Interface-first design: Before Claude Code generates an implementation, you define the interface it must satisfy. This is your specification — the AI generates to the contract:
// You write this first (the contract)
interface MemoryStore {
  readonly provider: string;
  
  store(key: string, value: Readonly<SessionData>): Promise<void>;
  retrieve(key: string): Promise<SessionData | null>;
  search(query: string, limit: number): Promise<readonly SessionData[]>;
  prune(olderThan: Date): Promise<number>;
}
 
// Then Claude Code generates the implementation
// Your review verifies it satisfies the contract
class SqliteMemoryStore implements MemoryStore {
  readonly provider = 'sqlite';
  // ...
}
  • Abstract base classes for shared behaviour: When multiple implementations share logic, use abstract classes with template methods:
abstract class BaseChannel {
  abstract readonly channelName: string;
  abstract connect(): Promise<void>;
  abstract disconnect(): Promise<void>;
  abstract sendRaw(payload: ChannelPayload): Promise<void>;
  
  // Shared behaviour with defensive checks
  async send(message: OutboundMessage): Promise<void> {
    assertDefined(message.content, 'message.content');
    const payload = this.formatPayload(message);
    await this.sendRaw(payload);
  }
  
  protected abstract formatPayload(message: OutboundMessage): ChannelPayload;
}
  • Dependency inversion: Functions and classes accept interfaces as parameters, not concrete implementations. This makes code testable, replaceable, and reviewable:
// ✅ Accepts the interface — testable, swappable
function createAgent(memory: MemoryStore, channel: BaseChannel): Agent { ... }
 
// ❌ Coupled to a specific implementation
function createAgent(memory: SqliteMemoryStore, channel: WhatsAppChannel): Agent { ... }
  • Readonly by default: Use readonly on interface properties and Readonly<T> / ReadonlyArray<T> for parameters that shouldn't be mutated. Mutation is opt-in, not the default.

Review checklist item: "Is this component coupled to a specific implementation? Could I swap the dependency without changing this code? If not, an interface is missing."


Unified Review Protocol

Every code review (whether of AI-generated code or your own) follows this checklist, in order:

Gate 1 — Toolchain (Automated)

  • TypeScript compiles with zero errors under strict: true
  • ESLint passes with zero warnings (warnings are treated as errors)
  • Prettier formatting applied
  • All tests pass

If Gate 1 fails, do not proceed to Gate 2. Fix toolchain issues first.

Gate 2 — Type Discipline (Manual)

  • No any types (or each instance has a documented justification)
  • All function signatures have explicit parameter and return types
  • External data is validated at the boundary (Zod schemas)
  • Null/undefined cases are handled explicitly
  • Exhaustive pattern matching on unions and enums

Gate 3 — Design Discipline (Manual)

  • Names are self-documenting (follow naming conventions)
  • Functions depend on interfaces, not concrete implementations
  • Mutation is explicit and minimised (readonly by default)
  • Error handling is explicit (no swallowed errors)
  • Single Responsibility: each function/class does one thing

Gate 4 — Logic and Architecture (Manual)

  • Logical correctness (does it do what it should?)
  • Performance (appropriate data structures, no unnecessary work)
  • Security (input validation, no secrets in code, principle of least privilege)
  • Architectural fit (does this belong here? right abstraction level?)

Operational Instantiation: Clean Code Pipeline

The Four-Gate Unified Review Protocol above is the doctrine; the Clean Code Pipeline TechSpec (CS.AK.LISA.TechSpec.CleanCodePipeline) is its operational instantiation in the Cipher Shinobi repo topology. The pipeline maps each manual gate to a tool-enforced layer:

  • Gate 1 — Toolchain (Automated) is enforced at write-time by the Semgrep MCP hook (TechSpec §06 Gate 1) — SAST + SCA + secrets, blocking pre-tool. The TS compiler / ESLint / Prettier portion runs at IDE save and at the pre-push gate.
  • Gate 2 — Type Discipline and Gate 3 — Design Discipline run at pre-push as Gate 2a (Qodo Command CLI for test coverage + generation) and Gate 2b (CodeRabbit CLI for logic / style / architecture). These match the manual Type and Design gates above with tool-assisted coverage.
  • Gate 4 — Logic and Architecture runs server-side at the GitHub Actions tier (TechSpec §06 Gate 3) plus CODEOWNERS approval from @the OS core repo (LISA's 2FA'd identity, ratified ADR-3). LISA reviews under her own identity and posts review comments in voice.

Genji and Raiden execute under this pipeline. Genji implements; Raiden verifies. The pipeline's custom Semgrep rules (authored by Gray Fox in Sprint 0) cite this Code Discipline Protocol directly — when this protocol changes, the rules' cite_version SHA256 hashes must be re-validated at Gate 3 to prevent drift (TechSpec §09 Governance Integration + OQ-3 resolution).

For full architecture, repo topology, ADRs, risk register, and migration order, read the TechSpec.


Implications for Curriculum Structure

This protocol does not change the module structure or hour allocations. It changes the methodology within every module:

ChangeDetail
M3 reframingTypeScript-first, not "JS then TS." JavaScript runtime behaviour is taught through the lens of TypeScript's type system. strict: true from exercise one.
Toolchain setup in M2/M3The static analysis toolchain (ESLint, Prettier, Husky) is configured as part of initialising the LISA Git repo. It becomes infrastructure you maintain, not something bolted on later.
Zod introduced in M3Runtime validation is taught alongside types as the bridge between TypeScript's compile-time guarantees and runtime reality.
Interface-first exercisesIn every module from M3 onward, the generate → review → correct workflow begins with you writing the interface/contract, then having Claude Code implement it. Your review verifies conformance.
Review protocol in every moduleThe four-gate checklist above is applied to every exercise. Modules progressively add items to Gates 3 and 4 as your knowledge expands.
M12 (ML) applies the same rigour in Pythonmypy --strict, Ruff, type hints on all signatures, dataclasses with frozen=True as the Python equivalent of readonly.

This protocol is a living document. As modules are expanded in Phase 2, specific exercises and examples will demonstrate each pillar in the context of that module's content.