<!-- single source of truth: scripts/okstra_ctl/report_views.py serialize_user_response -->
<!-- byte-identical client implementation: templates/reports/report.js buildUserResponseMarkdown -->

# User-Response Sidecar Template

This file defines the standard format of the markdown produced by the **Export user response** button in the final-report HTML. The output is saved to the path `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md`. The original final-report MD is never merged under any circumstances.

## Frontmatter Schema

```yaml
task-key: <task-group>/<task-id>
task-type: <requirements-discovery | error-analysis | implementation-option-selection | implementation-planning | implementation | final-verification | release-handoff | improvement-discovery | project-analysis | feature-analysis | change-impact-analysis>
seq: <3-digit zero-padded run sequence>
source-report: <project-relative path to the final-report .md the HTML was derived from>
source-data: <project-relative path to the final-report data.json; omit for legacy reports without one>
source-data-sha256: <SHA-256 of the exact source-data bytes; omit when source-data is omitted>
created-by: user
created-at: <ISO 8601 UTC timestamp>
```

## Body Schema

The body separates each response with a `## <Response ID>` heading under a single `# User Response` H1 heading. The Response ID quotes the `ID` column (`C-NNN`) of the final-report `## 1. Clarification Items` table verbatim.

The schema of each response block:

```markdown
## <Response ID>
- Kind: <material | decision | data-point>
- Disposition: <answer | reframe | select | accept-risk | request-revision | reject>   # omit the line when answer (default)
- Value:
  > <multi-line value, trimmed>
- Rationale: <optional one-line rationale>
```

`answer`, `select`, `accept-risk`, `request-revision`, and `reject` count as user answers when `Value` is non-empty. `reframe` alone does not count as an answer; it defers the answer and asks the next run to present the row again.

An empty response set (when the user presses Export without filling in any row) outputs the following single line in the body:

```markdown
_(No user responses recorded.)_
```

## Example

```markdown
---
task-key: demo/T-1
task-type: implementation-planning
seq: 003
source-report: runs/implementation-planning/reports/final-report-implementation-planning-003.md
created-by: user
created-at: 2026-05-17T10:00:00Z
---

# User Response

## C-001
- Kind: decision
- Value:
  > (a) One-off. No recurrence.
- Rationale: Rechecking the payment logs showed no recurrence of the same pattern.

## C-003
- Kind: data-point
- Value:
  > (prediction=0: 1,204) (prediction=1: 38)
```

## PLAN DECISION block (implementation-planning only)

When you pick a verdict in the Plan Decision widget and Export, the following block is appended to the end of the body:

```markdown
## PLAN DECISION
- Status: <approved | revision-requested | rejected>
- Implementation-Option: <legacy plans only: the name exactly as in Option Candidates>
- Reason:
  > <one quoted line per input line>
```

- `revision-requested` and `rejected` require a reason: a plan sent back without one leaves the next run guessing at what to change, so Export refuses to serialise it.
- A `planningContract: selected-direction` plan never emits `Implementation-Option:`. The direction was confirmed before planning, and this block decides only whether the detailed plan is approved, revised, or rejected.
- A legacy candidate plan emits `Implementation-Option:` only for `approved`, and only when you moved off the recommended default. Otherwise implementation falls back to the legacy plan's Recommended Option.
- `Reason:` is optional for `approved`, and the line is omitted when empty.
- The consumer of an `approved` block is the approve-confirm step of the implementation start wizard (`scripts/okstra_ctl/wizard.py`). After user confirmation the wizard applies `--approve`; it applies `--implementation-option` only for a legacy plan. The sidecar itself does not bypass approval-gate validation. A non-approved status is ignored there — that step only ever looks for an approval.
- The parser is `parse_plan_decision` in `scripts/okstra_ctl/user_response.py`, and it accepts only the lowercase statuses that are byte-identical to the producer output (hand-edited values such as `Approved` are rejected fail-closed).
- `--resume-clarification` attaches the sidecar verbatim, so the PLAN DECISION block reaches the next planning run — which is how a rejection and its reason get acted on.

## ANALYSIS REVIEW block (analysis reports only)

The Analysis Review control appends one decision block. `revision-requested` and `rejected` require at least one affected structured analysis ID and a reason. Optional empty values keep their field lines but do not emit an empty quote line.

```markdown
## ANALYSIS REVIEW
- Status: <accepted | revision-requested | rejected>
- Affected-IDs: <comma-separated structured analysis IDs>
- Reason:
  > <one quoted line per input line>
- Additional-Evidence:
  > <optional evidence>
- Requested-Scope-Change:
  > <optional scope change>
```

The block is the review decision's sole storage location. Export never changes the source final-report markdown. A later analysis rerun receives the sidecar through `--clarification-response` and records one `analysisReviewResolution` row for each imported affected ID.

## DIRECTION SELECTION block (implementation-option-selection comparison only)

Selecting one ranked direction and checking `Confirmed` appends this block. `Selection-Note` and `Constraints` keep their field lines when empty, but do not emit empty quote lines.

```markdown
## DIRECTION SELECTION
- Status: selected
- Option-ID: IO-002
- Option-Name: Adapter boundary
- Confirmed: true
- Selection-Note:
  > Use the existing port.
- Constraints:
  > Keep PB-001 unchanged.
```

The parser accepts exactly one block, lowercase `selected`, and lowercase `true`. A `preselected-validation` report displays its confirmed upstream direction read-only and does not export a new selection block.

## Compatibility Rules

- If `Kind` has an unknown value, the form renders with a `<textarea>` fallback, and the received `Kind` string is preserved as-is during serialization.
- Rows whose `Status` is `resolved` or `obsolete` render `disabled` in the HTML form and are automatically excluded from the Export result. To reopen such a row, revert the final-report `Status` to `open` / `answered` and regenerate the report.
- The output of the Python (`serialize_user_response` in `scripts/okstra_ctl/report_views.py`) and the JavaScript (`buildUserResponseMarkdown` in `templates/reports/report.js`) MUST be **byte-identical**. `tests/test_report_views.py` verifies the equivalence of the two implementations.
