@appport/contract-builder
v1.0.1
Published
Contract builder for creating AppPort application contracts.
Readme
@appport/contract-builder
Contract builder for creating portable AppPort application contracts.
Overview
The contract builder provides tools for defining AppPort application contracts independently of their implementation. Contracts are the canonical machine-readable format that describe what an application can do.
Usage
# Create a new contract
appport contract init
# Inspect a contract
appport contract inspect appport/contract.json
# Validate a contract
appport contract validate appport/contract.json
# Compare two contracts
appport contract diff old.json new.json
# Generate a TypeScript implementation skeleton
appport contract generate --output my-appContract Format
A contract is a JSON file describing the application and its capabilities:
{
"protocol": "appport/1",
"application": {
"id": "com.example.myapp",
"name": "My App",
"version": "1.0.0",
"description": "A sample application"
},
"capabilities": [
{
"name": "documents.list",
"version": 1,
"description": "List all documents",
"purpose": "Retrieve all documents",
"authority": "documents",
"input": { "type": "object", "properties": {} },
"output": {
"type": "object",
"properties": {
"documents": {
"type": "array",
"items": { "type": "object" }
}
}
},
"authorization": ["documents.read"],
"effects": [
{
"type": "read",
"description": "Reads document data"
}
]
}
],
"events": []
}Building Contracts Programmatically
PR15 introduces a contract-native capability builder for fluent contract construction:
import { createContractBuilder, capability } from "@appport/contract-builder";
// Create a contract builder
const contract = createContractBuilder({
id: "com.example.invoicing",
name: "Invoicing",
version: "1.0"
});
// Define and attach capabilities using the builder pattern
const refund = contract
.capability("invoice.refund", "1")
.purpose("Refund an invoice")
.input({
type: "object",
properties: {
invoice_id: { type: "string" },
reason: { type: "string" }
},
required: ["invoice_id", "reason"]
})
.output({
type: "object",
properties: {
refund_id: { type: "string" },
invoice_id: { type: "string" },
status: { type: "string" }
},
required: ["refund_id", "invoice_id", "status"]
})
.authority("invoicing")
.requiresScope("invoice.refund")
.effect({
type: "state",
description: "Invoice becomes refunded"
})
.emits("invoice.refunded@1")
.build();
contract.attach(refund);
// Manage capabilities
contract.getCapability("invoice.refund@1"); // Get by ID (name@version)
contract.listCapabilities(); // List all attached capabilities
contract.detach("invoice.refund@1"); // Remove a capabilityCapability Builder API
.purpose(text: string)- Set the semantic purpose of the capability.input(schema: JSONSchema)- Define the input schema.output(schema: JSONSchema)- Define the output schema.authority(auth: string)- Set the domain/authority this capability belongs to.requiresScope(scope: string)- Add an authorization scope requirement.effect(effect: CapabilityEffect)- Add an effect this capability produces.emits(event: string)- Add an event this capability emits.description(text: string)- Set the description.kind(kind: "request" | "stream")- Set the capability kind.build()- Build and return the capability
Contract Builder API (PR15)
.capability(name: string, version: string | number)- Create a capability builder.attach(capability: ContractCapability)- Attach a capability to the contract.detach(capabilityId: string)- Remove a capability by ID (name@version format).getCapability(capabilityId: string)- Retrieve a capability by ID.listCapabilities()- Get all attached capabilities.listProvidedCapabilities()- Alias for listCapabilities
Application Capability Relationships (PR16)
PR16 extends contracts to declare both what an application provides and what it requires from other applications.
Capability Requirements
Applications can declare external dependencies using capability requirements. Requirements support version constraints to enable dynamic composition:
import { createContractBuilder, requirement } from "@appport/contract-builder";
const commerce = createContractBuilder({
id: "com.example.commerce",
name: "Commerce",
version: "1.0"
});
// Add a provided capability
const orderCreate = commerce
.capability("order.create", 1)
.purpose("Create an order")
.input({ type: "object", properties: {} })
.output({ type: "object", properties: {} })
.build();
commerce.attach(orderCreate);
// Add requirements from other applications
const invoiceRequirement = commerce
.requirement("invoice.create", "^1")
.purpose("Create an invoice for a completed order")
.from("com.example.invoicing")
.build();
commerce.addRequirement(invoiceRequirement);
// Manage requirements
commerce.listRequiredCapabilities(); // List all required capabilities
commerce.getRequirement("invoice.create"); // Get by name
commerce.requiresCapability("invoice.create"); // Check if required
commerce.removeRequirement("invoice.create"); // Remove a requirementRequirement Builder API
.purpose(text: string)- Set the purpose of the requirement.from(applicationId: string)- Constrain the provider to a specific application ID.build()- Build and return the requirement
Contract Builder API (PR16 Extensions)
.requirement(name: string, versionConstraint: string)- Create a requirement builder.addRequirement(requirement: CapabilityRequirement)- Add a required capability.removeRequirement(name: string)- Remove a requirement by name.getRequirement(name: string)- Retrieve a requirement by name.listRequiredCapabilities()- Get all required capabilities.requiresCapability(name: string)- Check if a capability is required.provides(capabilityId: string)- Check if a capability is provided.hooks(hooks: ContractBuilderHooks)- Register lifecycle hooks
Version Constraints
Requirements support semantic version constraints:
^1- Compatible with 1.x.x~1.2- Compatible with 1.2.x>=1- Version 1 or higher>1- Version higher than 1<=2- Version 2 or lower<2- Version lower than 21- Exact version 1
Compatibility Checking
Check if a provided capability satisfies a requirement:
import { isCapabilityCompatible, satisfiesVersionConstraint } from "@appport/contract-builder";
// Check version constraint satisfaction
satisfiesVersionConstraint(1, "^1"); // true
satisfiesVersionConstraint(2, "^1"); // false
// Check full compatibility (name and version)
const requirement = { name: "invoice.create", versionConstraint: "^1" };
const capability = { name: "invoice.create", version: 1, input: {}, output: {} };
isCapabilityCompatible(requirement, capability); // trueKey Distinctions
Provides vs Requires
- Provides: Capabilities owned by this application (declared via
.attach()) - Requires: Capabilities expected from other applications (declared via
.addRequirement())
Discovery vs Compatibility vs Authorization
PR16 establishes three distinct stages:
- Discovery - Find available capabilities at runtime
- Compatibility - Check version constraints and schema compatibility
- Authorization - Verify permission to use the capability
These stages are independent. A capability can be discovered and compatible but still not authorized for use.
No Compile-Time Coupling
Requirements do not include implementation details:
- No SDK imports
- No provider URLs
- No provider code
- No transport details
The consumer contract describes a requirement. The runtime resolves the provider.
Builder Hooks
Register hooks to be notified of contract authoring events:
const contract = createContractBuilder({...});
contract.hooks({
onCapabilityAdded(capability) {
console.log(`Added capability: ${capability.name}`);
},
onCapabilityRequired(requirement) {
console.log(`Added requirement: ${requirement.name}`);
},
onCapabilityRequirementRemoved(name) {
console.log(`Removed requirement: ${name}`);
}
});
// Hooks are called when capabilities/requirements are added/removed
contract.attach(cap);
contract.addRequirement(req);Available hooks:
onCapabilityAdded(capability)- Called when a capability is attachedonCapabilityRemoved(capabilityId)- Called when a capability is detachedonCapabilityChanged(old, new)- Called when a capability is modifiedonCapabilityRequired(requirement)- Called when a requirement is addedonCapabilityRequirementRemoved(name)- Called when a requirement is removedonCapabilityRequirementChanged(old, new)- Called when a requirement is modified
Contract Diff with Requirements
The diff function now tracks requirement changes:
import { diffContracts } from "@appport/contract-builder";
const diff = diffContracts(oldContract, newContract);
console.log(diff.capabilities); // Capability changes
console.log(diff.requirements); // Requirement changes
console.log(diff.events); // Event changes
console.log(diff.hasBreakingChanges); // true if any breaking changes
// Requirement changes include:
// { type: "added", name: "invoice.create", versionConstraint: "^1" }
// { type: "removed", name: "payment.authorize" }
// { type: "modified", name: "invoice.send", versionConstraint: "^2", breaking: true }Extracting Requirements from Contracts
Extract requirements for analysis:
import { getContractRequirements } from "@appport/contract-builder";
const requirements = getContractRequirements(contract);
// Returns array of requirement objects with full detailsAPI
createContract(application: ContractApplication): Contract
Create a new contract with sensible defaults. (Legacy API)
createContractBuilder(application: ContractApplication): ContractBuilder
Create a new contract builder with the fluent API for constructing contracts programmatically.
capability(name: string, version: string | number): CapabilityBuilder
Create a new capability builder for defining a single capability.
requirement(name: string, versionConstraint: string): RequirementBuilder
Create a new requirement builder for defining a capability requirement.
validateContract(contract: Contract): ValidationResult
Validate a contract. Returns validation issues and overall validity. Validates both provided capabilities and requirements.
diffContracts(from: Contract, to: Contract): ContractDiff
Compare two contracts and produce a diff highlighting changes and breaking changes. Includes capability, event, and requirement changes.
isCapabilityCompatible(requirement: CapabilityRequirement, capability: ContractCapability): boolean
Check if a provided capability can satisfy a requirement by verifying:
- Name matches
- Provided version satisfies version constraint
- Schemas exist
Returns true if compatible, false otherwise. Authorization is NOT checked here.
satisfiesVersionConstraint(providedVersion: number, constraint: string): boolean
Check if a provided version number satisfies a semantic version constraint (e.g., "^1", "~1.2", ">=2").
getContractRequirements(contract: Contract): CapabilityRequirement[]
Extract all required capabilities from a contract, useful for analyzing external dependencies.
contractToManifest(contract: Contract): AppPortManifest
Convert a portable Contract to an AppPortManifest for use with client generation and other tools.
generateTypescriptServer(contract: Contract): GeneratedTypescriptServer
Generate a complete TypeScript implementation skeleton from a contract, including:
- Configuration
- Capability definitions
- Event handlers
- Server and client setup
Key Design Principles
- Contract-Native: The contract is the canonical definition of capabilities and requirements
- Fluent API: Build contracts programmatically with a natural, fluent interface
- Ownership: Capabilities are explicitly attached to contracts, establishing ownership
- Identity: Capabilities and requirements are uniquely identified by name@version
- Canonical Format: The contract is the source of truth, not the implementation
- Portable: Contracts can move between TypeScript, Python, Rust, etc.
- Validation: Strong validation ensures contract correctness
- Generation: Contracts can generate implementation skeletons
- Diffing: Compare contracts to track changes and breaking changes
- Distinct Relationships: Provides (ownership) and requires (participation) are separate concerns
- Version Constraints: Requirements use semantic version constraints for dynamic composition
- No Compile-Time Coupling: Applications can require capabilities without importing provider code
- Discovery ≠ Compatibility ≠ Authorization: Three independent stages in capability resolution
- Lifecycle Hooks: Contract authoring events trigger optional hooks for validation and logging
