Skip to content

feat(bin): advisory risk level beside a recorded pull request, opt-in - #45

Closed
talktejas wants to merge 3 commits into
mainfrom
fm/fm-jev-pr-risk-level-2
Closed

talktejas wants to merge 3 commits into
mainfrom
fm/fm-jev-pr-risk-level-2

Conversation

@talktejas

Copy link
Copy Markdown
Owner

Intent

A research report on using Jev (TypeSafe's decision-only model) in the coding workflow proposed ten integrations in one table. The answer on row 4: "impement"

Row 4 as it was shown:

  • What it does: Risk level on every merge request. Code collects the facts (database migrations, login and permissions, payments, deletions, size). Jev adds three yes/no answers: behaviour changed with no test, description does not match the change, something hard to undo. Result: low, medium or high beside the merge request.
  • Where it plugs in: bin/fm-pr-check.sh, the step that already records a pull request when a worker reports it ready
  • What it saves: Shows which merges need a real look. Projects without the no-mistakes review pipeline get a risk level too; today only that pipeline gives one.
  • Who else does it: hush (2★) flags pull requests as risky, untested or undescribed; its issue labels were 78% right with 19% left untouched on 800 issues. jev-pr-risk-evaluator (0★, early): "No model performs arithmetic, assigns reviewers, or chooses the final route."
  • Build size: Small to medium. Most of the level comes from file paths, which is plain code.
  • If Jev is unsure or down: Shows "risk not rated". Never lowers a level.

Stated with the table: Jev only picks from a fixed list; in every row, if Jev is unsure or down, work carries on exactly as today.

What Changed

  • bin/fm-pr-check.sh now prints one advisory risk: low|medium|high - <why> or risk: not rated - <why> line after a pull request is recorded and its merge poll armed, via the new bin/fm-pr-risk-lib.sh. It is off unless TYPESAFE_API_KEY is present, and nothing reads the line to block, merge, approve, or discard anything. bin/fm-pr-merge.sh re-registers with the new --no-risk flag, so the level is rated once at ready time and never during a merge.
  • Code sets the level from the diff bin/fm-review-diff.sh prints: database migration, login and permissions, or payments paths, or 1500+ changed lines, are high; a deleted file, 400+ changed lines, or 20+ files are medium. Jev is then asked up to three yes/no questions (behaviour changed with no test, description does not match the change, something hard to undo). An answer counts only at or above the shared confidence floor and can only raise the level. risk: low needs all three questions settled; otherwise, with no fact raising the level, the line is risk: not rated and names each unanswered question.
  • New fm_jev_code_allowed in bin/fm-jev-lib.sh gates what is sent: only projects listed in the local config/jev-code-projects have their title, description, file list, and capped diff excerpt sent to Jev. Unlisted projects are rated from the facts alone, with no forge read and no model call. Adds tests/fm-pr-risk.test.sh (registered in bin/fm-test-run.sh), updates to tests/fm-pr-merge.test.sh and tests/fm-teardown.test.sh, a "Pull request risk level" section in docs/configuration.md, and docs/verification/jev-pr-risk.md.

Risk Assessment

✅ Low: The fix round correctly keeps the path intact for an authori[sz] match while still excluding authors/ paths, and the feature as a whole is an opt-in advisory line printed after registration completes, which sends nothing for unlisted projects, only ever raises a level, and cannot change the registration or its exit status.

Testing

Drove 35 real bin/fm-pr-check.sh registrations in a throwaway home with real git, the real bin/fm-review-diff.sh, and real curl over HTTP to a local stand-in for the Jev endpoint; every registration exited 0, recorded the pull request, and armed the merge poll, and every printed line matched the intent. Also ran tests/fm-pr-risk.test.sh (passed) and the full tests/fm-pr-merge.test.sh, whose test_merge_registration_never_rates_risk case I confirmed passing by running it alone. Not exercised: the real typesafe.ai model's answers, and a real GitHub or GitLab forge. One observation, not a failure: a file deleted beside a similar new file is reported by git as a rename, so it carries no deletion fact (run 12). No visual evidence applies; the surface is one CLI output line, captured as a transcript.

  • Live validation: ✅ go - 15 of 17 scenarios driven live against the product
Scenario Result Live Evidence
Worker reports a PR ready in a home with no TYPESAFE_API_KEY: output is exactly the registration line, no risk line, no Jev request ✅ pass live live-transcript.txt run 01
Key present and project listed, small code change, Jev answers no to all three: risk: low is printed after the armed: line ✅ pass live live-transcript.txt run 02 (includes the three requests the endpoint received)
A test file changed beside the code, or a docs-only change: untested is settled in code and Jev is asked only the other two questions ✅ pass live live-transcript.txt runs 03 and 15
Code-collected facts set the level: migration, login/permissions, payments paths and 1500+ lines print high; a deleted file and 400+ lines print medium ✅ pass live live-transcript.txt runs 04, 08, 10, 11, 13, 14
Review fix: authorization, authorize and unauthorized paths print risk: high - login and permissions ✅ pass live live-transcript.txt runs 05, 06, 07
Adversarial: a src/authors/ path is not read as login and prints low ✅ pass live live-transcript.txt run 09
A counted Jev yes only raises: untested or mismatch gives medium, hard-to-undo gives high, and each reason is named ✅ pass live live-transcript.txt runs 16-19
Adversarial: Jev below the 0.6 confidence floor (yes or no) prints risk: not rated, never low, and a low-confidence yes is not counted ✅ pass live live-transcript.txt runs 20, 21
Adversarial: Jev picks an option outside yes/no, replies non-JSON, returns HTTP 500, hangs past the timeout, or refuses the connection: risk: not rated, exit 0, PR recorded, merge poll armed, only o… ✅ pass live live-transcript.txt runs 22-26 (hang case took 7 s wall time)
Forge title and description unreadable: mismatch is left unanswered and the line is not rated, the other two questions are still asked ✅ pass live live-transcript.txt run 27
Adversarial: Jev never lowers a level - a migration stays high with confident no answers or HTTP 500, a deletion stays medium when Jev is unsure ✅ pass live live-transcript.txt runs 28, 29, 30
Project not in config/jev-code-projects (commented out, similar name, or file absent) with the key present: no Jev request, no forge description read, facts-only level with unanswered questions named ✅ pass live live-transcript.txt runs 31, 32, 33
Merge-time re-registration with --no-risk prints no risk line and makes no Jev request ✅ pass live live-transcript.txt run 34; test_merge_registration_never_rates_risk passed
The API key reaches Jev only in the Authorization header: absent from curl argv and from the task record ✅ pass live live-transcript.txt run 02 footer lines; tests/fm-pr-risk.test.sh child-environment assertions
GitLab merge request URL gets a risk line the same way ✅ pass live live-transcript.txt run 35 (glab was a stand-in; glab is not installed on this machine)
The real typesafe.ai Jev model gives sensible yes/no answers to the three shipped questions on real changes ⏸️ untested no No TYPESAFE_API_KEY was provided to this run's environment, and I did not go looking for one in other homes or send code to an external service without authorization. To cover it, export TYPESAFE_API_…
Risk line against a real GitHub pull request (real gh pr view title and description) ⏸️ untested no The fixture changes have no real pull request, so gh was a stand-in that answered title and body from a file. Covering it needs a real PR whose head commit exists in a task worktree.
Evidence: Live transcript of all 35 registrations

Source: Live transcript of all 35 registrations

##### A. Key absent: nothing changes

=== 01. no TYPESAFE_API_KEY anywhere, project listed
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: none]
[description read from forge: 0]
stderr: ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
stderr: ●  WATCHER DOWN - SUPERVISION IS OFF
stderr: ●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 999999s).
stderr: ●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
stderr: ●  This is a supervision warning only; the guarded operation WILL still run.
stderr: ●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
stderr: ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

##### B. Key in the home .env, project listed in config/jev-code-projects

=== 02. small code change, Jev answers no to all three -> low
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: low - no risk fact found and all three questions answered no
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: untested->no:0.95 mismatch->no:0.95 irreversible->no:0.95]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed
--- what the Jev endpoint received for that registration:
{"path":"/v1/systemone","question":"untested","reply":"no:0.95","auth_header":"Bearer fake-key-not-real-7f3a","options":["no","yes"],"title":"Rename helper","files":["src/util.js"],"facts":{"changed_lines":12,"files":1,"deleted_files":0,"test_files_changed":0,"code_files_changed":1,"touches_database_migration":false,"touches_login_or_permissions":false,"touches_payments":false},"diff_bytes":171}
{"path":"/v1/systemone","question":"mismatch","reply":"no:0.95","auth_header":"Bearer fake-key-not-real-7f3a","options":["no","yes"],"title":"Rename helper","files":["src/util.js"],"facts":{"changed_lines":12,"files":1,"deleted_files":0,"test_files_changed":0,"code_files_changed":1,"touches_database_migration":false,"touches_login_or_permissions":false,"touches_payments":false},"diff_bytes":171}
{"path":"/v1/systemone","question":"irreversible","reply":"no:0.95","auth_header":"Bearer fake-key-not-real-7f3a","options":["no","yes"],"title":"Rename helper","files":["src/util.js"],"facts":{"changed_lines":12,"files":1,"deleted_files":0,"test_files_changed":0,"code_files_changed":1,"touches_database_migration":false,"touches_login_or_permissions":false,"touches_payments":false},"diff_bytes":171}
--- curl argv (key must not appear): 0 occurrence(s) of the key
--- key in task record: 0 occurrence(s)

=== 03. code + test file changed -> untested settled in code, not asked
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: low - no risk fact found and all three questions answered no
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: mismatch->no:0.95 irreversible->no:0.95]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

##### C. Facts collected by code set the level (Jev answers no to everything)

=== 04. database migration path
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: high - database migration
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: mismatch->no:0.95 irreversible->no:0.95]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 05. authorization path (review fix)
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: high - login and permissions
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: mismatch->no:0.95 irreversible->no:0.95]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 06. authorize path (review fix)
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: high - login and permissions
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: mismatch->no:0.95 irreversible->no:0.95]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 07. unauthorized path (review fix)
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: high - login and permissions
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: mismatch->no:0.95 irreversible->no:0.95]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 08. login path
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: high - login and permissions
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: mismatch->no:0.95 irreversible->no:0.95]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 09. ADVERSARIAL authors/ path must NOT read as login
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: low - no risk fact found and all three questions answered no
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: mismatch->no:0.95 irreversible->no:0.95]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 10. payments path
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: high - payments
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: mismatch->no:0.95 irreversible->no:0.95]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 11. a deleted file (beside an unrelated test file)
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: medium - 1 deleted file

... [7437 bytes truncated] ...


armed: state/task-a.check.sh
risk: not rated - no risk fact found; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: untested->garbage]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 24. ADVERSARIAL Jev returns HTTP 500
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: not rated - no risk fact found; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: untested->500]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 25. ADVERSARIAL Jev hangs past the 5 s timeout
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: not rated - no risk fact found; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: untested->sleep]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed
[wall time: 7 s]

=== 26. ADVERSARIAL Jev endpoint is down (connection refused)
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: not rated - no risk fact found; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: none]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 27. forge description unreadable (gh fails)
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: not rated - no risk fact found; unanswered: mismatch (description unreadable)
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: untested->no:0.95 irreversible->no:0.95]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

##### F. Never lowers a level the facts set

=== 28. ADVERSARIAL migration + Jev confidently says no to everything -> still high
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: high - database migration
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: untested->no:0.99 mismatch->no:0.99 irreversible->no:0.99]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 29. ADVERSARIAL migration + Jev HTTP 500 -> still high
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: high - database migration; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: untested->500]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 30. ADVERSARIAL deletion + Jev unsure -> still medium
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: medium - 1 deleted file(s); unanswered: untested (Jev unsure), mismatch (Jev unsure), irreversible (Jev unsure)
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: untested->no:0.4 mismatch->no:0.4 irreversible->no:0.4]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

##### G. Project not listed: nothing about it leaves the machine

=== 31. unlisted, no fact
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: not rated - no risk fact found; unanswered: untested (project not listed), mismatch (project not listed), irreversible (project not listed)
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: none]
[description read from forge: 0]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 32. unlisted, migration fact
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: high - database migration; unanswered: untested (project not listed), mismatch (project not listed), irreversible (project not listed)
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: none]
[description read from forge: 0]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

=== 33. list file absent, migration fact
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
armed: state/task-a.check.sh
risk: high - database migration; unanswered: untested (project not listed), mismatch (project not listed), irreversible (project not listed)
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: none]
[description read from forge: 0]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

##### H. Merge-time re-registration (--no-risk) rates nothing

=== 34. listed project, key present, --no-risk
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 --no-risk
armed: state/task-a.check.sh
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: none]
[description read from forge: 0]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed

##### I. GitLab merge request (fake glab answers title/description)

=== 35. gitlab MR, Jev: untested = yes
$ bin/fm-pr-check.sh task-a https://gitlab.com/g/p/-/merge_requests/5 
armed: state/task-a.check.sh
risk: medium - behaviour changed with no test
[exit 0] [pr recorded: 1] [merge poll armed: yes] [Jev requests: untested->yes:0.9 mismatch->no:0.95 irreversible->no:0.95]
[description read from forge: 1]
stderr: WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
stderr: fm-contributions: data directory unavailable
stderr: contributions: observation not armed; coverage is unconfirmed
Evidence: Scenario label and printed risk line, one pair per run

Source: Scenario label and printed risk line, one pair per run

=== 01. no TYPESAFE_API_KEY anywhere, project listed
=== 02. small code change, Jev answers no to all three -> low
risk: low - no risk fact found and all three questions answered no
=== 03. code + test file changed -> untested settled in code, not asked
risk: low - no risk fact found and all three questions answered no
=== 04. database migration path
risk: high - database migration
=== 05. authorization path (review fix)
risk: high - login and permissions
=== 06. authorize path (review fix)
risk: high - login and permissions
=== 07. unauthorized path (review fix)
risk: high - login and permissions
=== 08. login path
risk: high - login and permissions
=== 09. ADVERSARIAL authors/ path must NOT read as login
risk: low - no risk fact found and all three questions answered no
=== 10. payments path
risk: high - payments
=== 11. a deleted file (beside an unrelated test file)
risk: medium - 1 deleted file(s)
=== 12. a file moved and edited (git reports a rename, not a deletion)
risk: low - no risk fact found and all three questions answered no
=== 13. size: 452 lines (>=400)
risk: medium - size (452 lines in 2 files)
=== 14. size: 1602 lines (>=1500)
risk: high - size (1602 lines in 2 files)
=== 15. docs only
risk: low - no risk fact found and all three questions answered no
=== 16. Jev: behaviour changed with no test = yes
risk: medium - behaviour changed with no test
=== 17. Jev: description does not match = yes
risk: medium - description does not match the change
=== 18. Jev: hard to undo = yes
risk: high - something hard to undo
=== 19. Jev: yes to all three
risk: high - behaviour changed with no test, description does not match the change, something hard to undo
=== 20. ADVERSARIAL Jev answers no at confidence 0.4 (below the 0.6 floor)
risk: not rated - no risk fact found; unanswered: untested (Jev unsure), mismatch (Jev unsure), irreversible (Jev unsure)
=== 21. ADVERSARIAL Jev answers yes at confidence 0.4 (below the floor, not counted)
risk: not rated - no risk fact found; unanswered: untested (Jev unsure), mismatch (Jev unsure), irreversible (Jev unsure)
=== 22. ADVERSARIAL Jev picks "maybe", which is not on the fixed list
risk: not rated - no risk fact found; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
=== 23. ADVERSARIAL Jev replies HTML instead of JSON
risk: not rated - no risk fact found; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
=== 24. ADVERSARIAL Jev returns HTTP 500
risk: not rated - no risk fact found; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
=== 25. ADVERSARIAL Jev hangs past the 5 s timeout
risk: not rated - no risk fact found; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
=== 26. ADVERSARIAL Jev endpoint is down (connection refused)
risk: not rated - no risk fact found; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
=== 27. forge description unreadable (gh fails)
risk: not rated - no risk fact found; unanswered: mismatch (description unreadable)
=== 28. ADVERSARIAL migration + Jev confidently says no to everything -> still high
risk: high - database migration
=== 29. ADVERSARIAL migration + Jev HTTP 500 -> still high
risk: high - database migration; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
=== 30. ADVERSARIAL deletion + Jev unsure -> still medium
risk: medium - 1 deleted file(s); unanswered: untested (Jev unsure), mismatch (Jev unsure), irreversible (Jev unsure)
=== 31. unlisted, no fact
risk: not rated - no risk fact found; unanswered: untested (project not listed), mismatch (project not listed), irreversible (project not listed)
=== 32. unlisted, migration fact
risk: high - database migration; unanswered: untested (project not listed), mismatch (project not listed), irreversible (project not listed)
=== 33. list file absent, migration fact
risk: high - database migration; unanswered: untested (project not listed), mismatch (project not listed), irreversible (project not listed)
=== 34. listed project, key present, --no-risk
=== 35. gitlab MR, Jev: untested = yes
risk: medium - behaviour changed with no test
Evidence: Driver script that produced the transcript

Source: Driver script that produced the transcript

#!/usr/bin/env bash
# Drives the real bin/fm-pr-check.sh in a throwaway home. Real git, real
# bin/fm-review-diff.sh, real curl over real HTTP. Only the two external
# services are stand-ins: api.typesafe.ai is a local HTTP server (the curl on
# PATH rewrites only the host, then execs /usr/bin/curl), and gh/glab answer the
# title and description from a file because the invented changes have no PR.
set -u
ROOT=$1; EV=$(cd "$(dirname "$0")" && pwd)
T=$(mktemp -d /tmp/fm-risk-live.XXXXXX); H=$T/home; BIN=$T/bin; PROJ=$T/alpha; WT=$T/wt
PORT=18473; KEY=fake-key-not-real-7f3a
mkdir -p "$H/state" "$H/config" "$BIN"
cat > "$BIN/curl" <<EOF
#!/usr/bin/env bash
args=()
for a; do args+=("\${a/https:\/\/api.typesafe.ai/http://127.0.0.1:$PORT}"); done
printf '%s\n' "\$*" >> "$T/curl-argv"
exec /usr/bin/curl "\${args[@]}"
EOF
cat > "$BIN/gh" <<EOF
#!/usr/bin/env bash
printf '%s\n' "\$*" >> "$T/forge-calls"
case "\$*" in *title,body*) [ -f "$T/pr.json" ] && cat "$T/pr.json" && exit 0 ;; esac
exit 1
EOF
cat > "$BIN/glab" <<EOF
#!/usr/bin/env bash
printf 'glab %s\n' "\$*" >> "$T/forge-calls"
case "\$*" in *"mr view"*"-F json"*) [ -f "$T/mr.json" ] && cat "$T/mr.json" && exit 0 ;; esac
exit 1
EOF
chmod +x "$BIN"/*
export PATH="$BIN:$PATH" FM_HOME="$H" FM_STATE_OVERRIDE="$H/state" FM_GUARD_GRACE=999999
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.invalid
unset TYPESAFE_API_KEY
git init -q -b main "$PROJ"; mkdir -p "$PROJ/src"; seq 1 4 > "$PROJ/src/old.js"; echo '# alpha' > "$PROJ/README.md"
git -C "$PROJ" add -A; git -C "$PROJ" commit -qm initial
git -C "$PROJ" worktree add --quiet -b fm/task-a "$WT"

server_up() { python3 "$EV/fake-jev-server.py" $PORT "$T/mode" "$T/requests" & SRV=$!; sleep 0.7; }
server_down() { kill "$SRV" 2>/dev/null; wait "$SRV" 2>/dev/null; }
change() {  # <path>:<lines>|<path>:deleted ...
  local spec path n
  git -C "$WT" reset -q --hard main
  for spec; do path=${spec%:*} n=${spec##*:}
    if [ "$n" = deleted ]; then git -C "$WT" rm -q -- "$path"
    else mkdir -p "$WT/$(dirname "$path")"; seq 1 "$n" > "$WT/$path"; git -C "$WT" add -- "$path"; fi
  done
  git -C "$WT" commit -qm change
}
N=0
run() {  # <label> <url> [extra args]   -> transcript of one registration
  local label=$1 url=$2; shift 2
  N=$((N+1))
  printf 'window=fm-task-a\nkind=ship\nworktree=%s\nproject=%s\n' "$WT" "$PROJ" > "$H/state/task-a.meta"
  rm -f "$H/state/task-a.pr-poll" "$H/state/task-a.check.sh" "$T/requests" "$T/forge-calls" "$T/curl-argv"
  printf '\n=== %02d. %s\n' "$N" "$label"
  printf '$ bin/fm-pr-check.sh task-a %s %s\n' "$url" "$*"
  "$ROOT/bin/fm-pr-check.sh" task-a "$url" "$@" 2>"$T/err"; rc=$?
  printf '[exit %s] [pr recorded: %s] [merge poll armed: %s] ' "$rc" \
    "$(grep -c "^pr=$url\$" "$H/state/task-a.meta")" "$([ -e "$H/state/task-a.pr-poll" ] && echo yes || echo no)"
  if [ -s "$T/requests" ]; then
    printf '[Jev requests: %s]\n' "$(jq -r '"\(.question)->\(.reply)"' "$T/requests" | paste -sd' ')"
  else printf '[Jev requests: none]\n'; fi
  printf '[description read from forge: %s]\n' "$(grep -c 'title,body\|mr view' "$T/forge-calls" 2>/dev/null || true)"
  [ ! -s "$T/err" ] || sed 's/^/stderr: /' "$T/err"
}
mode() { printf '%s' "$1" > "$T/mode"; }
pr() { jq -n --arg t "$1" --arg b "$2" '{title:$t, body:$b}' > "$T/pr.json"; }
GH=https://github.com/o/r/pull/8
ALLNO='{"*":"no:0.95"}'
mode "$ALLNO"; pr 'Rename helper' 'Pure rename, no behaviour change.'
server_up

echo "##### A. Key absent: nothing changes"
change src/util.js:12
run 'no TYPESAFE_API_KEY anywhere, project listed' "$GH"

echo; echo "##### B. Key in the home .env, project listed in config/jev-code-projects"
printf 'TYPESAFE_API_KEY=%s\n' "$KEY" > "$H/.env"
printf '# projects Jev may see\nalpha\n' > "$H/config/jev-code-projects"
run 'small code change, Jev answers no to all three -> low' "$GH"
echo "--- what the Jev endpoint received for that registration:"; jq -c . "$T/requests"
echo "--- curl argv (key must not appear): $(grep -c "$KEY" "$T/curl-argv") occurrence(s) of the key"
echo "--- key in task record: $(grep -c "$KEY" "$H/state/task-a.meta") occurrence(s)"
change src/util.js:12 tests/util.test.js:5
run 'code + test file changed -> untested settled in code, not asked' "$GH"

echo; echo "##### C. Facts collected by code set the level (Jev answers no to everything)"
change db/migrations/0042_drop.sql:3 tests/x.test.js:2;   run 'database migration path' "$GH"
change src/authorization/policy.rb:5 tests/x.test.js:2;   run 'authorization path (review fix)' "$GH"
change lib/authorize.js:5 tests/x.test.js:2;              run 'authorize path (review fix)' "$GH"
change app/unauthorized_handler.py:5 tests/x.test.js:2;   run 'unauthorized path (review fix)' "$GH"
change src/login/form.js:5 tests/x.test.js:2;             run 'login path' "$GH"
change src/authors/list.js:5 tests/x.test.js:2;           run 'ADVERSARIAL authors/ path must NOT read as login' "$GH"
change src/billing/invoice.js:5 tests/x.test.js:2;        run 'payments path' "$GH"
change src/old.js:deleted tests/x.test.js:40;             run 'a deleted file (beside an unrelated test file)' "$GH"
change src/old.js:deleted tests/x.test.js:2;              run 'a file moved and edited (git reports a rename, not a deletion)' "$GH"
change src/big.js:450 tests/x.test.js:2;                  run 'size: 452 lines (>=400)' "$GH"
change src/huge.js:1600 tests/x.test.js:2;                run 'size: 1602 lines (>=1500)' "$GH"
change docs/guide.md:10;                                  run 'docs only' "$GH"

echo; echo "##### D. A counted yes from Jev only raises"
change src/parser.js:12
mode '{"*":"no:0.95","untested":"yes:0.9"}';     run 'Jev: behaviour changed with no test = yes' "$GH"
mode '{"*":"no:0.95","mismatch":"yes:0.9"}';     run 'Jev: description does not match = yes' "$GH"
mode '{"*":"no:0.95","irreversible":"yes:0.9"}'; run 'Jev: hard to undo = yes' "$GH"
mode '{"*":"yes:0.9"}';                          run 'Jev: yes to all three' "$GH"

echo; echo "##### E. Jev unsure, off the fixed list, or down: 'not rated', never 'low', registration intact"
mode '{"*":"no:0.4"}';   run 'ADVERSARIAL Jev answers no at confidence 0.4 (below the 0.6 floor)' "$GH"
mode '{"*":"yes:0.4"}';  run 'ADVERSARIAL Jev answers yes at confidence 0.4 (below the floor, not counted)' "$GH"
mode '{"*":"maybe"}';    run 'ADVERSARIAL Jev picks "maybe", which is not on the fixed list' "$GH"
mode '{"*":"garbage"}';  run 'ADVERSARIAL Jev replies HTML instead of JSON' "$GH"
mode '{"*":"500"}';      run 'ADVERSARIAL Jev returns HTTP 500' "$GH"
mode '{"*":"sleep"}'; s=$(date +%s); run 'ADVERSARIAL Jev hangs past the 5 s timeout' "$GH"; echo "[wall time: $(( $(date +%s) - s )) s]"
server_down;             run 'ADVERSARIAL Jev endpoint is down (connection refused)' "$GH"
rm -f "$T/pr.json"; server_up; mode "$ALLNO"
run 'forge description unreadable (gh fails)' "$GH"
pr 'Rename helper' 'Pure rename, no behaviour change.'

echo; echo "##### F. Never lowers a level the facts set"
change db/migrations/0042_drop.sql:3 src/parser.js:12
mode '{"*":"no:0.99"}';  run 'ADVERSARIAL migration + Jev confidently says no to everything -> still high' "$GH"
mode '{"*":"500"}';      run 'ADVERSARIAL migration + Jev HTTP 500 -> still high' "$GH"
change src/old.js:deleted src/parser.js:12
mode '{"*":"no:0.4"}';   run 'ADVERSARIAL deletion + Jev unsure -> still medium' "$GH"

echo; echo "##### G. Project not listed: nothing about it leaves the machine"
mode '{"*":"yes:0.9"}'
printf '# alpha\nalpha-web\n' > "$H/config/jev-code-projects"
change src/parser.js:12;                run 'unlisted, no fact' "$GH"
change db/migrations/0042_drop.sql:3;   run 'unlisted, migration fact' "$GH"
rm -f "$H/config/jev-code-projects";    run 'list file absent, migration fact' "$GH"
printf 'alpha\n' > "$H/config/jev-code-projects"

echo; echo "##### H. Merge-time re-registration (--no-risk) rates nothing"
run 'listed project, key present, --no-risk' "$GH" --no-risk

echo; echo "##### I. GitLab merge request (fake glab answers title/description)"
jq -n '{title:"Add parser", description:"Adds the parser."}' > "$T/mr.json"
change src/parser.js:12; mode '{"*":"no:0.95","untested":"yes:0.9"}'
run 'gitlab MR, Jev: untested = yes' https://gitlab.com/g/p/-/merge_requests/5
server_down; rm -rf "$T"
Evidence: Local stand-in for the Jev endpoint

Source: Local stand-in for the Jev endpoint

#!/usr/bin/env python3
"""Local stand-in for https://api.typesafe.ai used only to drive bin/fm-pr-check.sh.
Replies per question key from a mode file; logs every request it receives."""
import json, sys, time
from http.server import BaseHTTPRequestHandler, HTTPServer
MODE, LOG = sys.argv[2], sys.argv[3]
class H(BaseHTTPRequestHandler):
    def log_message(self, *a): pass
    def do_POST(self):
        body = json.loads(self.rfile.read(int(self.headers.get('Content-Length', 0))))
        key = next(iter(body['questions']))
        mode = json.load(open(MODE))
        how = mode.get(key, mode.get('*', 'no:0.95'))
        ch = body['state']['change']
        with open(LOG, 'a') as f:
            f.write(json.dumps({'path': self.path, 'question': key, 'reply': how,
                'auth_header': self.headers.get('Authorization'),
                'options': sorted(body['questions'][key]['criteria']),
                'title': ch['title'], 'files': [x['path'] for x in ch['files']],
                'facts': ch['facts'], 'diff_bytes': len(ch['diff_start'])}) + '\n')
        if how == 'sleep': time.sleep(8)
        if how == '500':
            self.send_response(500); self.end_headers(); self.wfile.write(b'boom'); return
        if how == 'garbage': out = b'<html>not json</html>'
        elif how == 'maybe':
            out = json.dumps({'model': 'fake', 'answers': {key: {'choice': 'maybe', 'confidence': 0.99,
                'probabilities': {'maybe': 0.99, 'no': 0.01}}}}).encode()
        else:
            c, conf = how.split(':'); conf = float(conf); o = 'no' if c == 'yes' else 'yes'
            out = json.dumps({'model': 'fake', 'answers': {key: {'choice': c, 'confidence': conf,
                'probabilities': {c: conf, o: round(1 - conf, 4)}}}}).encode()
        self.send_response(200); self.send_header('Content-Type', 'application/json')
        self.send_header('Content-Length', str(len(out))); self.end_headers()
        try: self.wfile.write(out)
        except BrokenPipeError: pass
HTTPServer(('127.0.0.1', int(sys.argv[1])), H).serve_forever()
Evidence: Representative printed lines
key absent -> armed: state/task-a.check.sh (no risk line)
small change, Jev no x3 -> risk: low - no risk fact found and all three questions answered no
db/migrations/0042_drop.sql -> risk: high - database migration
src/authorization/policy.rb -> risk: high - login and permissions
src/authors/list.js -> risk: low - no risk fact found and all three questions answered no
deleted file -> risk: medium - 1 deleted file(s)
Jev: hard to undo = yes -> risk: high - something hard to undo
Jev confidence 0.4 -> risk: not rated - no risk fact found; unanswered: untested (Jev unsure), mismatch (Jev unsure), irreversible (Jev unsure)
Jev down / 500 / timeout -> risk: not rated - no risk fact found; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
migration + Jev HTTP 500 -> risk: high - database migration; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
project not listed, migration -> risk: high - database migration; unanswered: untested (project not listed), mismatch (project not listed), irreversible (project not listed)
--no-risk (merge time) -> armed: state/task-a.check.sh (no risk line)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ bin/fm-pr-risk-lib.sh:80 - gsub(/author/, &#34;&#34;, lp) is meant to stop authors/ paths reading as login, but it also erases the stem of every authorization path before the login/permissions pattern runs. Confirmed with the same awk expression: src/authorization/policy.rb -> src/ization/policy.rb, lib/authorize.js -> lib/ize.js, app/unauthorized_handler.py -> app/unized_handler.py, all auth=0. A pull request touching only such a file gets no login and permissions fact and can print risk: low (or not rated) instead of high, silently, on exactly the category the intent names ("login and permissions"). The stripped lp is also reused for the migration, payments, test and docs patterns. Fix: keep lp intact and exclude only the author word, e.g. compute a separate copy with gsub(/authors?([^a-z]|$)/, &#34;/&#34;)-style narrowing or test lp ~ /authori[sz]/ before stripping; add a case for authorization/authorize next to the existing src/authors/list.js test.
  • ⚠️ bin/fm-jev-lib.sh:93 - New component not named by the intent: the per-project allow-list config/jev-code-projects read by fm_jev_code_allowed (used at bin/fm-pr-risk-lib.sh:158), a second per-project opt-in beside the existing one in config/house-rules.json. It ships absent, so with only the key set every project is unlisted: Jev is asked nothing and a change with no path/size fact always prints risk: not rated ... (project not listed), never low. That narrows the intent's "Risk level on every merge request" / "Result: low, medium or high beside the merge request" and "Projects without the no-mistakes review pipeline get a risk level too" to projects someone lists by hand. The intent requires no such list, so the simplification remedy is to remove it and let the key alone enable the rating. The counter-consideration is a policy decision only the author can make: without the list, any home that already holds TYPESAFE_API_KEY for dispatch resolution would start sending each ready pull request's title, description, file list and up to 30000 bytes of diff to https://api.typesafe.ai. Please decide: key alone is sufficient consent to send project code (remove the list), or keep the list as the required privacy gate. If kept, note it matches on the directory basename of project=, so two projects with the same folder name under different parents are opted in together.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 15 of 17 scenarios driven live against the product
Scenario Result Live Evidence
Worker reports a PR ready in a home with no TYPESAFE_API_KEY: output is exactly the registration line, no risk line, no Jev request ✅ pass live live-transcript.txt run 01
Key present and project listed, small code change, Jev answers no to all three: risk: low is printed after the armed: line ✅ pass live live-transcript.txt run 02 (includes the three requests the endpoint received)
A test file changed beside the code, or a docs-only change: untested is settled in code and Jev is asked only the other two questions ✅ pass live live-transcript.txt runs 03 and 15
Code-collected facts set the level: migration, login/permissions, payments paths and 1500+ lines print high; a deleted file and 400+ lines print medium ✅ pass live live-transcript.txt runs 04, 08, 10, 11, 13, 14
Review fix: authorization, authorize and unauthorized paths print risk: high - login and permissions ✅ pass live live-transcript.txt runs 05, 06, 07
Adversarial: a src/authors/ path is not read as login and prints low ✅ pass live live-transcript.txt run 09
A counted Jev yes only raises: untested or mismatch gives medium, hard-to-undo gives high, and each reason is named ✅ pass live live-transcript.txt runs 16-19
Adversarial: Jev below the 0.6 confidence floor (yes or no) prints risk: not rated, never low, and a low-confidence yes is not counted ✅ pass live live-transcript.txt runs 20, 21
Adversarial: Jev picks an option outside yes/no, replies non-JSON, returns HTTP 500, hangs past the timeout, or refuses the connection: risk: not rated, exit 0, PR recorded, merge poll armed, only o… ✅ pass live live-transcript.txt runs 22-26 (hang case took 7 s wall time)
Forge title and description unreadable: mismatch is left unanswered and the line is not rated, the other two questions are still asked ✅ pass live live-transcript.txt run 27
Adversarial: Jev never lowers a level - a migration stays high with confident no answers or HTTP 500, a deletion stays medium when Jev is unsure ✅ pass live live-transcript.txt runs 28, 29, 30
Project not in config/jev-code-projects (commented out, similar name, or file absent) with the key present: no Jev request, no forge description read, facts-only level with unanswered questions named ✅ pass live live-transcript.txt runs 31, 32, 33
Merge-time re-registration with --no-risk prints no risk line and makes no Jev request ✅ pass live live-transcript.txt run 34; test_merge_registration_never_rates_risk passed
The API key reaches Jev only in the Authorization header: absent from curl argv and from the task record ✅ pass live live-transcript.txt run 02 footer lines; tests/fm-pr-risk.test.sh child-environment assertions
GitLab merge request URL gets a risk line the same way ✅ pass live live-transcript.txt run 35 (glab was a stand-in; glab is not installed on this machine)
The real typesafe.ai Jev model gives sensible yes/no answers to the three shipped questions on real changes ⏸️ untested no No TYPESAFE_API_KEY was provided to this run's environment, and I did not go looking for one in other homes or send code to an external service without authorization. To cover it, export TYPESAFE_API_…
Risk line against a real GitHub pull request (real gh pr view title and description) ⏸️ untested no The fixture changes have no real pull request, so gh was a stand-in that answered title and body from a file. Covering it needs a real PR whose head commit exists in a task worktree.
  • bash ~/.no-mistakes/evidence/01M46QQZZMBH33BFP81N8CVVQC/drive.sh &#34;$PWD&#34; - 35 live bin/fm-pr-check.sh task-a &lt;url&gt; [--no-risk] registrations against a local Jev stand-in
  • bash tests/fm-pr-risk.test.sh - all four groups passed
  • bash tests/fm-pr-merge.test.sh - full file run; no failure lines in the filtered output
  • test_merge_registration_never_rates_risk from tests/fm-pr-merge.test.sh run alone via a temporary trimmed copy (removed afterwards) - passed
  • Manual check: the Jev stand-in's request log shows path /v1/systemone, options exactly no/yes, one question per request, and the key only in the Authorization header (0 occurrences in curl argv and in the task record)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

bin/fm-pr-check.sh prints one advisory risk line after a pull request is
registered as ready, when TYPESAFE_API_KEY is present. Code sets the level
from paths, deletions and size; for a project listed in
config/jev-code-projects Jev answers up to three yes/no questions through
bin/fm-jev-lib.sh and may only raise it. An unanswered question never
yields low, an unlisted project sends nothing, and the registration
bin/fm-pr-merge.sh performs skips the rating.
@talktejas

Copy link
Copy Markdown
Owner Author

Superseded by #50, which carries this change's final content together with the other three Jev items on one branch.

@talktejas talktejas closed this Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant