feat(engine+web): wire the discretionary journal — CLI-only in, self-reported everywhere out (#705) - #736
Merged
Merged
Conversation
…reported everywhere out (#705) The `journal` table has been in the schema from the beginning with no repository method and no caller anywhere in the code. Dead schema is worse than no schema: a reader assumes a declared table is a used one. THE ONE RECORD HERE THAT NOTHING CAN CHECK Everything else keel keeps is a machine's observation or a claim about the world. An order is what a venue reported. A transaction is a line out of a venue's own export. An asset attestation says PAXG is backed by allocated gold -- a claim a prospectus could contradict. "I felt rushed and broke my own rule" has no external referent at all. That is not a defect: self-assessment is the only way this information exists, and no venue can produce it. But it means the record's value depends entirely on it staying visibly separate from the ones that can be checked. So `self-reported` is a SIXTH provenance in the timeline's closed vocabulary, not a reuse of `human-attested`. Filing a self-assessment under the word this feed uses for checkable human claims would put the one unverifiable record in the database under a heading implying otherwise. It carries WARN for the same reason `simulated` does: both mark a row that reads like evidence and is not. THE CLI IS THE ONLY WAY IN, AND `add` TAKES NO OPTIONS Attestations are human-sourced or refused. `keel journal add` prompts and requires a terminal, and it accepts no value options at all -- not merely "prompts by default". A `--emotion 3` would make the entry scriptable and leave the TTY gate guarding a ceremony that no longer needed a human to supply anything. A test asserts `keel/web/api.py` never reaches the writer. `_is_interactive`, NOT `_require_interactive_confirmation`. The heavier gate demands a typed `yes` and exists for DANGEROUS actions -- releasing a kill-switch, spending money -- and its own docstring warns against ceremony without a matching threat model. Writing a sentence about your own trading is not dangerous; it is unverifiable, which is a different problem that a confirmation prompt does nothing about. What is load-bearing is the terminal: off a TTY there is no human, so cron, a pipe and a script are all refused. And there is no edit. No update method, pinned by name, because the natural thing for a later contributor to add beside two methods is a third that updates -- and a journal you can go back and change records what you wish you had thought. BLANK IS "DID NOT SAY", AND `rules_followed` IS THREE-VALUED Every field but the timestamp may be skipped, including all of them: an operator recording one sentence about one day must not have to invent an emotion score to do it. A blank answer reaches the database as NULL, never a zero. `rules_followed=False` is the operator confessing they broke their own rules -- the most consequential thing this table can hold -- and `bool(None)` would print that confession over a question they skipped. Three values, three words, on every surface. An emotion score off the 1-5 scale and a dollar impact that is not a number are REFUSED rather than kept. A journal whose emotion column holds "9", "very bad" and "3" has three vocabularies and no series; one that read "lots" as zero would say the day cost nothing. NOTHING AN OPERATOR SAYS ABOUT THEMSELVES COMES BACK GREEN Every other green on the Insights page is a fact: a fill happened, a chain verified, an attestation is in date. "I followed my rules" is the operator's opinion of the operator, and a surface reflecting it back as a pass would be flattering them with their own words. NEUTRAL is the strongest thing that can honestly be said, and a swept test asserts no state in this section is ever `good` -- including a self-reported PROFIT, where `money()`'s default would have graded the sign of a number the operator chose. The entries are not the route's sortable collection, and that is a refusal rather than an omission: a journal reads forwards, and sorted by dollar impact it becomes a ranking of the operator's own worst days. The Strathern rail, in the one place where the thing being ranked is a person. ON THE SAME PAGE AS THE OTHER JOURNAL, DELIBERATELY `keel insights journal` already exists and is a filterable view of closed TRADES. Two things called a journal, and separate pages would let a reader meet one without learning the other exists -- so the discretionary entries ride `/api/journal` at `data.notes`, below the closed trades, with the marker on the heading and on every impact figure. Both CLI surfaces say which they are. Chained into `audit_events` (#721) like everything else a human swore to, so it rides the audit export with a real `row_hash` beside the orders and fills. Twelve mutants killed, including: a blank emotion stored as zero, a skipped rules question becoming a confession, an out-of-scale score kept as free text, an unparseable impact becoming zero, `journal add` no longer needing a terminal, the row filed as human-attested, `bool(None)` confessing, the entry going unchained, the limit keeping the oldest entries, the window closing at both ends, a self-reported impact that stops warning, and a followed rule graded good. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZZxmspQXe5qJ9FAsG13s6
…as truncating the journal An independent review found eleven issues. Every one of them sat on a seam the mutation battery had not reached: the client, and the limit. ONE RECORD'S PAGE CONTROL WAS TRUNCATING A DIFFERENT RECORD `read_journal` passed `?limit=` to both journals, so narrowing to one closed trade silently hid 300 of an operator's 301 notes -- and the page said nothing, because the payload shipped `shown_count` with no total and `render.js` never read even that. Measured: 60 entries, `?limit=1`, one note served. The rule is stated twice in the neighbouring docstrings this change should have read. `journal_payload` already says a client showing "50 of 812" needs both counts; `get_equity_points` says a caller that bounds a read is showing a WINDOW and must say so, and names `count_equity_points` as how it learns what it left out. There was no `count_journal_entries`, so the payload could not have said it. The journal now has its OWN cap -- the coupling was a coincidence of two records sharing a route -- and carries `total_count` beside `shown_count` plus the sentence composed from them. Mutating the wiring back survived the whole 6,000- test suite before this. THE WHOLE SECTION COULD BE DELETED FROM THE CONSOLE WITH EVERY GATE GREEN Two independent deletions survived the full suite: the call to `notesSection`, and the SELF-REPORTED pill. The acceptance test claimed to cover "the console that renders it" and in fact called the payload function directly, and the parity scan cannot help -- an uncalled function still reads the keys it reads. That is the third time this series a client-side pin has needed a mutant to show it was pinning nothing. `--limit 0` PRINTED "No journal entries yet" OVER A JOURNAL And `--limit -1` printed everything, because SQLite reads a negative LIMIT as unbounded. `keel/web/api.py::_journal_limit` names this exact hazard for the other journal on the same route. Refused in `gather_journal` rather than only at the click option, so the guard travels with the function instead of one caller. THE "NO WEB WRITE PATH" TEST GUARDED A FILE WHERE A WRITE COULD NOT LIVE It grepped `keel/web/api.py`, whose own docstring says "Reads only. Not one route below answers a POST". The write surface is `keel.commands.setup.ACTIONS`, the only thing `server.do_POST` routes to -- and it ALREADY carries a human-attestation writer (`attest_asset`), which is precisely why a "quick note" box is the plausible next addition. Adding a journal writer to `setup.py` passed the old test untouched. THE EXPORT DROPPED EVERY SENTENCE AFTER THE FIRST `errors_made` and `chart_note` were an if/elif, so an entry carrying both exported only the error, and `screenshot_ref` reached the file in no column and no summary. This row is the journal's WHOLE representation in a CSV an operator hands to an auditor -- there is nowhere else any of it could reappear. AND THE READ INVENTED A THIRD SPELLING OF AN IDIOM TWO SIBLINGS SHARE `get_equity_points` and `get_cycle_balances` both cap with the same re-ordered subquery, and one names the other as canon. This used a DESC read reversed in Python "without a subquery" -- behaviourally equivalent for a positive limit, and exactly where the negative-limit bug came from. Also: the blocklist test for `journal add`'s options is now `journal_add.params == []` (a `--feeling 3` walked past six literal names); the append-only pin sweeps the package for `UPDATE journal`/`DELETE FROM journal` rather than three method names; `_optional_text`'s empty-string rule is tested; a stray `#:` attribute comment above a `def` is gone; and `_function_body` now finds module-private functions, which is why `notesSection` could be scanned at all. TWO THINGS NAMED RATHER THAN CLAIMED The module docstring said the TTY gate means "nothing can reach past it". A `pty.fork()` driver runnable from cron allocates a real terminal and this command answers it. True of every gate built on the same predicate; the honest statement is that the check makes automated entry something someone has to MEAN. And `test_the_limit_breaks_a_timestamp_tie_by_id` pins the outcome, not the `id DESC` clause: measured on this SQLite build, dropping it returns the same rows. The clause stays because the sibling states why, but no test here can make its absence fail, and the test says so. Thirteen mutants killed of fourteen tried; the fourteenth is the tie-break above, recorded in the test as a named blind spot rather than left implied. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZZxmspQXe5qJ9FAsG13s6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #705.
The
journaltable has been in the schema from the beginning with no repository method and no caller anywhere in the code. Dead schema is worse than no schema: a reader assumes a declared table is a used one.The one record here that nothing can check
Everything else keel keeps is a machine's observation or a claim about the world — an order is what a venue reported, a transaction is a line out of a venue's own export, an asset attestation says PAXG is backed by allocated gold and a prospectus could contradict it.
"I felt rushed and broke my own rule" has no external referent at all. That isn't a defect — self-assessment is the only way this information exists, and no venue can produce it — but it means the record's value depends entirely on staying visibly separate from the ones that can be checked.
So
self-reportedis a sixth provenance in the timeline's closed vocabulary, not a reuse ofhuman-attested. It carriesWARNfor the same reasonsimulateddoes: both mark a row that reads like evidence and is not.The CLI is the only way in, and
addtakes no optionsNot merely "it prompts by default" — a
--emotion 3would make the entry scriptable and leave the TTY gate guarding a ceremony that no longer needed a human to supply anything. A test assertskeel/web/api.pynever reaches the writer._is_interactive, not_require_interactive_confirmation. The heavier gate demands a typedyesand exists for dangerous actions — its own docstring warns against ceremony without a matching threat model. Writing a sentence about your own trading isn't dangerous; it's unverifiable, which a confirmation prompt does nothing about. What is load-bearing is the terminal: off a TTY there is no human.And there is no edit — no update method, pinned by name, because a journal you can go back and change records what you wish you had thought.
Blank is "did not say"
Every field but the timestamp may be skipped, including all of them.
rules_followed=Falseis the operator confessing they broke their own rules, andbool(None)would print that confession over a question they skipped — three values, three words, on every surface. An emotion score off the 1–5 scale and an impact that isn't a number are refused, not coerced: a column holding "9", "very bad" and "3" has three vocabularies and no series.Nothing an operator says about themselves comes back green
Every other green on this page is a fact. "I followed my rules" is the operator's opinion of the operator, and reflecting it back as a pass would be flattering them with their own words.
NEUTRALis the strongest honest reading, and a swept test asserts no state in this section is evergood— including a self-reported profit, wheremoney()'s default would otherwise have graded the sign of a number the operator chose.The entries are not the route's sortable collection — a refusal, not an omission. A journal reads forwards; sorted by dollar impact it becomes a ranking of the operator's own worst days. The Strathern rail, where the thing being ranked is a person.
On the same page as the other journal, deliberately
keel insights journalalready exists and is a filterable view of closed trades. Separate pages would let a reader meet one without learning the other exists, so the discretionary entries ride/api/journalatdata.notes, below the closed trades, with the marker on the heading and on every impact figure.Chained into
audit_events(#721) like everything else a human swore to, so it rides the audit export with a realrow_hashbeside the orders and fills.Acceptance
self-reportedjournalgains coverage; a test fails if it regresses to dead schema, and asserts the methods reach the real tableGates
E501is pre-existing indocs/experiments/)journal addno longer needing a terminal · the row filed ashuman-attested·bool(None)confessing · the entry going unchained · the limit keeping the oldest entries · the window closing at both ends · a self-reported impact that stops warning · a followed rule gradedgoodOne shared-test improvement fell out:
_ROW_ENDPOINTSnow accepts a dotted payload path, so a collection nested under a key (data.notes.entries) is checkable without inventing a route for the sake of the test.🤖 Generated with Claude Code
https://claude.ai/code/session_01KZZxmspQXe5qJ9FAsG13s6