feat(telemetry): a usage event records which project the reader was in (#4196, #3735)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m5s
CI & Build / Python tests (push) Failing after 1m15s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m5s
CI & Build / Python tests (push) Failing after 1m15s
CI & Build / Build & push image (push) Skipped
`RetrievalLog` has carried `project_id` since it existed, so "this record was SURFACED on project B" was always answerable. `note_usage_events` had none, so "this record was OPENED on project B" was not — and the two cannot be joined to recover it, because there is deliberately no session identity server-side. NoteUsageEvent's own docstring rules that out. That gap sat exactly on the question milestone 385 exists to answer. A lesson's whole claim is that it reaches a session on a project it was not written on, and step 8's acceptance is "retrieved on a different project AND opened". Each half was answerable; the conjunction was not. WHICH project, because the name is ambiguous and the wrong reading makes the column useless: it is the project the READER was in, never the one the record belongs to. The record's own project is already on the note; copying it here would answer a question nobody asked while looking like it answered this one. The surfacing half is free — every arm already holds the scope it just searched, so auto_inject, lesson_slot, the write-path arms and enter_project now record it. process_skill_sync does not and should not: it installs every Process the operator can reach, which is not a project-scoped question, so a project there would be a fiction. The pull half needs the caller, since a getter knows only what it was handed. The five single-record getters take `project_id: int = 0` and pass it through, following the convention `search` and `create_*` already set. Null stays an ordinary answer meaning "not reported" — a pull with no project is still a pull and still counts toward dead weight; it simply cannot speak to transfer. The four REST detail views report none for now: a human opening a record in a browser is a different event from an agent recalling one, and #2245 left that asymmetry deliberately undecided. Guarded the way #2245 and #2476 taught: by source inspection, because a parameter that was never threaded through changes no return value and shows up only as a column that is mysteriously always null. Three guards — the signature, the pass-through, and the arms — plus the can-fail test rule 167 asks for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"""note_usage_events records WHICH project the reader was in (#4196, #3735)
|
||||
|
||||
Revision ID: 0105
|
||||
Revises: 0104
|
||||
Create Date: 2026-09-20
|
||||
|
||||
Milestone 385. `RetrievalLog` has carried `project_id` from the start, so
|
||||
"this record was SURFACED on project B" has always been answerable. The usage
|
||||
table never had one, so "this record was OPENED on project B" never was — and
|
||||
the two cannot be joined to recover it, because there is no session identity
|
||||
server-side (see the NoteUsageEvent docstring, which rules that out on
|
||||
purpose).
|
||||
|
||||
That gap sits exactly on the question milestone 385 exists to answer. A
|
||||
lesson's whole claim is that it reaches a session on a project it was not
|
||||
written on; step 8's acceptance is "retrieved on a different project, at the
|
||||
moment it applies, AND opened". Surfacing cross-project: answerable. Opening:
|
||||
answerable. Both at once: not, until this column.
|
||||
|
||||
WHICH PROJECT THIS IS, because the name is ambiguous and the wrong reading
|
||||
makes the column useless. It is the project the READER was working in at the
|
||||
moment of the event — NOT the project the record belongs to. The record's own
|
||||
project is already on `notes.project_id`; copying it here would answer a
|
||||
question nobody asked and quietly look like it answered this one.
|
||||
|
||||
NULLABLE, and not backfilled. Every existing row was written by a reader who
|
||||
was somewhere, but naming it would be inventing a fact. Null means "not
|
||||
reported", which is a different answer from a project id that might be wrong —
|
||||
the same reasoning 0104 applied to its calibration stamps.
|
||||
|
||||
NULL IS ALSO THE ONGOING DEGRADED CASE, not only a historical one. A surfacing
|
||||
arm always knows the project it searched; a getter only knows what its caller
|
||||
passed. A pull with no project is still a pull and still counts toward "is this
|
||||
dead weight" — it simply cannot answer "was it opened away from home".
|
||||
|
||||
THE INDEX carries `note_id` first, matching `ix_note_usage_note_event`: every
|
||||
readout starts from a set of note ids and narrows, never from a project.
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "0105"
|
||||
down_revision = "0104"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"note_usage_events",
|
||||
sa.Column("project_id", sa.Integer(), nullable=True),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_note_usage_note_project",
|
||||
"note_usage_events",
|
||||
["note_id", "project_id"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_note_usage_note_project", table_name="note_usage_events")
|
||||
op.drop_column("note_usage_events", "project_id")
|
||||
Reference in New Issue
Block a user