| A lesson only moves forward, and the database is what says soC111 | status_rank is a persisted computed column (observed 0, documented 1, enforced 2, archived 3) and tg_build_lessons_status_monotonic rolls back any UPDATE that lowers it; archived is absorbing, and no surface offers a way back. The service refuses first, so a caller gets a conflict rather than a broken transaction | shipped |
| Identity and slug are fixed at insertC111 | the same trigger rolls back any UPDATE touching workspace_id, lesson_id or lesson_slug — which is what makes the rank comparison unevadable, since a row cannot rewrite its key and slip the join. The project can still be corrected | shipped |
| Nothing reaches enforced until the prevention mechanism is writtenC112the gate is structural, the human in it is convention: any credential holding write_session_state can enforce a lesson. What cannot be skipped is writing the mechanism down | two CHECK constraints, beneath the service: CK_build_lessons_status_requires_pattern refuses documented or enforced with no pattern write-up, CK_build_lessons_enforced_requires_prevention refuses enforced with no prevention mechanism. A lesson can be filed only at observed or documented — enforced is reachable only by advancing it | shipped with caveat |
| Observations are append-only by constructionC113by construction, not by trigger — say it that way: deleting the lesson does delete its observations, in the same transaction | build_lesson_observations carries no trigger and needs none — the only writes are the insert behind append_lesson_observation and the cascade the hard delete performs itself. Each records a sighting and its provenance, and bumps the lesson’s last_observed_at | shipped with caveat |
| Every bound is checked twiceC114the two markdown caps are written as byte bounds rather than character counts, so a value padded with trailing spaces cannot slip past a length check that ignores them | a slug of 1–100 characters unique to the project, a title of 1–300, a pattern write-up up to 32,000 characters, a prevention mechanism up to 8,000, an observation of 1–4,000, and up to 32 applicability tags from a controlled vocabulary of 12 — each a service check and a database constraint | shipped |
| A candidate cannot be typed inC115 | there is no create tool and no create route on any transport; a candidate exists only because a detection pass filed it, which is what keeps signal_source a fact rather than a claim | shipped |
| A candidate is decided once, and the decision is finalC115 | promoted, rejected or superseded — three peers, not a ladder, which is why absorption is a trigger rather than a rank: tg_lesson_candidates_terminal_absorbing rolls back any UPDATE that changes status out of a terminal state, and any UPDATE touching the identity columns | shipped |
| Promotion lands at documented, never at enforcedC115 | promote_lesson_candidate creates the lesson one rung short of the gate, carries every observation across as its evidence and back-links both rows — lesson, audit row and link in one transaction. So accepting a proposal can never skip the prevention mechanism | shipped |
| A decision has to say somethingC115 | reject needs a stated reason; supersede refuses a rejected successor and refuses a cycle; and a candidate that others were superseded into cannot be rejected, because that would discard their evidence | shipped |
| Detection is optional and off by defaultC116nothing is proposed unless someone turns it on, and nothing becomes a lesson until a person promotes it — the model never promotes | the detection sweep ships disabled on a daily cadence, the deployment template’s switch defaults to off, and the LLM client is inert until you bring your own Anthropic key — with no provider, detect_lesson_candidates answers deferred and files nothing. The one detector that ships clusters a project’s resolved backlog items | default-off |
| A rule derives only from an enforced lessonC12mis-filed lessons and rules can be hard-deleted by an authorised credential; archiving is the retire-without-delete path | derive_rule reads the lesson under an update lock inside the same transaction that inserts the rule and refuses one that is not enforced; the prevention mechanism becomes the rule’s body and the applicability tags seed its project_attributes | shipped |
| 9 events on the audit chainC19 | build_lesson.filed, build_lesson.status_changed, build_lesson.observation_appended, build_lesson.deleted; lesson_candidate.detected, lesson_candidate.observation_appended, lesson_candidate.promoted, lesson_candidate.rejected, lesson_candidate.superseded — each written into the workspace’s tamper-evident SHA-256 chain in the same transaction as the row it records; a same-rank advance writes none | shipped |
| Writes need one scope; reads need only authenticationC117 | the nine writes require write_session_state over REST and MCP alike; the five reads — the lesson, the search, the queue, a candidate, the counts — require an authenticated caller in the workspace, and the workspace always comes from the credential | shipped |
| REST and MCP return the same bytesC05 | one shared session-state serializer per aggregate builds the lesson, its observations, the candidate and the detection result, so the two transports are byte-identical by construction and tested that way | shipped |