Build one deterministic model prompt from an explicit prompt contract, supplied values, ordered sections, and declared construction policy.
The narrow, deterministic boundary between authored prompt authority and model execution. It does not choose a model, call a provider, or interpret a response — it constructs the exact request, and proves how it was assembled.
Capability ID: builds-model-prompt · Version 0.1.0
Without this boundary, application code does this everywhere:
const prompt = `
You are an expert software architect.
Here is the user request:
${intent}
Here is the project context:
${context}
`;Prompt wording ends up buried in application code, substitution is
inconsistent, context order changes accidentally, missing values silently
become undefined, and the final prompt cannot be reproduced or proven.
Prompt Definition + Supplied Values + Construction Policy
↓
Deterministic Prompt Package + Receipt
npm install
# Run the proven behaviour
npm test
# Build a prompt through the CLI front door
npm run build-prompt -- \
--definition ./demonstrations/explains-capability.prompt.json \
--values ./demonstrations/explains-capability.values.json
# Emit only connector-ready messages
npm run build-prompt -- \
--definition ./demonstrations/explains-capability.prompt.json \
--values ./demonstrations/explains-capability.values.json \
--output messagesOutput discipline:
stdout → canonical constructed prompt JSON
stderr → JSONL progress and construction testimony
exit → stable disposition code
The CLI and any library consumer enter through the same operation.
import {
buildsModelPrompt,
sha256Hashes,
systemClock,
} from "./src/index.js";
const constructed = buildsModelPrompt(definition, values, {
clock: systemClock,
hashes: sha256Hashes,
});
if (constructed.disposition === "PROMPT_BUILT") {
console.log(constructed.proof.constructedPromptHash);
}The builder constructs; the connector delivers. Neither has to be trusted about the other's work, because each returns its own receipt.
const constructed = buildsModelPrompt(definition, values, builderPorts);
if (constructed.disposition !== "PROMPT_BUILT") {
return constructed; // fail closed; nothing was sent
}
const response = await obtainsModelResponse(
{
requestId,
providerAuthorityId,
modelAlias,
interaction: { mode: "text-generation", messages: constructed.messages },
responsePolicy,
executionPolicy,
evidencePolicy,
},
connectorPorts
);Prompt Builder ──▶ Generic LLM Connector ──▶ Response Validator
constructedPromptHash requestHash (next capability)
responseHash
A prompt definition holds the wording; values are supplied separately.
{
"promptId": "explains-deterministic-capability",
"version": "1.0.0",
"messages": [
{
"role": "user",
"sections": [
{ "sectionId": "task", "template": "Explain {{capabilityName}}." },
{ "sectionId": "intent", "template": "Canonical intent:\n{{canonicalIntent}}" }
]
}
],
"declaredInputs": [
{ "inputId": "capabilityName", "required": true, "valueType": "text" },
{ "inputId": "canonicalIntent", "required": true, "valueType": "text" }
],
"constructionPolicy": {
"rejectMissingInputs": true,
"rejectUndeclaredInputs": true,
"rejectUnusedInputs": true,
"preserveSectionOrder": true,
"trimInsertedValues": false,
"allowEmptyValues": false,
"sectionSeparator": "\n\n"
}
}Every policy flag is declared. The builder applies no hidden default, and
preserveSectionOrder may only be true — declaration order is the sole
authority for assembly, and a definition that switches it off is asking the
builder to choose an order, which it will not do.
Ambiguous construction is refused, and every reason found is reported — not just the first. A refusal returns findings and proof, never a partial prompt package.
| Disposition | Exit | Refused because |
|---|---|---|
PROMPT_BUILT |
0 | — |
PROMPT_DEFINITION_REJECTED |
10 | the prompt contract is malformed |
PROMPT_SECTION_DUPLICATED |
11 | two sections answer to one sectionId |
PROMPT_MESSAGE_ROLE_UNSUPPORTED |
12 | a role outside system/user/assistant |
PROMPT_PLACEHOLDER_UNDECLARED |
13 | a template references an undeclared input |
PROMPT_INPUT_MISSING |
20 | a required input was not supplied |
PROMPT_INPUT_UNDECLARED |
21 | a value was supplied the contract never declared |
PROMPT_INPUT_TYPE_MISMATCH |
22 | a value contradicts its declared valueType |
PROMPT_INPUT_EMPTY |
23 | a value is empty and the policy forbids it |
PROMPT_INPUT_UNUSED |
24 | a declared input no template references |
INTERNAL_CONSTRUCTION_FAILED |
50 | an internal fault |
PROMPT_INPUT_UNUSED is fail-closed by default because an unreferenced
declaration means the contract and its sections have drifted apart. It is
governed by rejectUnusedInputs, so a definition can declare otherwise.
Try it:
npm run build-prompt -- \
--definition ./demonstrations/explains-capability.prompt.json \
--values ./demonstrations/rejects-missing-input.values.json
# → PROMPT_INPUT_MISSING, exit 20, no messages returnedThree hashes make the determinism claim checkable rather than asserted.
definitionHash → which prompt contract authored this request
valuesHash → which values were supplied to it
constructedPromptHash → what was actually produced
Each is SHA-256 over canonical JSON with sorted keys, so a semantically identical input hashes identically regardless of key order, while any change to wording, ordering, or a value changes the hash. Running the same build twice produces byte-identical output.
One form only: {{inputName}}.
Deliberately not a template language. There are no expressions, conditionals,
loops, function calls, file includes, environment lookups, or hidden defaults.
A single left-to-right pass replaces each placeholder and never rescans its own
output, so a value containing {{...}} is inserted literally and cannot inject
a new placeholder.
Each declared type has exactly one textual form:
| Declared type | Rendered as |
|---|---|
text |
the string itself |
number |
its JSON form |
boolean |
true or false |
json |
canonical JSON, keys sorted |
prompt-builder/
├── intent/ # the canonical intent this package answers
├── authority/ # the contracts that govern behaviour
│ ├── prompt-builder.sej.v1.json
│ ├── prompt-definition.schema.v1.json
│ ├── prompt-values.schema.v1.json
│ ├── constructed-prompt.schema.v1.json
│ ├── prompt-construction-receipt.schema.v1.json
│ └── feature-body.contract.v1.json
├── acceptance/
│ └── builds-model-prompt.feature
├── ascii/ # capability context, flow, and proof diagrams
├── src/
│ ├── builds-model-prompt/
│ │ ├── validates-prompt-definition.ts
│ │ ├── validates-supplied-inputs.ts
│ │ ├── resolves-template-values.ts
│ │ ├── assembles-prompt-messages.ts
│ │ ├── calculates-prompt-identity.ts
│ │ ├── returns-construction-receipt.ts
│ │ └── builds-model-prompt.ts
│ ├── shared/
│ │ ├── prompt-builder-contract.ts
│ │ └── runtime-ports.ts
│ └── index.ts
├── bin/prompt-builder.ts # CLI front door
├── demonstrations/ # executable claims, replayed by the tests
└── tests/
├── acceptance/ # one describe per scenario, in feature order
├── conformance/ # one describe per feature-body invariant
└── demonstrations/ # replays every shipped demonstration
It owns construction, and nothing else. It does not choose the model or provider, call a model, retry, count provider tokens, parse or validate a response, search a vector database, load workspace files, summarize or select context, mutate prompts from responses, or maintain a conversation.
Reusable prompt definitions, version lookup, and template discovery belong to the next micro-app — the Prompt Template Resolver — not here.
npm run typecheck
npm test # 68 tests
npm run test:acceptance # scenarios from the feature file
npm run test:conformance # invariants from the feature-body contract
npm run test:demonstrations # replays the shipped demonstrationsConformance proves, among other invariants, that the capability performs no network, file-system, or environment access: every source file is asserted free of provider, transport, and filesystem imports, and the whole dependency surface is a clock and a hash port.