ADR-0028: Copilot Suggestions — seams suggest Asks a Provider Command for a Model Change and Applies It Only on Accept
Status: Accepted
Date: 2026-11-04
Source: milestone M025/S01; backlog R2; decision D061; research note A data-centric model, SemPKM as its store, and a model copilot section 5; builds on ADR-0020 (the provider command) and ADR-0017 (the one write path)
Context
R2 asks an LLM to propose improvements to the model itself, not to code: move logic into DMN, merge shapes into a class, anchor classes in gist. Each proposal must be validated, regenerated and reviewed before it changes the model. C4 (ADR-0020) already calls an LLM through a provider command and keeps a draft only when its acceptance tests pass. The research note left open whether the model copilot should reuse that connection or SemPKM's AI copilot, which talks to OpenAI-compatible endpoints.
A model change is riskier than a handler draft: it touches the TriG seams graph and the native BPMN and DMN payloads, which every generator reads. An LLM writing those bytes directly could break IRIs (ADR-0002), the seams graph (ADR-0001) or a modeler's XML layout, and its output would not be repeatable.
Decision
- The provider is ADR-0020's command (D061).
seams suggestruns the command named by[propose] providerinseams.toml, or by--provider, through the same helper asseams propose: prompt on stdin, answer on stdout, no shell, no SDK, no key, no network code in the repository. SemPKM's copilot connection is not used; it would make a model-authoring tool depend on a running SemPKM. A non-zero exit, empty output or a timeout is a provider failure. The LLM model can be named too, with[propose] modelor--llm-model; it joins the cache key when named (ADR-0020's amendment, M027/S05, backlog X11). - The answer is a structured suggestion, from a versioned prompt.
prompts/suggest-v1.txt(the version is in the file name) is filled deterministically from the model. Model text, labels, conditions and native BPMN and DMN fragments sit in fenced DATA blocks the prompt says are data, not instructions, as indraft-v1.txt. The prompt ends with the JSON answer schema, and the answer must be one JSON object of a named kind. Anything else, or a suggestion naming a gateway, flow, field or decision the model does not have, is rejected with aGeneratorErrororResolutionErrorthat names the fact. - The first kind is
condition-to-dmn. A gateway whose outgoing conditions compute over names no upstream decision sets (form fields a user task writes, for exampleamount > 100) is an inline rule. The suggestion names the gateway's BPMN id, a decision id and name, its input field names, its output variable, its rules (input tests mapped to output values) and the new condition for each outgoing flow. Applied, it gives:- a DMN decision added to the process's existing DMN payload;
- a
businessRuleTaskinserted before the gateway in the BPMN payload, with itsseam:decidedByand the other seam links the model's existing business-rule tasks carry; dec:Decisionfacts (inputs withdec:variableanddec:fromPath, the output'sdec:processVariable,seam:reads,dec:payloadElement) shaped like the decisions already in the model;- each flow's condition rewritten over the decision's output variable.
- Deterministic code builds the change. The LLM only proposes.
build_changeturns a parsed suggestion into aseams_editPatchfor the TriG facts and new payload texts for the BPMN and DMN files, editing the XML with lxml and keeping the modeler's layout asseams_edit.xmldoes. Every name comes from the model or the suggestion; none is hard-coded. The same model and the same answer give the same bytes. - Checked in a scratch copy. The project is copied to a temporary directory and the change is written there through
seams_edit(files.apply,xml.save_payload). The copy must pass the seam shapes andseams.decisions_check.decision_findings, and must generate. A failure is reported with the subject and predicate of each finding, and nothing is stored. - A passing suggestion is pending, not applied. It is stored under
.seams/suggestions/<id>/(gitignored, like.seams/drafts/):suggestion.jsonand one unified diff per changed file. The id is the sha256 of the model hash, the template, the provider command and the response. The provider response is cached by (model hash, template, provider), so a re-run on the same model is free; a changed model misses the cache. Failed calls are not cached. - A person accepts.
seams suggest --accept IDre-checks the suggestion against the current model hash and applies it throughseams_edit, the one write path. A stale id (the model changed since the suggestion was made) is refused; asking again makes a fresh one.--listshows pending suggestions. After accept the change is ordinary model text the modeler owns and commits after review. - Never in CI. CI calls no provider. The tests of
seams suggestuse fake provider scripts, and the change builder is tested with suggestions built test-side.
Later kinds
The same loop (prompt, JSON suggestion, deterministic patch, scratch check, pending, accept) carries the other two R2 kinds, each with its own template version and builder:
merge-shapes: shapes with the same properties become one target-classed shape (ADR-0023) on an ontology class, with the forms and decisions that read them repointed.gist-anchor: an ontology class without a gist superclass gets a proposedrdfs:subClassOfa gist class (ADR-0022), for a person to confirm.
Neither is built in M025/S01.
Consequences
- The command prints the suggestion id, the files changed, the validation and regeneration verdicts and
cache hit|miss, and exits 0 when a suggestion is stored or applied. A rejection names the subject and predicate and exits 1; a provider failure exits 1; a project error exits 2. - Changing the prompt text means a new template file (
suggest-v2.txt), which also invalidates cached answers and suggestion ids. - Passing the shapes, the decision checks and generation is the machine half of acceptance; reading the diff before
--acceptand before committing stays the person's half. - Applying through
seams_editkeeps TriG layout, payload layout and undo as the editor has them; no new write path is added.