Skip to content

Commit 73ba800

Browse files
feat: Synthesis Engine (C1+C2), Idris2 contract + validation boundary (C3), research mode, dedup wired (#51)
## Summary Builds the Synthesis Engine (pipeline contracts C1+C2), the honest C3 validation boundary (real Idris2 contract spec + strict Elixir runtime enforcement), wires the previously-inert Deduplicator, authors the repo's issue forms, and retires the honesty debt. Closes the core-pipeline gap tracked in hyperpolymath/boj-server#274 via hyperpolymath/boj-server#282 and hyperpolymath/boj-server#283. **Design intent** (now stated in the README's Goal section): fluid for the sender — raw feedback from any front door (MCP stdio/TCP, HTTP intake, CLI, boj cartridge); shaped for the receiver — their own issue template, hydrated and validated; intent-preserving — the gate is usefulness, not tone (zero-signal hostility rejected with a stated reason and audit-logged, never filed; mixed content salvaged with the stripping reported; useless-but-genuine feedback returned as open questions); foundationally sorted — duplicates and recurring themes recognized via forge search + the now-live local dedup index. ## Changes - **Synthesis Engine** (`elixir-mcp/lib/feedback_a_tron/synthesis/`): `TemplateFetcher` + 15-min `TemplateCache` (C1, engine-side — decision recorded in the pipeline doc: every front door benefits, not just the boj path); `IntentClassifier` (intent taxonomy + signals + the usefulness doctrine); `Hydrator` (context/system-state fill with provenance marks, optional NetworkVerifier preflight — wiring that island at last); `FormRenderer`; `FormValidator` (strict, fail-closed); `Research` (forge duplicate search scored by title similarity + local dedup recurrence + template questions); `Synthesizer` (orchestration; draft + open_questions back to the caller — agent-in-the-loop, no embedded LLM). - **Tools on every front door**: new MCP tools `research_feedback` and `synthesize_feedback` + matching HTTP routes; `submit_feedback` extended with `template`/`template_data` (validated against the fetched form schema before dispatch; violations → 422, template unavailable → 502). - **Deduplicator wired**: successful real submissions are recorded (dry runs never); `TextSimilarity` extracted and the "Jaro-Winkler" mislabel corrected (it's normalized Levenshtein). - **Real Idris2 contract (C3)**: `src/abi/FeedbackOTron/Contract.idr` — `%default total`, zero `believe_me`/`postulate`; `ValidPayload` unconstructable outside `validate`; `validCompleteness` proved. CI gate `.github/workflows/proofs.yml`. `ABI-FFI-README.md` rewritten truthfully (Zig FFI is a stub and says so; full FFI enforcement tracked in #49). - **Issue forms**: `.github/ISSUE_TEMPLATE/{bug,feature,config}.yml` (C1 prerequisite + dogfood). - **Honesty debt retired**: README badges, TOPOLOGY.md, STATE.a2ml, ROADMAP.adoc, EXPLAINME.adoc, and the pipeline doc's reality table all reconciled to what actually exists. - **En-route fixes**: `submit_batch` no longer crashes on `{:error, _}` replies; audit events no longer kill AuditLog on non-JSON-safe payloads. ## Testing - `mix test`: **272 tests, 0 failures** (new suites for every synthesis module + extended router/submitter/MCP-server tests; Bypass-served templates, no live network). - `idris2 --check` clean on `src/abi/feedback-o-tron.ipkg` (Idris2 0.7.0). - Live loop exercised against a running engine (`FEEDBACK_A_TRON_HTTP=1`): bug text → draft + open questions; zero-signal hostility → `{"rejected": true, "reason": …}`; mixed content → salvaged core with `stripped_reason`; `template_data` with unfetchable template → fail-closed 502; missing fields → 400. Companion PRs: cartridge v0.2 in `boj-server-cartridges`, bundling/menu wiring in `boj-server`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017LDhR8gtnittEWSafunnrq --- _Generated by [Claude Code](https://claude.ai/code/session_017LDhR8gtnittEWSafunnrq)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 321a372 commit 73ba800

75 files changed

Lines changed: 6563 additions & 1155 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/ISSUE_TEMPLATE/bug.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
name: "Bug report"
3+
description: "Report something that is broken or behaving unexpectedly."
4+
labels: ["bug"]
5+
body:
6+
- type: textarea
7+
id: what-happened
8+
attributes:
9+
label: "What happened?"
10+
description: "Describe the problem. What did you see?"
11+
validations:
12+
required: true
13+
- type: textarea
14+
id: repro-steps
15+
attributes:
16+
label: "Steps to reproduce"
17+
description: "Exact steps that trigger the problem, one per line."
18+
placeholder: "1. ...\n2. ...\n3. ..."
19+
validations:
20+
required: true
21+
- type: textarea
22+
id: expected
23+
attributes:
24+
label: "Expected behaviour"
25+
description: "What did you expect to happen instead?"
26+
- type: input
27+
id: version
28+
attributes:
29+
label: "Version"
30+
placeholder: "e.g. 1.0.0 / commit SHA"
31+
validations:
32+
required: true
33+
- type: dropdown
34+
id: component
35+
attributes:
36+
label: "Component"
37+
description: "Which part of feedback-o-tron is affected?"
38+
options:
39+
- "MCP server"
40+
- "HTTP intake"
41+
- "Synthesis"
42+
- "Channels/dispatch"
43+
- "CLI"
44+
- "Other"
45+
- type: textarea
46+
id: logs
47+
attributes:
48+
label: "Relevant logs / stack trace"
49+
description: "Paste any logs or stack traces. This block is rendered as shell, so no backticks needed."
50+
render: shell
51+
- type: textarea
52+
id: environment
53+
attributes:
54+
label: "Environment"
55+
description: "OS, OTP/Elixir versions, network specifics"

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
blank_issues_enabled: true

.github/ISSUE_TEMPLATE/feature.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
name: "Feature request"
3+
description: "Suggest an improvement or new capability."
4+
labels: ["enhancement"]
5+
body:
6+
- type: textarea
7+
id: problem
8+
attributes:
9+
label: "Problem"
10+
description: "What problem are you trying to solve? What is painful or missing today?"
11+
validations:
12+
required: true
13+
- type: textarea
14+
id: proposed-solution
15+
attributes:
16+
label: "Proposed solution"
17+
description: "What would you like to happen?"
18+
validations:
19+
required: true
20+
- type: textarea
21+
id: alternatives
22+
attributes:
23+
label: "Alternatives considered"
24+
description: "Other approaches or workarounds you have considered."
25+
- type: input
26+
id: scope
27+
attributes:
28+
label: "Scope"
29+
placeholder: "e.g. MCP server, HTTP intake, CLI"

.github/workflows/elixir-ci.yml

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,24 @@ jobs:
3434
- name: Detect relevant changes
3535
id: detect
3636
working-directory: ${{ github.workspace }}
37-
env:
38-
GH_TOKEN: ${{ github.token }}
3937
run: |
4038
set -euo pipefail
4139
PATTERN='^elixir-mcp/'
40+
# Fail-safe detector (estate required-gate convention): default to
41+
# running the suite; skip ONLY when a successful diff against the
42+
# base tip shows nothing relevant. Uses git (two-dot tree diff, so
43+
# shallow checkouts work) instead of `gh api`, whose non-JSON error
44+
# responses hard-failed this step (#51).
4245
if [ "${{ github.event_name }}" = "pull_request" ]; then
43-
FILES=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" --paginate --jq '.[].filename')
44-
if printf '%s\n' "$FILES" | grep -qE "$PATTERN"; then
45-
echo "relevant=true" >> "$GITHUB_OUTPUT"
46+
if git fetch --no-tags --depth=1 origin "${{ github.base_ref }}" \
47+
&& FILES=$(git diff --name-only FETCH_HEAD HEAD); then
48+
if printf '%s\n' "$FILES" | grep -qE "$PATTERN"; then
49+
echo "relevant=true" >> "$GITHUB_OUTPUT"
50+
else
51+
echo "relevant=false" >> "$GITHUB_OUTPUT"; echo "No elixir-mcp changes — pass-through (required-check shim)."
52+
fi
4653
else
47-
echo "relevant=false" >> "$GITHUB_OUTPUT"; echo "No elixir-mcp changes — pass-through (required-check shim)."
54+
echo "relevant=true" >> "$GITHUB_OUTPUT"; echo "Detector could not compute the diff — failing safe (run=true)."
4855
fi
4956
else
5057
echo "relevant=true" >> "$GITHUB_OUTPUT"
@@ -92,17 +99,24 @@ jobs:
9299
- name: Detect relevant changes
93100
id: detect
94101
working-directory: ${{ github.workspace }}
95-
env:
96-
GH_TOKEN: ${{ github.token }}
97102
run: |
98103
set -euo pipefail
99104
PATTERN='^elixir-mcp/'
105+
# Fail-safe detector (estate required-gate convention): default to
106+
# running the suite; skip ONLY when a successful diff against the
107+
# base tip shows nothing relevant. Uses git (two-dot tree diff, so
108+
# shallow checkouts work) instead of `gh api`, whose non-JSON error
109+
# responses hard-failed this step (#51).
100110
if [ "${{ github.event_name }}" = "pull_request" ]; then
101-
FILES=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" --paginate --jq '.[].filename')
102-
if printf '%s\n' "$FILES" | grep -qE "$PATTERN"; then
103-
echo "relevant=true" >> "$GITHUB_OUTPUT"
111+
if git fetch --no-tags --depth=1 origin "${{ github.base_ref }}" \
112+
&& FILES=$(git diff --name-only FETCH_HEAD HEAD); then
113+
if printf '%s\n' "$FILES" | grep -qE "$PATTERN"; then
114+
echo "relevant=true" >> "$GITHUB_OUTPUT"
115+
else
116+
echo "relevant=false" >> "$GITHUB_OUTPUT"; echo "No elixir-mcp changes — pass-through (required-check shim)."
117+
fi
104118
else
105-
echo "relevant=false" >> "$GITHUB_OUTPUT"; echo "No elixir-mcp changes — pass-through (required-check shim)."
119+
echo "relevant=true" >> "$GITHUB_OUTPUT"; echo "Detector could not compute the diff — failing safe (run=true)."
106120
fi
107121
else
108122
echo "relevant=true" >> "$GITHUB_OUTPUT"

.github/workflows/proofs.yml

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
#
4+
# Proofs Gate — type-check the Idris2 contract layer (src/abi/) and enforce
5+
# the empty trusted base (no believe_me / postulate / assert_total).
6+
#
7+
# FeedbackOTron.Contract is the verified CONTRACT SPEC for the Elixir
8+
# FormValidator (see ABI-FFI-README.md). This gate keeps it honestly
9+
# compiling: every push/PR touching src/abi/** re-checks the package under
10+
# a pinned Idris2 toolchain (same asdf+Chez install pattern as boj-server's
11+
# proofs gate).
12+
#
13+
# NOTE: this workflow is path-filtered because it is NOT a required status
14+
# check. If it is ever promoted to required, the `on.*.paths` filters MUST
15+
# be removed and replaced with an always-run `changes` detector job (see
16+
# boj-server .github/workflows/proofs.yml and the estate CI conventions:
17+
# a path-filtered required check that never triggers reports as permanently
18+
# "Expected" and deadlocks the PR).
19+
name: Proofs Gate
20+
21+
on:
22+
push:
23+
branches: [main]
24+
paths:
25+
- 'src/abi/**'
26+
- '.github/workflows/proofs.yml'
27+
pull_request:
28+
branches: [main]
29+
paths:
30+
- 'src/abi/**'
31+
- '.github/workflows/proofs.yml'
32+
workflow_dispatch:
33+
34+
permissions:
35+
contents: read
36+
37+
concurrency:
38+
group: proofs-${{ github.ref }}
39+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
40+
41+
env:
42+
IDRIS2_VERSION: '0.7.0'
43+
44+
jobs:
45+
# Cheap, dependency-free gate: the contract layer claims an EMPTY trusted
46+
# base — no believe_me, no postulate, no assert_total anywhere in src/abi.
47+
trusted-base:
48+
name: Trusted-base audit (empty — no axioms)
49+
runs-on: ubuntu-latest
50+
timeout-minutes: 5
51+
steps:
52+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
53+
- name: Enforce empty trusted base
54+
run: |
55+
set -euo pipefail
56+
found=0
57+
while IFS= read -r -d '' f; do
58+
# Strip `--` and `|||` comments so documentation may mention the
59+
# banned constructs without tripping the audit.
60+
if sed -e 's/--.*$//' -e 's/|||.*$//' "$f" \
61+
| grep -nE 'believe_me|postulate|assert_total'; then
62+
echo "::error file=$f::Unsound construct in $f — the contract layer must stay axiom-free."
63+
found=1
64+
fi
65+
done < <(find src/abi -name '*.idr' -print0)
66+
if [ "$found" -eq 0 ]; then echo "Trusted base is empty."; fi
67+
exit "$found"
68+
69+
typecheck:
70+
name: Idris2 type-check (contract layer)
71+
runs-on: ubuntu-latest
72+
timeout-minutes: 45
73+
steps:
74+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
75+
76+
- name: Install build deps (Chez Scheme + GMP)
77+
run: sudo apt-get update && sudo apt-get install -y chezscheme libgmp-dev build-essential
78+
79+
- name: Cache asdf + Idris2 toolchain
80+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
81+
with:
82+
path: ~/.asdf
83+
key: idris2-asdf-${{ runner.os }}-${{ env.IDRIS2_VERSION }}
84+
85+
- name: Install asdf + Idris2 ${{ env.IDRIS2_VERSION }}
86+
env:
87+
# Ubuntu's chezscheme package installs the interpreter as `scheme`;
88+
# Idris2's bootstrap Makefile reads $SCHEME (defaults to `chez`).
89+
SCHEME: scheme
90+
run: |
91+
set -euo pipefail
92+
if [ ! -f "$HOME/.asdf/asdf.sh" ]; then
93+
git clone --depth 1 --branch v0.14.0 https://github.com/asdf-vm/asdf.git "$HOME/.asdf"
94+
fi
95+
. "$HOME/.asdf/asdf.sh"
96+
asdf plugin add idris2 || true
97+
asdf install idris2 "$IDRIS2_VERSION"
98+
asdf global idris2 "$IDRIS2_VERSION"
99+
100+
- name: Put Idris2 on PATH
101+
run: echo "$HOME/.asdf/shims" >> "$GITHUB_PATH"
102+
103+
- name: Idris2 version
104+
run: idris2 --version
105+
106+
- name: Type-check the contract package
107+
working-directory: src/abi
108+
run: idris2 --typecheck feedback-o-tron.ipkg

.machine_readable/6a2/STATE.a2ml

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,34 @@
11
# SPDX-License-Identifier: MPL-2.0
22
# STATE.a2ml — Project state checkpoint
33
# Converted from STATE.scm on 2026-03-15
4-
# Updated 2026-04-09
4+
# Updated 2026-07-09
55

66
[metadata]
77
project = "feedback-o-tron"
8-
version = "1.0.0"
9-
last-updated = "2026-04-09"
10-
status = "production"
8+
version = "1.2.0-dev"
9+
last-updated = "2026-07-09"
10+
status = "active-development"
1111

1212
[project-context]
1313
name = "feedback-o-tron"
14-
completion-percentage = 95
15-
phase = "Production"
14+
completion-percentage = 75
15+
phase = "Synthesis Engine + honest reconciliation"
1616

1717
[components]
1818
cli = "100%"
19-
mcp-server = "100%"
19+
mcp-server = "100% (research_feedback, synthesize_feedback, submit_feedback, migration_observe)"
20+
http-intake = "done (Bandit :7722, localhost-only, off by default; boj bug-filing-mcp wire)"
21+
synthesis-engine = "done (this PR: TemplateFetcher, TemplateCache, IntentClassifier, Hydrator, Research, Synthesizer, FormValidator, FormRenderer, TextSimilarity)"
2022
channels-github = "100%"
21-
channels-gitlab = "100%"
22-
channels-bitbucket = "100%"
23-
channels-codeberg = "100%"
23+
channels-gitlab = "90%"
24+
channels-bitbucket = "90%"
25+
channels-codeberg = "90%"
2426
channels-bugzilla = "100%"
2527
channels-email = "80%"
26-
deduplication = "100%"
28+
deduplication = "wired (record/3 called on successful submission; check/1 consulted pre-dispatch and by Research)"
2729
audit-logging = "100%"
28-
network-verifier = "90%"
30+
network-verifier = "70% (real probing; opt-in via Hydrator network_probe; BGP/RPKI partial)"
2931
rate-limiter = "100%"
3032
retry-backoff = "100%"
31-
zig-ffi = "95%"
32-
idris2-proofs = "80%"
33+
zig-ffi = "stub"
34+
idris2 = "contract spec only (Contract.idr), no FFI enforcement"

0 commit comments

Comments
 (0)