Skip to content

Repository files navigation

Prompt Builder

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

The problem

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

Quick start

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 messages

Output discipline:

stdout  → canonical constructed prompt JSON
stderr  → JSONL progress and construction testimony
exit    → stable disposition code

Library use

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);
}

Composition with the generic LLM connector

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

Input contract

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.

Fail-closed behaviour

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 returned

Proof

Three 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.

Template syntax

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

Package layout

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

What this capability must not become

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.

Tests

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 demonstrations

Conformance 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.

About

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.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages