# Sharing with Jadpo HQ

Sharing is how Jadpo learns which signals are right: which findings people
take, which they ignore and why, and what the scan missed. Jadpo HQ is the
service that collects them.

**Sharing is on unless it is turned off, and nothing goes unless someone
runs `share`.** Only hashes go without a person's approval; a snippet's
text never does.

```text
$ jadpo suggest share
✓ Sent 3 hashes to Jadpo HQ
    kinds  1 accepted, 1 found, 1 ignored
    asked  nothing more
    token  new, in app.jadpo: commit it so every copy counts as one project

Sharing is on
  Only hashes went. To stop sharing, add `share: false` to `suggestions { }`.
```

## The project token

`app.jadpo` holds `suggestions { project_token: "…" }`: 32 random hex
digits tied to nothing, no repository, person or machine. It is committed,
so a team counts as one project, and HQ counts distinct projects without
knowing which they are.

Whenever it is missing, `share` writes it before sending, made from the
operating system's random generator. With nothing pending it writes none.
`share --new-token` replaces it and sends nothing. A fork shares its
parent's token until it does. Only 32 lower-case hex digits read as a
token: any other value is `type.suggestions_value_invalid`, so a
repository's name can't be used as one.

## Turning it off

`suggestions { share: false }` turns sharing off for the project.
`JADPO_SUGGEST_SHARE=off` in the environment turns it off for a whole
machine or organisation; any value but `on` or empty turns it off, so a
misspelling never sends. `share: false` counts in whichever file the
`suggestions { }` is in. Neither can turn sharing back on over the other.

Either way, every form of `share`, `--approve` and `--new-token` included,
stops at once. It sends nothing, writes nothing, keeps the pending
entries and says where sharing was turned off. The scan's report drops its
instruction to share. The scan, `ignore` and `found` still record entries.
A `suggestions { }` block with a problem stops `share` too.

## What `share` sends

`share` sends every pending entry's text hash with its kind (`found`,
`accepted` or `ignored`), its signal and its suggestion, and the token. It
does so at once, with no prompt: the agent runs it itself at the end of a
suggest session, as the report's instruction says. The signal and
suggestion go because HQ reassesses a signal by how often it is ignored,
and most entries never send their text. Nothing else goes, not even
Jadpo's version.

HQ replies with the hashes whose text it wants: those seen from enough
distinct projects. The command writes the request's address and body to
`.jadpo/suggest_wanted`, shows exactly what `share --approve` would send,
and stops.

```text
$ jadpo suggest share
✓ Sent 3 hashes to Jadpo HQ
    kinds  1 accepted, 1 found, 1 ignored
    asked  1 snippet

What `jadpo suggest share --approve` sends
  POST https://hq.example.test/v1/suggestions/snippets
  {
    "snippets": [
      {
        "kind": "ignored",
        "reason": "Basis points, not money",
        "signal": "conversion",
        "suggestion": "Money",
        "text": "total_price * 100"
      }
    ]
  }

◆ decision  Jadpo HQ asks for 1 snippet of this project's code
  next      Ask the person whether to send it, showing them the payload above.
            Run `jadpo suggest share --approve` only after they say yes;
            otherwise run `jadpo suggest share --decline`. (human)
```

Here `(human)` means the choice is yours: whether a snippet of your code
leaves your machine.

## Approving or declining

A person approves, directly or through the agent asking them in its
conversation. Only then does `share --approve` send those bytes to that
address, whatever `JADPO_SUGGEST_HQ` says by then. It sends the snippets,
each with its kind, signal and suggestion and, for an ignored one, its
reason; a `found` entry's reason stays local. HQ recomputes their hashes.

`share --decline` drops them and sends nothing. The next `share` replaces
what waits, unsent.

```text
$ jadpo suggest share --approve
✓ Sent 1 snippet to Jadpo HQ

$ JADPO_SUGGEST_SHARE=off jadpo suggest share
✓ Sent nothing: sharing is turned off
    where  JADPO_SUGGEST_SHARE=off in the environment
```

`--json` gives the counts, the hashes HQ asked for, the payload, the
address it goes to, and an `approval` object an agent relays: the
question, the payload, the two commands, and the rule that `--approve` is
only valid after the person said yes. The command can't check that they
did, so asking is the agent's job, written into the decision's `next`.

## Unattended runs

An unattended run (`--unattended`, or `CI` set to anything) sends only
hashes and skips HQ's request for text. HQ asks the next project that
sends that hash, and the next `share` drops a request left waiting. An
unattended run can't `--approve` (`cli.suggest_arguments`), and `share`'s
flags given without `share` are refused rather than ignored.

## Afterwards

The pending file is wiped, whether HQ wanted anything or not; a send that
fails keeps it. The first time a checkout sends hashes, `share` says so in
one line, with how to turn sharing off. That first time is noted in
`.jadpo/suggest_shared`, so nothing is written outside the project.

## While HQ isn't running

No HQ address is configured by default, so a suggest session never ends
in a failure while HQ doesn't exist. `share` says
`Sharing isn't available yet: Jadpo HQ isn't running. Nothing was sent;
your entries are kept.`, sends nothing, keeps the entries and succeeds.
The scan's report leaves out its instruction to share, as it does when
sharing is off ([The protocol](sharing-with-jadpo-hq.md#the-protocol)).

## The protocol

This is the contract the real HQ implements unchanged: HTTPS and JSON,
UTF-8, under a versioned path.

- **Address.** `JADPO_SUGGEST_HQ` in the environment. There is no default
  yet: the release that launches HQ, with the registry, sets the real
  address as the default. `http://` is accepted only for a loopback host
  (`localhost`, `127.0.0.1`, `[::1]`), which is how the tests reach the
  fake. Anything else must be `https://`, through OpenSSL, with the
  platform's certificate store and the host name checked. Only the
  `jadpo` tool links OpenSSL; no deployable does.
- **Headers.** `Content-Type: application/json`, `Accept:
  application/json`, `User-Agent: jadpo`, without a version, and
  `Connection: close`: the client is a small HTTP/1.1 one of `jadpo`'s
  own. The token in the body is the only identifier the client sends. The
  client logs nothing about a request: no repository name, user, machine
  or address.
- **`POST /v1/suggestions/hashes`** sends at most 500 entries; `share`
  sends more in several requests.

  ```json
  {
    "token": "5f0c2a9e7b1d4c38a6e2f9b0d7c4a1e3",
    "entries": [
      { "hash": "8c1d0e5a4f3b2a19", "kind": "ignored", "signal": "conversion", "suggestion": "Money" }
    ]
  }
  ```

  `hash` is the text hash (16 hex digits of BLAKE3 of the snippet's text
  after `jadpo fmt`, comments removed). `kind` is `found`, `accepted` or
  `ignored`. `signal` is a signal's word (`taught` for a `found` entry).
  `suggestion` is a standard type or operation. HQ answers `200` with the
  entries whose text it wants, each a hash and kind it was sent, so an
  ignored entry's reason can be asked for without the accepted ones'
  texts. It names at most 20, so an approval stays readable:

  ```json
  { "wanted": [ { "hash": "8c1d0e5a4f3b2a19", "kind": "ignored" } ] }
  ```

  For each hash, kind, signal and suggestion, HQ counts the distinct
  tokens it has seen it from, so sending the same entry twice changes
  nothing and a retry is safe. It wants a hash's text once that count
  reaches its threshold and it doesn't hold the text yet; for an ignored
  entry, until it holds a few reasons. A hash below the threshold is never
  asked for.
- **`POST /v1/suggestions/snippets`**, only after approval, carries no
  token: a snippet is never tied to a project.

  ```json
  { "snippets": [ { "kind": "ignored", "reason": "Basis points, not money", "signal": "conversion", "suggestion": "Money", "text": "total_price * 100" } ] }
  ```

  `text` is the snippet's text without comments, whose hash is the one HQ
  asked for. `reason` is sent only for an ignored snippet. HQ recomputes
  each hash, keeps the snippets it asked for and refuses the rest,
  answering `200` with `{ "received": 1, "refused": 0 }`.
- **Errors.** Any other status has the body
  `{ "error": { "code": "…", "message": "…" } }`: `400 invalid_request`
  (with the field), `413 too_large` (a body over 1 MiB), `410
  version_retired` (this Jadpo is too old to share; update it), `429
  rate_limited` with `Retry-After` in seconds, and `500`, `502`, `503` or
  `504` for HQ's own trouble.
- **Timeouts and retries.** 5 seconds to connect and 15 for each answer.
  A failed connection, a timeout, `429`, and `500`, `502`, `503` and `504`
  are tried again up to three times: after half a second, 1 and 2 seconds
  (at most three and a half seconds of waiting), or after `Retry-After`
  when HQ gives one. A `Retry-After` over 60 seconds ends the attempt.
  Nothing else is retried. When the last try fails, `share` fails with
  `suggest.share_failed`, sends nothing more and keeps the pending entries
  for the next run.
- **What HQ keeps.** For each hash, the set of tokens it came from and its
  kinds, signals and suggestions; and the texts and reasons it asked for,
  without a token. Nothing else: addresses are used only to rate-limit, in
  memory, and never written down.

## HQ's agent

HQ runs an agent over what is shared. It turns shared patterns into signal
data for the next release: new constants, words and validation texts on a
type's entry, a threshold, the tests each new signal is generated with,
and the release notes saying what changed.

It reassesses a signal that is ignored often across distinct projects. It
reads the ignored snippets and their reasons beside the accepted ones,
then narrows the signal or removes it, tested against the accepted
snippets so it keeps catching what it got right. Where a pattern points at
something the language lacks, it proposes a language feature for a person
to decide on, and never builds one.

Not built yet: Jadpo HQ itself, which comes later, with the registry.
Sharing is built against a local fake of HQ that speaks the protocol
above. `jadpo new` writes the project token; for a project without one,
`share` writes it.
