# SCHEME.md - @playform/hook-dsh-edit-suggester (v0.2.1, TypeScript, function plugin)

Status: **IMPLEMENTED (PoC phase plus the D8 learning layer)**, because the Source is complete, the
build and the five smoke folders pass, and the decision-record, follow-through, and learned-rate
layer of section 3.2 has landed, while the ETS twin is deferred per section 8 of POC-BRIEF.md.

## 0. What this plugin is

This is the modification-intent **companion suggester**: when a `write`/`edit`/`raw-write` call
lands on a path that matches one of the seven convention families, the plugin prepends ONE labeled
`additionalContexts` note on the `tools/post-execute` waterfall and names the standard companions
that the model has not touched yet.

The rules are deterministic path rules, no LLM runs, the gate is hard, the posture is fail-open, and
the plugin is silent by default because it ships `enabled: false`.

It is the second consumer of the pattern algebra of the search-suggester and the two-consumer
extraction trigger.

The source of truth is `Documentation/Plans/SEARCH-SUGGESTER-PLUGIN.md`, sections 1.4 and 8 plus the
shared algebra of section 3.

## 1. The surfaces

| Surface              | Mode                          | What it does                                                                              |
| -------------------- | ----------------------------- | ----------------------------------------------------------------------------------------- |
| `tools/post-execute` | waterfall, plain registration | the note: `next()` first, then detect, gate, and prepend one `additionalContexts` message |
| `agent/pre-step`     | plain                         | the per-turn reset (touched paths, cooldowns, dedupe, counters)                           |
| `tools/pre-execute`  | NOT used                      | a suggestion cannot rewrite arguments; a deny would be a coach, not a suggester           |

## 2. The config schema (Variable/Config.ts)

| Field             | Type     | Default                                                                   | Volatile | Meaning                                                              |
| ----------------- | -------- | ------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------- |
| `log`             | boolean  | `true`                                                                    | yes      | Ledger enabled                                                       |
| `logFile`         | string   | `~/.dsh/playform/hook-dsh-edit-suggester.log`                             | yes      | The suggester's own ledger (one log per module)                      |
| `enabled`         | boolean  | `true` (patch ships `false`)                                              | yes      | The master switch                                                    |
| `threshold`       | number   | `0.75`                                                                    | yes      | The default confidence gate (Version: 0.85 in the pack)              |
| `tools`           | string[] | `["write","edit","raw-write"]`                                            | yes      | The judged modification tools                                        |
| `maxCompanions`   | number   | `7`                                                                       | yes      | Maximum companions in one note                                       |
| `maxNoteChars`    | number   | `1200`                                                                    | yes      | Hard character cap on the note text                                  |
| `cooldownCalls`   | number   | `6`                                                                       | yes      | No second note for the same family within this many judged calls     |
| `maxNotesPerTurn` | number   | `3`                                                                       | yes      | Maximum notes per turn, per agent                                    |
| `exclude`         | string[] | `["node_modules","Target",".git","dist"]`                                 | yes      | The excluded directories                                             |
| `learn`           | boolean  | `true`                                                                    | yes      | The learning layer's master switch (false = the pure PoC posture)    |
| `learnWindow`     | number   | `3`                                                                       | yes      | The follow-through window, in judged calls (no timers)               |
| `heuristics`      | string[] | the six names (`scope`,`graph`,`prior`,`literal`,`history`,`accept`)      | yes      | The ENABLED pickable heuristics; a name left out is dropped          |
| `weights`         | object   | `{scope:0.35, graph:0, prior:0.25, literal:0.25, history:0.15, accept:0}` | yes      | The pickable weight table (a missing name falls back to the default) |
| `storeLimit`      | number   | `512`                                                                     | yes      | The in-memory fallback's bound (rate rows and decision ring)         |

## 3. The ledger (byte-stable strings, Function-owned)

```
edit-suggester: disabled (enabled=false)
edit-suggester: activated (bundle 0.2.1, families 7, threshold 0.75)
edit-suggester: suggest <Family> <k> <n> companions <edited path>
edit-suggester: decided <Family> <k> <n> companions <head path> <values> weights <weights> store=<pattern|memory>
edit-suggester: accepted <Family> <companion>
edit-suggester: rejected <Family> <n> companions
edit-suggester: abstained low-confidence <Family> <k>
edit-suggester: abstained ambiguous-target <Family> <k>
edit-suggester: abstained already-touched <Family> <k>
edit-suggester: abstained no-companions <Family> <k>
edit-suggester: abstained no-signal <Family> <k>
edit-suggester: abstained foreign-workspace <Family> <k>
edit-suggester: suppressed cooldown <Family>
edit-suggester: suppressed dedupe <Family>
edit-suggester: suppressed cap <Family>
```

The activation line uses the `Activate` composer of the core and the field list of the module, and
every other string is composed in Function/Apply, with the `decided` line in Function/Decided and
the outcome lines in Function/Apply.

`<values>` and `<weights>` are the canonical `name=value` tables of Function/Table
(`scope=1.00,graph=0.50,prior=1.00,literal=0.50,history=0.33,accept=0.33`), and the head is the
top-ranked companion, whose row the values describe.

The post-execute listener resolves the workspace root before the score: the root is the nearest
ancestor whose PACK SIGNATURE matches, where the signature is the `signature` list of the resolved
pack, defaulting to `Documentation/ARCHIVE-STYLE.md`, taken from the call path plus the session cwd.

A foreign workspace has no pack and logs `abstained foreign-workspace  0.00`, because the gate runs
before the family is known and the family segment is empty with a no-signal kappa of 0.00.

The `edit/` families of the resolved pack (`Families(pack, "edit")`) drive Detect, so a repo pack
overrides the built-in families without a restart.

With the root known, a companion is kept when it exists under ANY of the three bases of the call,
which are the repo root, the package root (the nearest ancestor of the resolved call path carrying
`package.json`, at or below the repo root), and the package parent, so repo-relative,
package-relative, and sibling-relative pack paths all survive.

A winning family with none left logs `abstained no-companions <Family> <k>`.

The note text, the relative companion paths, and the cooldown, dedupe, and cap behavior are
unchanged, because the filter only drops a target and never rewrites one.

## 3.2 The learning layer (D8)

### The decision record (Interface/Decision)

Every emitted note, and every gated silence worth learning from (`low-confidence`,
`already-touched`), records ONE decision:

```ts
{
  family: "Version",
  kappa: 0.87,
  companions: [{ path: "CHANGELOG.md", score: 0.775,
                 heuristics: { scope: 1, graph: 0.5, prior: 1, literal: 0.5,
                               history: 0.3333, accept: 0.3333 } }, ...],
  weights: { scope: 0.35, graph: 0, prior: 0.25, literal: 0.25, history: 0.15, accept: 0 },
  enabled: ["scope", "graph", "prior", "literal", "history", "accept"],
  outcome: "pending",            // then "accepted" or "rejected"
  store: "pattern",              // or "memory" (the bounded fallback)
}
```

`Function/Decide` builds the record from the rows that `Function/Measure` already scored, where a
gated silence measures the would-be companions of the family through `Function/Would` on the same
leaf, and the record is logged as the `decided` line and stored through `Function/Store`.

The store is the optional `pattern` service (`ctx.get("pattern")`, probed per use and never
injected) when it is present, and the bounded memory ring otherwise.

The service projection maps `intent` to the family, `targets` to the companion paths, `arbiter` to
the outcome, and `confidence` to the kappa, and the `transform` lines carry `weights <table>`,
`enabled <names>`, `outcome <verdict>`, and one `<path> <table>` line per companion.

The store keeps the record ring in memory too, so `Learning.Resolve` can fill the outcome
placeholder when the follow-through closes.

### The follow-through (the outcome signal)

An emitted note opens a window (`Ring.Open`) that holds the family and the companion paths, and
every judged IN-WORKSPACE call runs `Ring.Follow`/`Ring.Age`:

- if the call path names a companion, which `Function/Match` decides glob-aware, the call is an
  **accept**: the companion and its family move by the accept bonus of the service
  (`accepted <Family> <path>`), the window closes, and the outcome of the decision record becomes
  `accepted`;
- otherwise the window ages by one call, and the call that reaches `learnWindow` closes it untouched
  as a **soft reject**: every named companion and the family move by the reject penalty
  (`rejected <Family> <n> companions`), and the outcome becomes `rejected`.

No timers and no clock run, because the window is a counter of judged calls, and the per-turn reset
forgets an open note without a verdict, because a turn boundary is not a judged call.

### The learned rate and the score

`Function/Store` answers the rates from the `confidence` of a record in the service or the row of
the fallback in memory, both starting at the neutral 1/3 and moving by +0.1 on accept and -0.2 on
reject, clamped to 0..1.

The score is

```
score(c) = sum over the ENABLED heuristics of weight(h) * value(h)
```

| Heuristic | Value                                               | Default weight |
| --------- | --------------------------------------------------- | -------------- |
| `scope`   | 1.00 (the call matched the family's path rules)     | 0.35           |
| `graph`   | 0.50 (the neutral graph-distance placeholder)       | 0.00           |
| `prior`   | the pack's prior                                    | 0.25           |
| `literal` | 0.50 (the neutral literal)                          | 0.25           |
| `history` | the learned per-companion accept rate (neutral 1/3) | 0.15           |
| `accept`  | the learned per-family accept rate (neutral 1/3)    | 0.00           |

The default row reproduces the PoC formula exactly (`0.35*1 + 0.25*prior + 0.25*0.5 + 0.15*(1/3)`),
and a missing store answers the neutral 1/3, so the default behavior is unchanged byte-for-byte.

The `heuristics` of the config (the enabled names) and the `weights` of the config (the table) make
every term pickable: a disabled name is dropped from the sum with an effective weight of 0, and the
decision record keeps the enabled list, so the choice is auditable.

## 4. The architecture

```
Source/
  Library.ts            the entry: name, apply, Config, inject (pluginFactory)
  Function/
    Apply.ts            the wiring: the State build, the activation, the 2 listeners
    Pack.ts             the edit projection of the built-in pack (@playform/lib-dsh-pack):
                        7 families, path rules, companions (thin projection, no data)
    Detect.ts           the deterministic path classifier, pure, no LLM
    Measure.ts          one companion -> the six pickable values + the weighted score
    Would.ts            a gated silence's would-be companion rows (the same Measure)
    Score.ts            k = sigmoid(sum - 0.6); the pickable companion scoring; the gate
    Weights.ts          the enabled list + the weight table -> the EFFECTIVE table
    Decide.ts           the decision record: build it and hand it to the learned layer
    Decided.ts          the `decided` ledger line
    Table.ts            the canonical `name=value` heuristic table formatter
    Store.ts            the learned layer: the pattern service or the bounded memory fallback
    Match.ts            companion <-> call path (the follow-through's lookup)
    Note.ts             the labeled additionalContexts message + the 120-char summary
    Ring.ts             the per-agent state: touched paths, cooldowns, dedupe, cap, the open note
  Interface/
    Config.ts           the validated config shape
    Decision.ts         the decision record and its heuristic values
    Execution.ts        the family/detection/verdict types
    Factory.ts          the factory service's structural view + the Context augmentation
    Learning.ts         the learned layer's view (rate/move/remember/resolve)
    State.ts            the per-apply state
  Variable/
    Config.ts           the Schemastery schema
    Heuristics.ts       the six pickable heuristic names
    Weights.ts          the default weight table (the schema default's one source)
```

## 5. The end-phase

The smoke folders are `Post-Execute-And-Ring`, `Wiring-And-Ledger`, `Workspace-Gate`, `Pack-Driven`,
and `Learning-Loop`, which covers the decision records, the follow-through, the learned rate, the
pickable heuristics, and the storage fallback in 12 checks.

The learned layer of the end-phase list in the plan has landed in section 3.2, and what remains is
the replay benchmark on the recorded lanes and the ETS twin
(`@playform/ets-hook-dsh-edit-suggester`), which is DEFERRED for this wave under the ETS deferral of
the user in section 8 of POC-BRIEF.md.
