# LineSpec — Full Documentation > This file concatenates the full LineSpec v3.13.0 documentation — the project overview (README), the Provenance Records reference, and the LineSpec Testing DSL reference — as a single Markdown corpus for AI agents. The curated index is at https://linespec.dev/llms.txt. --- # ===== Project Overview (README) ===== # LineSpec v3.13.0 [![Version](https://img.shields.io/badge/version-3.13.0-blue.svg)](https://github.com/livecodelife/linespec/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/livecodelife/linespec)](https://goreportcard.com/report/github.com/livecodelife/linespec) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) **Provenance Records** - Structured YAML artifacts for documenting architectural decisions **LineSpec Testing** - DSL-based integration testing for containerized services --- ## Overview LineSpec is a tool for managing **Provenance Records** - structured decision documents that capture the intent, constraints, and reasoning behind architectural changes. It includes a powerful CLI for creating, validating, and enforcing these records. As of v3.13.0, **LineSpec Testing** is stable and included in the default installation alongside Provenance Records. --- ## Installation ### Homebrew (Recommended) ```bash brew tap livecodelife/linespec brew install linespec ``` The Homebrew formula automatically builds the `linespec:latest` Docker image after installation. If Docker was not running at install time, run it manually once Docker is started: ```bash linespec build ``` ### Go Install ```bash go install github.com/livecodelife/linespec/cmd/linespec@v3.13.0 ``` ### GitHub Releases Download pre-built binaries from the [releases page](https://github.com/livecodelife/linespec/releases). - `linespec_3.13.0_*` - Full release (Provenance Records + LineSpec Testing) --- ## Quick Start - Provenance Records (Stable) ```bash # 1. Install LineSpec brew tap livecodelife/linespec brew install linespec # 2. Create your first provenance record linespec provenance create --title "Add user authentication" # 3. View the decision graph linespec provenance graph # 4. Validate all records linespec provenance lint ``` ### Example Provenance Record ```yaml id: prov-2026-001 title: "Use PostgreSQL for primary data store" status: open created_at: "2026-03-15" author: "dev@example.com" intent: > After evaluating options, we choose PostgreSQL for our primary data store due to better JSON support and concurrent write handling. constraints: - All new tables must use PostgreSQL - Use connection pooling (min 10, max 100) affected_scope: - pkg/db/** - migrations/** associated_specs: - path: tests/db/postgres_integration_spec.rb type: rspec tags: - architecture - database ``` **[Complete Provenance Records Reference →](./PROVENANCE_RECORDS.md)** --- ## Feature Overview | Feature | Commands | Status | |---------|----------|--------| | **Provenance Records** | `provenance` | ✅ Stable | | **LineSpec Testing** | `test`, `proxy` | ✅ Stable | --- ## Provenance Records - Full Feature Set ### CLI Commands ```bash # Record management linespec provenance create # Create new record linespec provenance lint # Validate records linespec provenance status # View status linespec provenance graph # Render decision graph # Agent guidance linespec provenance next # Compute the correct next provenance action linespec provenance govern # List the active records governing given files linespec provenance context # Show which records govern given files # Semantic search (requires embedding configuration) linespec provenance search # Search by semantic similarity linespec provenance audit # Audit changes against history linespec provenance index # Index records for search # Git integration linespec provenance check # Check commits for violations linespec provenance install-hooks # Install git hooks linespec provenance install-skills # Install all Claude Code skills (provenance + linespec-testing) linespec provenance install-plugin # Install the Claude Code provenance plugin (hooks + slash command) # Lifecycle management linespec provenance lock-scope # Lock scope to allowlist linespec provenance complete # Mark as implemented linespec provenance deprecate # Mark as deprecated # Manifest distribution linespec provenance publish # Package records into a versioned manifest artifact linespec clone # Bootstrap a project from a published manifest linespec import # Import provenance records from a published manifest # Output formats linespec provenance lint --format json # JSON output for CI linespec provenance lint --format sarif # SARIF output for GitHub Code Scanning ``` ### Key Features - **Structured YAML format** - Clear, version-controlled decision records - **Scope enforcement** - Automatic validation of what files can be modified - **Git integration** - Pre-commit hooks and commit message validation - **Graph visualization** - Query and visualize decision relationships - **Monorepo support** - Service-specific records with ID suffixes - **CI/CD ready** - JSON output and strict enforcement modes - **SARIF output** - GitHub Code Scanning integration ### SARIF Output for GitHub Code Scanning LineSpec can emit lint results in SARIF (Static Analysis Results Interchange Format) 2.1.0 format for integration with GitHub Code Scanning: ```bash # Generate SARIF output linespec provenance lint --format sarif > provenance-results.sarif # Upload to GitHub Code Scanning (in your CI workflow) - name: Lint provenance records run: linespec provenance lint --format sarif > provenance-results.sarif continue-on-error: true - name: Upload to Code Scanning uses: github/codeql-action/upload-sarif@v3 with: sarif_file: provenance-results.sarif category: linespec-provenance ``` The SARIF output includes: - **19 stable rule IDs** (PROV001-PROV019) for consistent alerting - **File hashes** for GitHub's deduplication - **%SRCROOT% base** for proper path resolution - **Severity mapping** (error→error, warning→warning, hint→note) ### Documentation - **[PROVENANCE_RECORDS.md](./PROVENANCE_RECORDS.md)** - Complete reference guide - **[CLAUDE.md](./CLAUDE.md)** - Guidelines for AI agents using LineSpec --- ## LineSpec Testing LineSpec Testing is a DSL-based integration testing framework for containerized services. It intercepts database and HTTP traffic at the protocol level, making tests language-agnostic and framework-independent. ### Commands ```bash # Build the linespec:latest Docker image (required for proxy sidecars) linespec build # Run integration tests linespec test # Start protocol proxies linespec proxy mysql linespec proxy postgresql linespec proxy http linespec proxy kafka linespec proxy grpc ``` ### Example LineSpec Test ```linespec TEST create-user RECEIVE HTTP:POST http://localhost:3000/users WITH {{payloads/user_create_req.yaml}} EXPECT WRITE:MYSQL users WITH {{payloads/user_db_write_record.yaml}} VERIFY query MATCHES /\bpassword_digest\b/ VERIFY query NOT_CONTAINS '`password`' RESPOND HTTP:201 WITH {{payloads/user_create_resp.yaml}} NOISE body.id body.created_at ``` ### Documentation - **[LINESPEC.md](./LINESPEC.md)** - DSL syntax reference - **[CLAUDE.md](./CLAUDE.md)** - Contributor & AI-agent guidelines --- ## Configuration Create a `.linespec.yml` in your repository root: ```yaml # Provenance Records configuration provenance: dir: provenance # Records directory enforcement: warn # none|warn|strict commit_tag_required: false # Require IDs in commits auto_affected_scope: true # Auto-populate from git run_associated_specs_on_complete: false # Run associated_specs on completion overlap_specs_on_complete: block # Overlap teeth severity: block|warn|off shared_repos: [] # Additional directories (monorepos) # Semantic search configuration (optional - supports voyage or openai) embedding: provider: voyage # Embedding provider: voyage or openai index_model: voyage-4-large # Model for indexing (2048 dims for voyage) query_model: voyage-4-lite # Model for queries (2048 dims for voyage) api_key: ${VOYAGE_API_KEY} # API key from environment similarity_threshold: 0.50 # Minimum similarity for results index_on_complete: true # Auto-index on complete # LineSpec Testing configuration service: name: my-service type: web port: 3000 framework: rails # rails, fastapi, django, express, chi, or custom start_command: bundle exec rails server # Optional: override framework default migration_command: bundle exec rake db:migrate # Optional: custom migration command warmup_endpoint: /health # Optional: custom warmup endpoint warmup_delay_ms: 100 # Optional: warmup delay in milliseconds database: type: mysql port: 3306 database: my-service_development # Database name (default: {service}_development) username: my-service_user # Username (default: {service}_user) password: my-service_password # Password (default: {service}_password) host: db # Host for external DBs infrastructure: database: true kafka: false grpc: false # Start a gRPC proxy sidecar redis: false # Start a Redis proxy sidecar proxy_image: "linespec:latest" # Docker image for protocol proxies (default: linespec:latest) # Protobuf descriptor set for gRPC binary protobuf mocks (optional) grpc_descriptor_set: proto/workflow.pb # Container naming configuration (optional) container_naming: database_container: linespec-shared-db network_name: linespec-shared-net network_alias: real-db migrate_container: linespec-migrate- ``` **Framework Support:** | Framework | Start Command | Migration Command | Needs Warmup | |-----------|---------------|-------------------|--------------| | `rails` | `bundle exec rails server -b 0.0.0.0 -p ${PORT}` | `bundle exec rails db:migrate` | Yes (100ms) | | `fastapi` | `python -m uvicorn main:app --host 0.0.0.0 --port ${PORT}` | None | No | | `django` | `python manage.py runserver 0.0.0.0:${PORT}` | `python manage.py migrate` | Yes (100ms) | | `express` | `npm start` | None | No | | `chi` | `PORT=${PORT} go run .` | None | No | | Custom | Via `start_command` | Via `migration_command` | Via `needs_warmup` | The **Needs Warmup** column shows the framework default for `needs_warmup` (and the default `warmup_delay_ms` where applicable); override any value per-service under `service:`. --- ## Development ```bash # Clone repository git clone https://github.com/livecodelife/linespec.git cd linespec # Build (Provenance Records + LineSpec Testing — both included by default) go build -o linespec ./cmd/linespec # `make build-beta` / `-tags beta` remain only as backward-compatible aliases; # they produce the same binary as the default build (identical since v2.0.0). # Run unit tests go test ./... # Run integration tests (requires Docker) make test-integration # Run the example test suites ./linespec test ./examples/ ``` --- ## Updating the Docs Site The site at [linespec.dev](https://linespec.dev) is built **from source** on every push to `main` (`scripts/build-docs.sh` + `.github/workflows/pages.yml`). Markdown is the single source of truth; the rendered `docs/*.html` and `docs/llms*.txt` are gitignored build artifacts — **don't edit them by hand**, they're regenerated on every push. | To change… | Edit… | |------------|-------| | Provenance reference content | `PROVENANCE_RECORDS.md` | | Testing / DSL reference content | `LINESPEC.md` | | Overview content | `README.md` | | Landing page / docs hub / curated `llms.txt` | the templates in `docs/templates/` (use the `__VERSION__` token for the version) | | Page shell or design | `docs/templates/content.html`, `docs/templates/site-filter.lua`, `docs/assets/` | | The version everywhere | `./scripts/bump-version.sh ` (touches `VERSION` + the `.md` sources only) | Preview locally before pushing — the output is identical to CI: ```bash brew install pandoc # one-time; CI installs it via apt ./scripts/build-docs.sh # renders docs/*.html and docs/llms*.txt open docs/index.html ``` --- ## Documentation Index | Document | Status | Description | |----------|--------|-------------| | **[PROVENANCE_RECORDS.md](./PROVENANCE_RECORDS.md)** | ✅ Stable | Complete provenance reference | | **[README.md](./README.md)** | ✅ Stable | This file - overview and installation | | **[CLAUDE.md](./CLAUDE.md)** | ✅ Stable | Contributor & AI-agent guidelines | | **[LINESPEC.md](./LINESPEC.md)** | ✅ Stable | DSL syntax for integration testing | | **[CHANGELOG.md](./CHANGELOG.md)** | ✅ Stable | Version history and breaking changes | ### Reading Order 1. **Start here** (README.md) - Installation and overview 2. **PROVENANCE_RECORDS.md** - Complete reference for Provenance Records 3. **LINESPEC.md** - If using LineSpec Testing 4. **CLAUDE.md** - If contributing or using AI agents with LineSpec --- ## FAQ ### How do I migrate from the old repository? The module path changed from `github.com/calebcowen/linespec` to `github.com/livecodelife/linespec`. Update your imports and use the new installation path. ### Where are my compiled YAML files? LineSpec doesn't generate YAML files. Provenance Records are the authoritative source and are executed directly. --- ## Contributing 1. Check for an existing provenance record covering your work 2. If none exists, create a new record describing your decision 3. Make your changes 4. Run the linter: `linespec provenance lint` 5. Submit a pull request See [CLAUDE.md](./CLAUDE.md) for detailed guidelines. --- ## License MIT License - See [LICENSE](./LICENSE) for details. --- ## Support - **Issues:** https://github.com/livecodelife/linespec/issues - **Discussions:** https://github.com/livecodelife/linespec/discussions - **Releases:** https://github.com/livecodelife/linespec/releases --- **LineSpec v3.13.0** - Built with Provenance Records --- # ===== Provenance Records Reference ===== # Provenance Records **Version:** 1.0.0 (Stable) **LineSpec CLI Documentation** Provenance Records are structured YAML artifacts that capture the organizational intent, constraints, and reasoning behind system changes. They live in a `provenance/` directory at the repository root and form a queryable graph of architectural decisions over time. --- ## Table of Contents 1. [Quick Start](#quick-start) 2. [Installation](#installation) 3. [Core Concepts](#core-concepts) 4. [Schema Reference](#schema-reference) 5. [CLI Commands](#cli-commands) 6. [Configuration](#configuration) 7. [Git Integration](#git-integration) 8. [Best Practices](#best-practices) 9. [Examples](#examples) --- ## Quick Start ```bash # Install LineSpec brew tap livecodelife/linespec brew install linespec # Or use go install go install github.com/livecodelife/linespec/cmd/linespec@v3.11.7 # Create your first provenance record linespec provenance create --title "Add user authentication" # Validate all records linespec provenance lint # View the decision graph linespec provenance graph ``` --- ## Installation ### Homebrew (Recommended) ```bash brew tap livecodelife/linespec brew install linespec ``` ### Go Install ```bash go install github.com/livecodelife/linespec/cmd/linespec@v3.11.7 ``` ### GitHub Releases Download pre-built binaries from the [releases page](https://github.com/livecodelife/linespec/releases). --- ## Core Concepts ### What are Provenance Records? Provenance Records are **structured decision documents** that capture: - **Intent** - What we want to achieve and why - **Constraints** - Rules that must be followed - **Scope** - What files are affected (or explicitly forbidden) - **Relationships** - How decisions connect (supersedes, related) - **Status** - Where the decision is in its lifecycle They provide a **queryable history** of architectural decisions that can be linted, graphed, and enforced at commit time. ### ID Format Records use the format: `prov-YYYY-XXXXXXXX` or `prov-YYYY-XXXXXXXX-service-name` Where: - `YYYY` is the four-digit year - `XXXXXXXX` is 8 cryptographically random hex characters (lowercase) The year prefix limits the collision probability per year. With 8 hex characters, there are 2^32 (4,294,967,296) possible combinations per year, making collisions extremely unlikely even with concurrent record creation. Examples: - `prov-2026-a1b2c3d4` - Root-level decision - `prov-2026-deadbeef-user-service` - Service-specific decision **Backward Compatibility:** The system still accepts the legacy sequential format `prov-YYYY-NNN` for existing records, but all new records use the crypto random format. ### Status Lifecycle ``` ┌─────────┐ ┌─────────┐ ┌─────────────┐ ┌────────────┐ │ draft │───▶│ open │───▶│ implemented │───▶│ superseded│ └─────────┘ └─────────┘ └─────────────┘ └────────────┘ │ ▼ ┌────────────┐ │ deprecated │ └────────────┘ ``` - **draft** - Record created but enforcement not yet active. Scope and spec checks are suppressed. Transition to open with `linespec provenance open --record `. - **open** - Decision is active and under enforcement - **implemented** - Decision is complete and immutable - **superseded** - Replaced by a newer record (use `superseded_by`) - **deprecated** - No longer relevant --- ## Schema Reference ### Complete Example ```yaml id: prov-2026-001 title: "Protocol-level proxy interception for language-agnostic test evaluation" status: implemented created_at: "2026-03-12" author: "caleb.cowen@gmail.com" sealed_at_sha: "a3f92c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b" intent: > LineSpec evaluates service behavior by intercepting traffic at the TCP/protocol layer rather than through application-level mocking... constraints: - Interception must occur at the network/protocol layer - The system under test must not require modification - Proxy implementations must be protocol-correct affected_scope: - pkg/proxy/** - pkg/registry/** - cmd/linespec/main.go forbidden_scope: - "re:.*_test\\.go$" supersedes: null superseded_by: null related: - prov-2026-002 associated_specs: [] associated_traces: [] monitors: [] tags: - architecture - proxy - core ``` ### Field Descriptions #### Required Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier (prov-YYYY-XXXXXXXX or prov-YYYY-XXXXXXXX-service) | | `title` | string | Human-readable title | | `status` | string | One of: draft, open, implemented, superseded, deprecated | | `type` | string | Record tier: `brief`, `blueprint` (default), `bug`, or `imprint`. See [Tier Hierarchy](#tier-hierarchy). | | `created_at` | string | ISO 8601 date (YYYY-MM-DD) | | `author` | string | Email of the author | #### Intent and Constraints | Field | Type | Description | |-------|------|-------------| | `intent` | string | What we want to achieve and why (folded scalar `>`) | | `constraints` | array | List of rules that must be followed | #### Scope | Field | Type | Description | |-------|------|-------------| | `affected_scope` | array | Files/paths this decision affects | | `forbidden_scope` | array | Files/paths explicitly excluded | **Pattern Matching:** - Exact paths: `pkg/proxy/mysql/proxy.go` - Glob patterns: `pkg/proxy/**/*.go` - Regex patterns: `"re:.*_test\\.go$"` #### Graph Relationships | Field | Type | Description | |-------|------|-------------| | `supersedes` | string | ID of older record this replaces (same tier; Bug may also supersede Blueprint) | | `superseded_by` | string | ID of newer record that replaces this | | `extends` | string | Bug only: ID of Blueprint or Bug whose constraint coverage this record supplements | | `implements` | string | ID of the parent record one tier above (blueprint→brief, imprint→blueprint) | | `related` | array | Related record IDs (no directional relationship) | #### Proof of Completion | Field | Type | Description | |-------|------|-------------| | `sealed_at_sha` | string | Git SHA captured when record marked implemented (CLI-only, immutable) | | `associated_specs` | array | Proof artifacts validating this decision. Each entry has `path` (required) and optional `type` (e.g., `linespec`, `rspec`, `pytest`) | | `associated_traces` | array | Trace files or test output | | `monitors` | array | URLs or alerts for runtime monitoring | #### Metadata | Field | Type | Description | |-------|------|-------------| | `tags` | array | Arbitrary tags for filtering and organization | --- ## Tier Hierarchy Records can be organized into tiers using the `type` field: | Type | Role | Implements | Required fields | |------|------|------------|-----------------| | `brief` | High-level intent — the "why" | Nothing (top of hierarchy) | `constraints` | | `blueprint` | Design decision — the "what" | `brief` (optional) | — | | `bug` | Correction or gap-fill — the "fix" | Nothing (cannot use `implements`) | exactly one of `supersedes` or `extends` | | `imprint` | Implementation record — the "how" | `blueprint` (required) | `implements` | Records without a `type` field default to `blueprint` for backward compatibility (a lint hint is emitted). **Hierarchy rules enforced by the linter (always-on, graph integrity):** - **Supersession must stay within the same tier.** A `blueprint` cannot supersede a `brief`. Use `related` or `implements` for cross-tier connections. (PROV020) _Exception: `bug` may supersede `blueprint` or another `bug`._ - **`implements` must point exactly one tier up.** `blueprint` implements `brief`, `imprint` implements `blueprint`. Skipping a tier or pointing sideways is a lint error. `brief` and `bug` records may not use `implements`. (PROV021) - **`implements` must resolve.** The referenced record must exist locally or in a configured shared repo cache. A missing reference is always an error. (PROV022) - **Imprint supersession requires same parent.** When an Imprint supersedes another Imprint, both must share the same `implements` value. - **Bug records require exactly one of `supersedes` or `extends`.** Use `supersedes` when existing constraints are incorrect; use `extends` when constraints are missing. Neither or both is an error. - **`extends` target must be a Blueprint or Bug.** A Bug extending a Brief or Imprint is an error. - **`brief` records must carry `constraints`.** A Brief with no constraints is a lint error. **Lifecycle note:** Draft records skip all scope and spec enforcement. Use `linespec provenance open` to begin enforcement when the record is ready. --- ## CLI Commands ### Create Create a new provenance record: ```bash # Interactive (opens editor) linespec provenance create # With pre-populated fields linespec provenance create --title "Add caching layer" --tag architecture,performance # For a specific service linespec provenance create -i user-service --title "Add user auth" # Skip editor linespec provenance create --title "Quick fix" --no-edit ``` **Options:** - `--title "..."` - Pre-populate title - `--type brief|blueprint|bug|imprint` - Set record tier (default: blueprint) - `--supersedes prov-YYYY-XXXXXXXX` - Link to older record - `--tag tag1,tag2` - Add tags - `--no-edit` - Write without opening editor - `-i, --id-suffix name` - Append service suffix - `-c, --config path` - Use custom .linespec.yml **Note:** New records start in `draft` status. Scope and spec enforcement are suppressed until you run `linespec provenance open`. ### Open Transition a record from `draft` to `open`, activating scope and spec enforcement: ```bash linespec provenance open --record prov-2026-XXXXXXXX ``` Only records in `draft` status can be opened. Attempting to open a record in any other status returns an error. **Options:** - `--record prov-YYYY-XXXXXXXX` - Record ID to open (required) - `-c, --config path` - Use custom .linespec.yml ### Lint Validate provenance records: ```bash # Lint all records linespec provenance lint # Lint specific record linespec provenance lint --record prov-2026-001 # JSON output for CI linespec provenance lint --format json # SARIF output for GitHub Code Scanning linespec provenance lint --format sarif > provenance-results.sarif # Override enforcement level linespec provenance lint --enforcement strict ``` **Options:** - `--record prov-YYYY-XXXXXXXX` - Lint single record - `--enforcement level` - none|warn|strict (default from config) - `--format format` - human|json|sarif - `--warn` - Show only warnings - `--info` - Show only informational hints - `--all` - Show all output (errors, warnings, and hints) - `-c, --config path` - Use custom config **Enforcement Levels:** - **none** - Don't enforce at all - **warn** - Show warnings but allow (default) - **strict** - Fail on any violation **SARIF Output:** When using `--format sarif`, the command produces a valid SARIF 2.1.0 document that can be uploaded to GitHub Code Scanning: - Includes all 19 lint rules in the tool descriptor - Maps severity to SARIF levels (error→error, warning→warning, hint→note) - Uses %SRCROOT% as uriBaseId for path resolution - Includes SHA-256 hashes for file deduplication - Supports enforcement level variation (e.g., PROV010 is error under strict, warning under warn) ### Status View record status: ```bash # Overview of all records linespec provenance status # Detailed view of one record linespec provenance status --record prov-2026-a1b2c3d4 # Filter by status linespec provenance status --filter open linespec provenance status --filter implemented # Filter by tag linespec provenance status --filter tag:architecture # Save auto-populated scope linespec provenance status --record prov-2026-a1b2c3d4 --save-scope ``` **Options:** - `--record prov-YYYY-XXXXXXXX` - Show detailed status - `--filter status|tag:name` - Filter results - `--format format` - human|json - `--save-scope` - Persist auto-populated scope - `-c, --config path` - Use custom config The detailed `--record` view includes two hierarchy sections derived at render time: - **Implements** — the parent record ID and title if this record's `implements` field is set, or `—` if not. - **Implemented by** — any records in the local provenance directory whose `implements` field points to this record's ID, shown with their type and title. `—` when none exist. ### Graph Render provenance graph: ```bash # Full graph linespec provenance graph # Graph from specific record (shows parent + record + all downstream children) linespec provenance graph --root prov-2026-a1b2c3d4 # Filter to open records only linespec provenance graph --filter open # JSON output with typed edges linespec provenance graph --format json ``` **Options:** - `--root prov-YYYY-XXXXXXXX` - Show subgraph centred on a record: one implements parent (if any) plus all downstream implements and supersession children - `--filter status` - Show only records with given status - `--format format` - human|json|dot - `-c, --config path` - Use custom config The graph renders two relationship dimensions: - **Supersession chains** (`└─` connector) — records that supersede one another, showing evolution within a tier - **Implements hierarchy** (`↳ [type]` connector) — brief → blueprint → imprint parent-child relationships, visually distinct from supersession. Bug records appear in supersession chains alongside blueprints. In `--format json`, edges carry an `edge_type` field with values `supersedes`, `implements`, or `related`, so visualization tooling can apply different visual treatments. Each node also includes an `implements_nodes` array for direct implements children. ### Check Check commits for violations: ```bash # Check current HEAD linespec provenance check # Check specific commit linespec provenance check --commit abc123 # Check commit range linespec provenance check --range HEAD~5..HEAD # Check against specific record only linespec provenance check --record prov-2026-a1b2c3d4 # Check staged files (used by commit-msg hook) linespec provenance check --staged --message-file .git/COMMIT_EDITMSG ``` **Options:** - `--commit SHA` - Check specific commit (default: HEAD) - `--range SHA..SHA` - Check commit range - `--record prov-YYYY-XXXXXXXX` - Check only against specific record - `--staged` - Check staged files instead of committed files - `--message-file path` - Path to commit message file (for use with --staged) - `-c, --config path` - Use custom config **Use with git hooks:** ```bash # commit-msg hook usage linespec provenance check --staged --message-file "$1" ``` This is used by the commit-msg hook to validate staged files against the commit message being written. ### Lock Scope Lock scope to allowlist mode: ```bash # Dry run first linespec provenance lock-scope --record prov-2026-a1b2c3d4 --dry-run # Lock scope (saves to file) linespec provenance lock-scope --record prov-2026-a1b2c3d4 ``` **Options:** - `--record prov-YYYY-XXXXXXXX` - Required. The record to lock - `--dry-run` - Print scope without writing - `-c, --config path` - Use custom config ### Lock Layer Create a locked layer record — an architectural declaration that crystallizes a portion of the system. The record is created immediately in `implemented` status with `locked: true` and a `sealed_at_sha` captured from HEAD. ```bash # Create a locked layer (opens editor) linespec provenance lock-layer --title "Core proxy layer" # Create without opening editor linespec provenance lock-layer --title "Core proxy layer" --no-edit ``` **Options:** - `--title "..."` - Required. Title for the locked layer record - `--no-edit` - Write without opening editor - `-c, --config path` - Use custom config **How it works:** The command creates a new provenance record that is immediately `implemented` and `locked: true`. After creation, edit the record to define `affected_scope` and `associated_specs` — these together define the protected surface. The linter checks both `affected_scope` and `associated_specs` from locked records against both fields of any `open` record. If any surface overlaps, it's a lint error unless the open record declares `supersedes` pointing at the locked record. This forces explicit acknowledgment when reopening a crystallized layer. **Unlocking a layer:** To modify files governed by a locked record, create a new record that supersedes it: ```bash linespec provenance create --title "Update proxy layer" --supersedes prov-2026-a1b2c3d4 ``` ### Complete Mark record as implemented: ```bash # Normal completion linespec provenance complete --record prov-2026-a1b2c3d4 # Force complete (skip LineSpec check) linespec provenance complete --record prov-2026-a1b2c3d4 --force ``` **Options:** - `--record prov-YYYY-XXXXXXXX` - Required. The record to mark as implemented - `--force` - Skip LineSpec existence check - `-c, --config path` - Use custom config **Hash sealing:** After marking a record as implemented, `complete` computes a SHA-256 hash of the record's canonical YAML content and writes it to `.linespec/hash_manifest.json`. The manifest is created automatically on first use. The full-graph hash and active-subset hash in the manifest are also recomputed at this time. Once sealed, any modification to the record's immutable fields will be detected by `linespec provenance lint` as a **PROV-IMM** error. ### Compile Rebuild the hash manifest from scratch: ```bash # Recompute hashes for all records and update the manifest linespec provenance compile # Use a custom config linespec provenance compile -c /path/to/.linespec.yml ``` **Options:** - `-c, --config path` - Use custom config **Idempotent:** If every record's hash already matches the stored manifest, no file is written and the command exits 0. Running `compile` when the manifest is missing or stale writes the full manifest covering every record and recomputes `FullGraphHash` and `ActiveSubsetHash`. **When to use:** After accidentally deleting or corrupting `.linespec/hash_manifest.json`, after a failed `complete` operation left the manifest incomplete, or as a recovery step when `linespec provenance lint` reports unexpected **PROV-IMM** errors on records you haven't edited. ### Publish Package a repository's provenance records into a versioned, content-addressed `linespec.manifest.json` artifact for distribution: ```bash # Publish provenance records only (minimum viable publish) linespec provenance publish # Include optional layers linespec provenance publish --specs linespecs/ --code pkg/ --prompt PROMPT.md # Pin to an explicit version instead of auto-incrementing linespec provenance publish --version v2 # Write the manifest to a custom path linespec provenance publish --manifest dist/linespec.manifest.json ``` **Options:** - `--manifest path` — Path to `linespec.manifest.json` (default: `./linespec.manifest.json`) - `--version label` — Explicit version label; default is auto-increment (`v1` → `v2` → `v3`) - `--specs path` — Path to a specs artifact file or directory - `--code path` — Path to a code artifact file or directory - `--prompt path` — Path to a prompt artifact file - `--help` — Show help **What it does:** 1. Loads the existing `linespec.manifest.json` (creates one if it doesn't exist) 2. Resolves the next version label (or uses `--version`) 3. Applies a deterministic transformation pipeline to the loaded records, making them consumer-appropriate: - Strips imprint records - Filters superseded records (retains the superseding record) - Promotes bug records to blueprint type - Cleans dangling references left by filtered records - Resets all retained records to `status: open` and removes `sealed_at_sha` 4. Packages each layer as a local artifact file: - **provenance** (always included) — tar archive of individual `.yml` files - **specs**, **code** — tar archive preserving repo-relative paths (for directories) or raw bytes with original extension (for single files) - **prompt** — raw bytes with original file extension 5. Computes SHA-256 for each layer artifact and a `root_hash` across all present layers in declaration order (`provenance`, `specs`, `code`, `prompt`) 6. Appends an immutable version entry to the manifest and updates the `latest` pointer **Artifact files** are written next to the manifest: `linespec-provenance-v1.tar`, `linespec-specs-v1.tar`, `linespec-prompt-v1.md`, etc. **URL fields** are always present in the manifest but left as empty strings — fill them in after uploading the artifacts to your hosting location: ```json { "latest": "v1", "versions": { "v1": { "created_at": "2026-05-15T12:00:00Z", "root_hash": "a3f2...", "layers": { "provenance": { "sha256": "7b1c...", "url": "" } } } } } ``` **Immutability:** `linespec publish` refuses to overwrite an existing version key and exits non-zero. To publish a new version, run `publish` again — it will auto-increment. **When to use:** When sharing a governed project with another team or agent. The recipient runs `linespec clone ` to bootstrap a fully-governed local copy with provenance records, hooks, and config already in place. ### Clone Bootstrap a new project directory from a published manifest: ```bash # Clone from a manifest URL (destination dir defaults to manifest filename stem) linespec clone https://example.com/linespec.manifest.json # Pin to a specific version linespec clone https://example.com/linespec.manifest.json@v2 linespec clone https://example.com/linespec.manifest.json --version v2 # Specify the destination directory name linespec clone https://example.com/linespec.manifest.json --dir myproject ``` **Options:** - `` — Required. URL to a `linespec.manifest.json` file. Append `@version` to pin to a specific version. - `--version label` — Pin to a specific version (overrides `@version` suffix) - `--dir path` — Destination directory name (default: derived from the manifest URL's filename stem) - `--help` — Show help **What it does:** 1. Fetches the manifest JSON and resolves the target version (pinned or `latest`) 2. Verifies `root_hash` against the concatenation of all layer hashes before fetching any artifact — aborts on mismatch 3. Creates the destination directory and runs `git init` 4. Writes `.linespec.yml` with `provenance.manifest_url` set to the source URL (without `@version` suffix) 5. Installs git hooks via `linespec provenance install-hooks` 6. Downloads each layer artifact, verifies its SHA-256 hash, and extracts it: - **provenance** — extracted into `/provenance/` - **specs**, **code**, **prompt** — extracted into `/` preserving repo-relative paths **When to use:** When a colleague or automated agent has published a governed project and you want a fully development-ready local copy with provenance records, hooks, and `.linespec.yml` already configured. ### Import Import provenance records from a published manifest into an existing repository: ```bash # Import all records from the latest manifest version linespec import https://example.com/linespec.manifest.json # Pin to a specific version linespec import https://example.com/linespec.manifest.json@v3 linespec import https://example.com/linespec.manifest.json --version v2 ``` **Options:** - `` — Required. URL to a `linespec.manifest.json` file. Append `@version` to pin. - `--version label` — Pin to a specific version (overrides `@version` suffix) - `--help` — Show help **What it does:** 1. Fetches the manifest and verifies all layer hashes 2. Reads the record IDs from the provenance layer and checks for conflicts with the local `provenance/` directory — aborts without writing anything if any ID already exists 3. Extracts all records from the provenance layer into the local `provenance/` directory 4. Runs `linespec provenance lint` to surface any reference issues introduced by the import **When to use:** When you want to pull in a set of published records to extend an existing repository's provenance graph. Use `clone` instead when starting from scratch. ### Deprecate Mark record as deprecated: ```bash linespec provenance deprecate --record prov-2026-a1b2c3d4 --reason "Replaced by new auth system" ``` **Options:** - `--record prov-YYYY-XXXXXXXX` - Required. The record to deprecate - `--reason "..."` - Deprecation reason - `-c, --config path` - Use custom config ### Context Show provenance context for specific files — which records govern them: ```bash # Check which records govern specific files linespec provenance context pkg/proxy/postgresql/proxy.go # Check multiple files linespec provenance context pkg/proxy/postgresql/proxy.go pkg/registry/registry.go # Compact output linespec provenance context --format compact pkg/proxy/**/*.go # JSON output for tooling linespec provenance context --format json pkg/proxy/postgresql/proxy.go ``` **Options:** - `` - File paths to check (positional arguments) - `--files f1 f2 f3` - Explicit file list (alternative to positional args) - `--format format` - human|compact|json - `-c, --config path` - Use custom config **Output:** - Shows all records whose scope matches the given files - Follows `supersedes` chains to show ancestry - Detects conflicts when multiple open records govern the same file ### Next Compute the single correct **next provenance action** for the current state, with record IDs filled in. Rather than navigating by enforcement errors, ask the engine what to do: ```bash # Ambient: reads staged + working-tree changes linespec provenance next # Intent-aware: plan governance for files you intend to change (before editing) linespec provenance next --files pkg/proxy/postgresql/proxy.go linespec provenance next --plan pkg/proxy/postgresql/proxy.go # --plan is an alias for --files # Machine-readable output for hooks/agents linespec provenance next --json ``` **Options:** - `--files f1 f2 f3` / `--plan f1 f2 f3` - Files you intend to change (intent-aware planning) - `--json` - Machine-readable output - `-c, --config path` - Use custom config **Output:** one primary action — `create` a record, `open` a draft, add an `associated_spec`, commit tagged, complete the blueprint, or "nothing pending" — with the ready-to-run command. When records already govern your files, it tells you to create one new record and tag your commits with it (you do **not** need to supersede them). Cache-backed (see [scope cache](#scope-cache)) for a fast path. ### Govern List the **active** records (open + implemented) that govern given files — excluding superseded/deprecated. This is the narrow, fast lookup the Claude Code plugin's per-edit hook uses; the full `context` command still shows history: ```bash linespec provenance govern --files pkg/proxy/postgresql/proxy.go linespec provenance govern --files pkg/proxy/postgresql/proxy.go --json ``` **Options:** - `--files f1 f2 f3` - Files to look up governance for (positional args also accepted) - `--json` - Machine-readable output (`{files, governing:[{id,status}], next}`) - `-c, --config path` - Use custom config Cache-backed; never pays a full record load on a cache hit. ### Search (Semantic) Search provenance records by semantic similarity: ```bash # Search with natural language query linespec provenance search --query "authentication system" # Limit results linespec provenance search --query "database schema" --limit 10 # Use custom config linespec provenance search --query "API changes" -c /path/to/.linespec.yml ``` **Options:** - `--query "text"` - Required. Natural language search query - `--limit N` - Maximum results to return (default: 5) - `-c, --config path` - Use custom config **Requirements:** - Requires embedding configuration in `.linespec.yml` - Records must be indexed using `linespec provenance index` ### Audit (Semantic) Audit recent changes against provenance history: ```bash # Audit with description linespec provenance audit --description "Added password validation" # Use custom config linespec provenance audit --description "Refactored user service" -c /path/to/.linespec.yml ``` **Options:** - `--description "text"` - Required. Description of changes to audit - `-c, --config path` - Use custom config **Output:** - Shows records with semantic similarity to your changes - Advisory only - always exits 0 - Helps identify potential conflicts with prior decisions ### Index Index provenance records for semantic search: ```bash # Index all unindexed implemented records linespec provenance index # Dry run - show what would be indexed linespec provenance index --dry-run # Re-index all records (even if already indexed) linespec provenance index --force # Use custom config linespec provenance index -c /path/to/.linespec.yml ``` **Options:** - `--dry-run` - Show what would be indexed without making API calls - `--force` - Re-index even if embedding already exists - `-c, --config path` - Use custom config **When to use:** - After enabling embedding configuration for the first time - To backfill historical records - After changing embedding models or formats ### Sync Refresh the local cache for every `shared_repos` entry in `.linespec.yml` so cross-repo `implements`/`supersedes` references resolve against current remote records: ```bash linespec provenance sync # refresh stale shared-repo caches linespec provenance sync --force # ignore the TTL and re-fetch all repos ``` The linter warns when a shared-repo cache is older than `cache_ttl_minutes` (default 60); `sync` clears that warning. ### Run Specs Run a record's `associated_specs` on demand — the same execution the pre-commit hook performs on the `open` → `implemented` transition when `run_associated_specs_on_complete` is enabled: ```bash linespec provenance run-specs --record prov-2026-001 ``` Useful to verify proof artifacts pass *before* attempting to complete a record. See [Scope Enforcement & When You're Blocked](#scope-enforcement--when-youre-blocked) for how `path`/`type`/`run_command` behave. ### Install Hooks Install git hooks for automatic validation: ```bash linespec provenance install-hooks ``` ### Install Skills (Claude Code / AI Agents) Install all LineSpec Claude Code skills into a skills directory: ```bash # Install to .claude/skills/ (default) linespec provenance install-skills # Override the target directory linespec provenance install-skills --path path/to/skills ``` This installs two skills: - **`provenance`** — encodes the full provenance record workflow so AI agents automatically follow the correct create → implement → complete lifecycle. Invoke with `/provenance` in Claude Code. - **`linespec-testing`** — covers running, writing, and debugging LineSpec integration tests. Invoke with `/linespec-testing` in Claude Code. Existing skill directories are overwritten silently. Both skills are always installed together. ### Install Plugin (Claude Code) Install the Claude Code **provenance plugin**, which surfaces the advice engine inside the agent loop: ```bash # Install to .claude/plugins/ (default) linespec provenance install-plugin # Override the target directory linespec provenance install-plugin --path path/to/plugins ``` The plugin ships three hooks (all rendering from `linespec provenance`, no duplicated logic): - **SessionStart** — injects the ambient `next` action so the agent starts each session knowing the next step. - **PreToolUse (Edit/Write)** — on the first edit of a governed file, advisorily notes the open record to tag (and a count of sealed records that also govern it); never blocks. - **PostToolUse (Bash)** — when a `git commit` is rejected on a provenance violation, surfaces the engine's exact remediation. It also bundles a `/provenance-next` slash command. The plugin is installable two ways: this `install-plugin` command (the plugin is embedded in the `linespec` binary), or the Claude Code marketplace via the bundled `.claude-plugin/marketplace.json`: ```bash claude plugin marketplace add /plugins/provenance claude plugin install linespec-provenance@linespec ``` ### Scope Cache `context`, `next`, and `govern` are backed by a scope cache at `.linespec/scope-index.json` — a compact projection of each record's governance fields plus a cheap stat-based fingerprint of the provenance directory. On a fingerprint match the per-file lookups skip the full record load (fast enough for a per-edit hook); on any mismatch or read error they fall back to the authoritative load. The cache is machine-specific (gitignored), self-healing, and safe to delete at any time — it simply rebuilds. --- ## Configuration Create a `.linespec.yml` file in your repository root: ```yaml provenance: # Directory containing provenance records (default: provenance) dir: provenance # Enforcement level: none|warn|strict (default: warn) enforcement: warn # Require provenance IDs in commit messages (default: false) commit_tag_required: false # Auto-populate affected_scope from git commits (default: true) auto_affected_scope: true # Run associated_specs before allowing a completion-transition commit (default: false) run_associated_specs_on_complete: false # Severity of the completion-time cross-record overlap teeth: block|warn|off # (default: block). Gated by run_associated_specs_on_complete above. overlap_specs_on_complete: block # Additional directories to load records from (for monorepos) shared_repos: - examples/user-service/provenance - examples/todo-api/provenance ``` ### Configuration Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `dir` | string | `provenance` | Directory containing records | | `enforcement` | string | `warn` | Global enforcement level | | `commit_tag_required` | bool | `false` | Require tags in commits | | `auto_affected_scope` | bool | `true` | Auto-populate scope | | `run_associated_specs_on_complete` | bool | `false` | Run specs on completion transition | | `overlap_specs_on_complete` | string | `block` | Completion-time overlap teeth severity: `block`\|`warn`\|`off` | | `shared_repos` | array | `[]` | Additional directories | #### `overlap_specs_on_complete` When a record is completed, the completion-time **overlap teeth** run the `associated_specs` of the already-implemented (sealed) records whose scope the change actually touches — verifying that those sealed behaviors still hold. This key sets the severity (it only applies when `run_associated_specs_on_complete` is enabled, the master switch): - **`block`** (default) — run the touched sealed records' specs; if any fails, roll the completion back atomically. This is the original behavior. - **`warn`** — run them, but a failure becomes a non-blocking FYI and the completion proceeds. Useful when the spec suite is environmentally fragile (e.g. flaky container startup) and a failure is more likely noise than a real regression. - **`off`** — skip the cross-record teeth entirely. The completing record's **own** `associated_specs` still run. Note: `complete --force` does **not** bypass the teeth — only this config key (and the master switch) controls them. Remote (`shared_repos`) records are never run as teeth against your local tree. ### Semantic Search Configuration Enable semantic search over provenance records using your choice of embedding provider: **Voyage AI (default):** ```yaml provenance: embedding: provider: voyage # Embedding provider index_model: voyage-4-large # Model for indexing (2048 dims) query_model: voyage-4-lite # Model for queries (2048 dims) api_key: ${VOYAGE_API_KEY} # API key from environment similarity_threshold: 0.50 # Minimum similarity (0.50-0.70 range) index_on_complete: true # Auto-index on complete ``` **OpenAI:** ```yaml provenance: embedding: provider: openai # Embedding provider index_model: text-embedding-3-small # Model for indexing query_model: text-embedding-3-small # Model for queries api_key: ${OPENAI_API_KEY} # API key from environment similarity_threshold: 0.50 # Minimum similarity index_on_complete: true # Auto-index on complete ``` **Configuration Options:** | Option | Type | Default | Description | |--------|------|---------|-------------| | `provider` | string | - | Embedding provider: `voyage` or `openai` | | `index_model` | string | Provider-specific | Model for document indexing | | `query_model` | string | Provider-specific | Model for query embeddings | | `api_key` | string | - | API key (use `${ENV_VAR}` format) | | `similarity_threshold` | float | `0.50` | Minimum similarity for results | | `index_on_complete` | bool | `true` | Auto-generate embeddings on complete | **Voyage AI Models:** - `voyage-4-large` - High-quality model for document embeddings (input_type: "document") - `voyage-4-lite` - Efficient model for query embeddings (input_type: "query") - Both output 2048-dimensional vectors in a shared embedding space - Cross-model similarity is valid due to Voyage's training process **OpenAI Models:** - `text-embedding-3-small` - Fast, cost-effective embeddings (1536 dims) - `text-embedding-3-large` - Best performance embeddings (3072 dims) - `text-embedding-ada-002` - Legacy model (1536 dims) **Note:** When using OpenAI, both document and query embeddings use the same model (no separate input_type handling). --- ## Git Integration ### Two-Hook Strategy The provenance system uses two git hooks that work together: 1. **pre-commit hook**: Runs first, lints modified provenance records for syntax/validity 2. **commit-msg hook**: Runs after you write your message, checks staged files against provenance scope ### Commit Message Format Reference provenance records in commit messages: ```bash # Single record git commit -m "Add user authentication [prov-2026-a1b2c3d4]" # Multiple records git commit -m "Fix auth and caching [prov-2026-a1b2c3d4] [prov-2026-deadbeef]" # Service-specific git commit -m "Update user service [prov-2026-a1b2c3d4-user-service]" ``` ### Pre-commit Hook The pre-commit hook validates that modified provenance records are well-formed, and optionally runs specs when a record is completed: - **Linting**: Checks YAML syntax, required fields, and valid values - **Quick validation**: Ensures records can be parsed and loaded - **Spec execution** (opt-in): When `run_associated_specs_on_complete: true` is set in `.linespec.yml`, detects `open` → `implemented` status transitions and runs the record's `associated_specs` before allowing the commit. Supported types: `linespec`, `rspec`, `pytest`, `jest`. Use `run_command` on any spec entry to override the default command for that type. ### Commit-msg Hook The commit-msg hook validates scope constraints: - **Extracts IDs**: Parses provenance IDs from the commit message - **Checks staged files**: Validates that staged files are in scope of referenced records - **Enforces commit_tag_required**: Blocks commits without provenance IDs when configured - **Self-modification exception**: Allows open records to modify their own YAML files - **Implemented record enforcement**: Rejects commits tagged with already-implemented records (they are immutable) **Implemented Record Enforcement:** Once a provenance record is marked as `implemented`, it becomes immutable. The commit-msg hook will reject any commits tagged with an implemented record ID: ```bash # This will FAIL - prov-2026-a1b2c3d4 is already implemented git commit -m "Fix typo [prov-2026-a1b2c3d4]" # Error: prov-2026-a1b2c3d4 is already implemented - cannot commit with this ID. # Create a new record or supersede this one. # Instead, create a new record or supersede: linespec provenance create --title "Fix typo in auth" --supersedes prov-2026-a1b2c3d4 git commit -m "Fix typo [prov-2026-deadbeef]" ``` The only exception is the completion transition (when a record's own file changes from `status: open` to `status: implemented`), which is allowed. ### Git Hook Installation ```bash # Install both hooks automatically linespec provenance install-hooks # This creates: # .git/hooks/pre-commit - Lints records; runs associated_specs on completion transitions # .git/hooks/commit-msg - Checks staged files against scope ``` **Note:** Both hooks respect the local `./linespec` binary when available (for development), otherwise fall back to the system `linespec`. ### Manual Hook Setup If you prefer manual installation: ```bash # pre-commit hook #!/bin/sh # Use local binary if available if [ -f "./linespec" ]; then LINESPEC="./linespec" else LINESPEC="linespec" fi # Lint modified provenance records modified_records=$(git diff --cached --name-only | grep "^provenance/prov-") for record in $modified_records; do $LINESPEC provenance lint --record "$record" if [ $? -ne 0 ]; then exit 1 fi done # commit-msg hook #!/bin/sh COMMIT_MSG_FILE="$1" if [ -f "./linespec" ]; then LINESPEC="./linespec" else LINESPEC="linespec" fi # Check staged files against scope $LINESPEC provenance check --staged --message-file "$COMMIT_MSG_FILE" if [ $? -ne 0 ]; then echo "Commit blocked due to provenance scope violations" exit 1 fi ``` ### CI Integration Add to your CI pipeline: ```yaml # GitHub Actions example - name: Check Provenance run: | go install github.com/livecodelife/linespec/cmd/linespec@latest linespec provenance lint --enforcement strict linespec provenance check --range HEAD~10..HEAD ``` ### GitHub Code Scanning Integration (SARIF) Upload provenance lint results to GitHub Code Scanning for inline PR annotations and repository-level alerts: ```yaml # GitHub Actions workflow - name: Lint provenance records run: linespec provenance lint --format sarif > provenance-results.sarif continue-on-error: true - name: Upload to Code Scanning uses: github/codeql-action/upload-sarif@v3 with: sarif_file: provenance-results.sarif category: linespec-provenance ``` **Benefits:** - **Inline PR annotations** - Violations appear directly in the Files Changed view - **Repository dashboard** - Track all provenance alerts in one place - **Alert deduplication** - GitHub uses file hashes to avoid duplicate alerts - **Suppression** - Use GitHub's alert management UI to dismiss false positives **Rule IDs:** The SARIF output uses stable rule IDs (PROV001-PROV022, PROV-IMM) that persist across LineSpec versions: - PROV001: InvalidYaml - PROV002: MissingRequiredField - PROV003: UnknownStatus - PROV006: UnresolvedSupersedes - PROV020: SupersessionTypeMismatch (supersedes crosses tier boundaries) - PROV021: ImplementsTypeMismatch (implements skips a tier or points sideways) - PROV022: UnresolvedImplements (implements references a non-existent record) - PROV-IMM: ContentHashMismatch (implemented record content differs from sealed hash in `.linespec/hash_manifest.json`) - ... (see full catalog in SARIF output) **Example PR annotation:** When a commit references an implemented record, GitHub will show an inline annotation: ``` ⚠ PROV010: MissingAssociatedSpecs Record prov-2026-001 is open with no associated specs. ``` ### Automated Embedding Indexing When semantic search is enabled, a GitHub Action workflow automatically indexes newly completed provenance records when they are merged to the main branch. This ensures the embedding store stays up-to-date without manual intervention. **How it works:** 1. When a record is marked `implemented` via `linespec provenance complete`, the embedding is generated locally (if `index_on_complete: true`) 2. When the record is merged to main, the GitHub Action runs and indexes any unindexed records 3. The action respects Voyage AI rate limits and handles batching automatically **Configuration for CI/CD:** For CI/CD environments where you want to skip local embedding generation (to avoid rate limits), set `index_on_complete: false` in `.linespec.yml`: ```yaml provenance: embedding: provider: voyage index_model: voyage-4-large query_model: voyage-4-lite api_key: ${VOYAGE_API_KEY} similarity_threshold: 0.50 index_on_complete: false # Skip local embedding, let GitHub Action handle it ``` **Required Secret:** Add `VOYAGE_API_KEY` as a repository secret in GitHub: 1. Go to Settings → Secrets and variables → Actions 2. Click "New repository secret" 3. Name: `VOYAGE_API_KEY` 4. Value: Your Voyage AI API key **Workflow file:** `.github/workflows/index-provenance.yml` The workflow triggers on pushes to main that modify provenance records and automatically indexes them. --- ## Sealed at SHA and Stale Scope Warnings ### Hash Manifest (`.linespec/hash_manifest.json`) When a record is completed, `linespec provenance complete` seals a **SHA-256 content hash** of the record into `.linespec/hash_manifest.json`. This file is created automatically on first use and is committed alongside the completion commit. **Structure:** ```json { "records": { "prov-2026-a1b2c3d4": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "prov-2026-deadbeef": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9820" }, "full_graph_hash": "abc123...", "active_subset_hash": "def456..." } ``` - **`records`** — Map of record ID to SHA-256 hex digest. The hash is computed over `yaml.Marshal` of the record with `FilePath` cleared, making it deterministic and path-independent. - **`full_graph_hash`** — SHA-256 of the sorted concatenation of all per-record hashes (all statuses). - **`active_subset_hash`** — Same, but only over records not in `superseded` or `deprecated` status. Both graph hashes are recomputed on every `complete` invocation. **Immutability enforcement:** `linespec provenance lint` includes a **PROV-IMM** check that computes the current hash of each implemented record and compares it against the manifest. A mismatch is reported as an error. This check: - Requires no git access — fully runnable in any CI environment or git hook - Is silent for records that predate the hash system (no manifest entry) - Is silent if `.linespec/hash_manifest.json` does not exist (graceful bootstrap) ### What is `sealed_at_sha`? When a provenance record is marked as `implemented`, the CLI automatically captures the current HEAD git SHA and stores it in the `sealed_at_sha` field. This field is: - **Immutable** - Set once by the CLI, never modified after - **CLI-only** - Never set manually - **Timestamp** - Captures the exact moment a decision was "locked in" - **Only on implemented records** - Open/superseded/deprecated records don't have this field Example: ```yaml id: prov-2026-001 title: "Add user authentication" status: implemented created_at: "2026-03-12" author: "user@example.com" sealed_at_sha: "a3f92c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b" # ↑ Captured when `linespec provenance complete` was run ``` ### Stale Scope Warnings The `sealed_at_sha` field enables a feature called **stale scope warnings**. When checking commits against provenance records: 1. If a commit touches files in a record's `affected_scope` 2. The CLI runs `git diff HEAD` on those files 3. If files **haven't actually changed** since the record was sealed, a warning is shown **Why this matters:** - Reduces false positives from files that were incidentally in scope - Distinguishes between meaningful changes and safe refactors - Gives engineers context about whether a change needs review **Example warning:** ``` ⚠ Stale scope warnings in staged (non-blocking): • prov-2026-001 lists pkg/utils/helpers.go in affected_scope, but file unchanged since record sealed at a3f92c1 (File listed in affected_scope but unchanged since record sealed) ``` **Key characteristics:** - **Non-blocking** - These are warnings, not errors - **Informational** - Helps engineers make informed decisions - **Configurable** - Controlled by the `enforcement` setting ### Viewing the Sealed SHA The sealed SHA is displayed in the status output for implemented records: ```bash linespec provenance status --record prov-2026-001 ``` Output: ``` prov-2026-001 · implemented ──────────────────────────────────────────────────────────── Title: Add user authentication Author: user@example.com Created: 2026-03-12 Sealed at: a3f92c1 ← Shows the short SHA ``` --- ## Scope Enforcement & When You're Blocked **The single most important rule:** the pre-commit scope check validates your changed files **only against the record you tag** in the commit message. It does **not** consult other records that happen to govern those files. So implemented/sealed records whose `affected_scope` overlaps your files do **not** block your commit and do **not** need to be superseded. To make a routine change: create **one** new record, set its `affected_scope` to cover exactly the files you are changing, open it, and tag your commits with it. Supersede a record *only* when you are deliberately revising the decision it captured — never merely because its scope overlaps your files. ### Happy path — do this before touching anything 1. Run `linespec provenance context -f ` for each file you will change. 2. Create one record: `linespec provenance create --type blueprint --no-edit --title "…"`. 3. Set its `affected_scope` to exactly those files. 4. Create the proof artifact, then `open` the record with that spec referenced in `associated_specs`. 5. Make your changes; commit tagged with the record ID `[prov-YYYY-XXXXXXXX]`. 6. Show proof, then `complete` the record. ### Scope modes - A record with an **empty `affected_scope`** is **observed** — its check permits any file (except `forbidden_scope`). - A record with a **non-empty `affected_scope`** is **allowlist** — it permits only files matching that scope. So a "scope violation" on *your tagged record* means its `affected_scope` is missing one of your changed files → widen that record's scope (free to edit while `draft`), don't touch other records. - `lock-scope` auto-populates a record's `affected_scope` from the files it changed in git (observed → allowlist). `lock-layer` creates a `locked` governance record — advanced and uncommon; only when locked records exist can an overlapping open record hard-fail lint. ### When you're blocked — decision tree | Message | Do this | |---------|---------| | `Commit tag required but no provenance ID found` | Tag the commit with your record ID `[prov-YYYY-XXXXXXXX]`. | | `X is already implemented - cannot commit with this ID` | Implemented records are immutable. Create **one new record** covering your files and tag that — do **not** supersede the records that govern the files. | | `forbids changes to ` / scope violation | Add `` to **your tagged record's** `affected_scope` (editable in draft). | | `No associated specs (open) [strict]` | Add `associated_specs` (proof) to the open record before committing/completing. | | `overlaps with locked record Y` | A deliberate governance gate (only if locked layers exist). **Stop and ask the maintainer** — do not blindly supersede multiple records. | > **Stale-scope warnings are non-blocking.** When you edit a file governed by an *implemented* record you may see a warning that the file "is governed by implemented record … create a superseding record." This is informational only — the commit still succeeds, no action is required, and it is **not** a reason to supersede anything. Proceed under your own new record; superseding is relevant only if you are intentionally revising that record's decision. **Hard rules:** Never use `--no-verify`. Never relax enforcement (`strict` → `warn`/`none`) to get unblocked — that is a maintainer + settings-level decision, not an agent's. When a wall is genuinely a governance call, stop and ask rather than brute-forcing. ### Use commands, not manual YAML edits Let the CLI update records and other records for you — hand-editing managed fields corrupts the graph. | Instead of manually editing… | Use | |------------------------------|-----| | `supersedes` + the old record's `superseded_by` + `status: superseded` | `create --supersedes ` (sets all of it and stages both files) | | `status: open` | `open --record ` | | `status: implemented` + `sealed_at_sha` | `complete --record ` | | `status: deprecated` | `deprecate --record --reason "…"` | | listing changed files into `affected_scope` | `lock-scope --record ` (auto-populates from git) | | the hash manifest | `compile` | Never hand-edit `status`, `superseded_by`, `sealed_at_sha`, or the hash manifest. ### associated_specs — proof artifacts `associated_specs` attach proof that a record's constraints are met. Each entry has three fields: - **`path`** — required. Must point to a file that *exists* (lint fails otherwise). Any file type: a test, a `.linespec`, a config, a doc, a screenshot, a log. - **`type`** — optional. These auto-run with no `run_command`: `linespec` → `linespec test `, `rspec` → `bundle exec rspec `, `pytest` → `pytest `, `jest` → `npx jest `. Any other type with no `run_command` is recorded as proof but **skipped** (not executed). - **`run_command`** — optional; overrides `type`. Runs as ` ` (the path is appended) **unless** the command contains `{{path}}`, which is substituted instead. Use `{{path}}` when the path is not the last argument. **Strict order of operations:** under strict enforcement an open record with no `associated_specs` is a hard error, and a referenced spec path that does not exist also fails lint — so create the proof file *first*, then reference it, then `open`: ```yaml # 1. write the proof file first (e.g. spec/models/user_spec.rb) # 2. reference it: associated_specs: - path: spec/models/user_spec.rb # required; must already exist type: rspec # optional; auto-runs `bundle exec rspec ` - path: linespecs/create_user.linespec type: linespec # auto-runs `linespec test ` - path: docs/architecture.md run_command: test -f {{path}} # non-test proof: just assert it exists # 3. then `linespec provenance open --record ` ``` Specs are validated on `lint` (path must exist) and executed on the `open` → `implemented` transition when `run_associated_specs_on_complete` is enabled. To author the `.linespec` files that back `type: linespec` specs, see [LINESPEC.md](./LINESPEC.md). --- ## Best Practices ### Writing Good Provenance Records 1. **Start with Intent** - Clearly state what you want to achieve and why 2. **Be Specific with Constraints** - Write verifiable rules 3. **Use Appropriate Scope** - Start in observed mode (empty affected_scope), then lock to allowlist 4. **Link Related Decisions** - Use `supersedes`, `superseded_by`, and `related` 5. **Tag Thoughtfully** - Use consistent tags for filtering ### Scope Management **Observed Mode** (empty affected_scope): - Allows any file changes - Good for early development - Auto-populated from git history **Allowlist Mode** (non-empty affected_scope): - Only allows changes to listed files - Good for mature decisions - Prevents scope creep **Transition:** ```bash # After some commits, lock the scope linespec provenance lock-scope --record prov-2026-001 ``` ### Monorepo Strategy For multiple services in one repo: 1. **Root provenance/** - Shared architectural decisions 2. **Service directories** - Service-specific decisions 3. **Use ID suffixes** - `prov-2026-a1b2c3d4-user-service` ```yaml # .linespec.yml provenance: dir: provenance shared_repos: - services/user-service/provenance - services/todo-api/provenance ``` ### Record Lifecycle ```bash # 1. Validate, then create (status: open) — standalone commit linespec provenance lint linespec provenance check linespec provenance create --title "New feature" --no-edit git commit -m "Create provenance record [prov-2026-deadbeef]" # 2. Develop (make commits, scope auto-populates) # Run check --staged before each implementation commit linespec provenance check --staged git commit -m "Implement feature [prov-2026-deadbeef]" # 3. Lock scope (when feature is complete) linespec provenance lock-scope --record prov-2026-deadbeef # 4. Validate, then complete (status: implemented) — standalone commit linespec provenance lint linespec provenance check linespec provenance complete --record prov-2026-deadbeef git commit -m "Complete provenance record [prov-2026-deadbeef]" # 5. (Optional) Supersede later linespec provenance create --title "Better approach" --supersedes prov-2026-deadbeef ``` --- ## Examples ### Example 1: Simple Architecture Decision ```yaml id: prov-2026-a1b2c3d4 title: "Use PostgreSQL for primary data store" status: implemented created_at: "2026-03-15" author: "dev@example.com" intent: > After evaluating MySQL, PostgreSQL, and SQLite, we choose PostgreSQL for our primary data store. It provides better JSON support, more advanced indexing, and better handling of concurrent writes. constraints: - All new tables must use PostgreSQL - Existing MySQL tables will be migrated gradually - Use connection pooling with minimum 10, maximum 100 connections affected_scope: - pkg/db/** - migrations/** - config/database.yml forbidden_scope: - "re:.*_test\\.go$" - vendor/** tags: - architecture - database - postgresql ``` ### Example 2: Service-Specific Decision ```yaml id: prov-2026-deadbeef-user-service title: "Implement JWT-based authentication" status: open created_at: "2026-03-16" author: "auth-team@example.com" intent: > The user service will implement JWT-based authentication to support stateless API access and microservice communication. constraints: - JWT tokens must expire after 24 hours - Refresh tokens must expire after 30 days - Use RS256 algorithm with 2048-bit keys affected_scope: - services/user-service/pkg/auth/** - services/user-service/handlers/auth.go associated_specs: - path: services/user-service/specs/auth/login_success.linespec type: linespec - path: services/user-service/specs/auth/login_failure.linespec type: linespec tags: - user-service - authentication - jwt ``` ### Example 3: Superseding an Old Decision ```yaml id: prov-2026-01234567 title: "Replace Redis caching with in-memory LRU" status: implemented created_at: "2026-03-17" author: "perf-team@example.com" intent: > After load testing, we found Redis adds unnecessary latency for our use case. An in-memory LRU cache provides better performance with simpler operations. constraints: - Maximum cache size: 10,000 entries - Eviction policy: LRU - TTL: 5 minutes maximum affected_scope: - pkg/cache/** - cmd/api/main.go supersedes: prov-2026-a1b2c3d4 tags: - performance - caching - lru ``` --- ## Troubleshooting ### Common Issues **"Record not found"** - Check that the record file exists in the provenance directory - Verify the ID matches the filename - Use `-c, --config` to specify the correct config file **"File outside scope"** - The file you're modifying isn't in affected_scope - Add it to affected_scope or create a new provenance record - Use `--force` to bypass (not recommended) **"Cycle detected in graph"** - You have circular supersedes relationships - Check that record A doesn't supersede B while B supersedes A - Fix the relationships in the YAML files **Linter is slow** - Reduce the number of shared_repos - Use `--record` to lint only specific records - Consider splitting large monorepos ### Getting Help - Open an issue: https://github.com/livecodelife/linespec/issues - Check existing provenance records in the examples/ - Review the changelog: CHANGELOG.md --- ## License MIT License - See LICENSE file for details. --- # ===== LineSpec Testing (DSL) Reference ===== # LineSpec DSL Reference LineSpec is a deterministic domain-specific language (DSL) for describing service behavior and defining integration tests that execute directly against containerized services. The goal of LineSpec is to: * Provide a concise, readable way to describe service behavior * Enforce strict structural rules to keep parsing simple * Execute deterministically without inference or heuristics * Support database mocking, HTTP interception, and message queue testing --- # Setup Before running `linespec test`, the `linespec:latest` Docker image must exist in your local Docker daemon. This image is used by all protocol proxy sidecars (PostgreSQL, MySQL, HTTP, Kafka, etc.). **Homebrew installs** build the image automatically during `brew install linespec`. If Docker was not running at install time, run: ```bash linespec build ``` **Go install / manual binary** installs must always run `linespec build` once after installation. If you see `Error response from daemon: No such image: linespec:latest`, run `linespec build` to fix it. --- # Core Design Principles 1. Deterministic parsing — no NLP, no guessing. 2. Single entrypoint and single exit per spec. 3. Clear separation between: * Trigger (RECEIVE) * External dependencies (EXPECT) * System response (RESPOND) 4. All payload shapes are defined externally in YAML or JSON files. --- # File Extension Recommended extension: ``` .linespec ``` Example: ``` create_todo_success.linespec ``` --- # DSL Grammar Overview A LineSpec file MUST follow this structure: 1. Exactly one RECEIVE statement 2. Zero or more EXPECT statements 3. Zero or more EXPECT_NOT statements 4. Exactly one RESPOND statement Statements MUST appear in this order: ``` TEST (optional) VARS (optional — declare typed variables) RECEIVE EXPECT (0..n) EXPECT_NOT (0..n) RESPOND ``` No statements may appear after RESPOND. --- # Top-Level Structure Optional test name declaration: ``` TEST ``` If omitted, the filename (without extension) is used as the test name. ## VARS Block (optional) Declare typed variables before `RECEIVE` to pre-generate values with an explicit type and optional constraints: ``` VARS VAR_NAME: [constraint=value ...] VAR_NAME: [constraint=value ...] ... ``` The `VARS` block must appear after `TEST` (if present) and before `RECEIVE`. Each line declares one variable using the format `VAR_NAME: type` followed by zero or more `key=value` constraint pairs. ### Supported types | Type | Default generated value | Supported constraints | |------|------------------------|-----------------------| | `uuid` | RFC 4122 v4 UUID, e.g. `550e8400-e29b-41d4-a716-446655440000` | _(none)_ | | `integer` | Random integer between 1 and 99999 | `min=N`, `max=N` | | `string` | `lowercase_varname_` + 8 random hex chars | `length=N`, `charset=`, `pattern=` | | `enum` | _(required: must provide `values`)_ | `values=a,b,c` | ### Constraints reference **`integer`** - `min=N` — lower bound (inclusive). Default: 1. - `max=N` — upper bound (inclusive). Default: 99999. **`string`** - `length=N` — exact character count of the generated string. - `charset=` — character pool. Supported values: `alphanumeric`, `alpha`, `numeric`, `hex`, `lowercase`, `uppercase`. Default: `hex`. - `pattern=` — generate a string matching a simplified regex. Supports character classes (`[a-z]`, `[A-Z0-9]`), repetition counts (`{N}`), and literal text. For example, `pattern=prov-[0-9]{4}-[a-f0-9]{8}` generates strings like `prov-2026-dab46dda`. **`enum`** - `values=a,b,c` — comma-separated list of allowed values. One is chosen at random each run. ### Why use VARS? Without `VARS`, variable types are inferred from the variable name (a variable ending in `_UUID` gets a UUID; everything else gets a string). `VARS` lets you be explicit — in particular, it is the only way to generate an integer-typed variable that encodes as a JSON number (not a quoted string) in payload files and HTTP responses. ### Resolution order 1. If the variable is already set in the environment, that value is used 2. Otherwise a random value of the declared type is generated and injected into the test container ### Examples **Integer with bounds, string with charset:** ```linespec TEST get_user_with_vars VARS AUTH_TOKEN: string length=32 charset=alphanumeric USER_ID: integer min=1 max=9999 RECEIVE HTTP:GET /api/v1/users/${USER_ID} HEADERS Authorization: Bearer ${AUTH_TOKEN} EXPECT READ:MYSQL users USING_SQL """ SELECT users.* FROM `users` WHERE `users`.`token` = '${AUTH_TOKEN}' LIMIT 1 """ RETURNS {{payloads/user_response.json}} EXPECT READ:MYSQL users USING_SQL_CONTAINS """ WHERE users.id = """ RETURNS {{payloads/user_response.json}} RESPOND HTTP:200 WITH {{payloads/user_public_response.json}} ``` `USER_ID` is declared as `integer`, so `${USER_ID}` is replaced by a number (e.g. `4271`) in the URL and in any payload file that references it. The mock registry receives it as a JSON number, so the service's response body encodes `user_id` as `4271`, not `"4271"`. **String with pattern (provenance ID format):** ```linespec VARS AUTH_TOKEN: string pattern=prov-[0-9]{4}-[a-f0-9]{8} ``` Generates values like `prov-2026-dab46dda` — useful when the service expects a token in a specific structured format. **Enum:** ```linespec VARS ORDER_STATUS: enum values=pending,active,cancelled ``` Picks one of the three values at random each run. --- # Statement Definitions ## 1. RECEIVE Defines the trigger request into the System Under Test (SUT). Syntax: ``` RECEIVE HTTP: [WITH {{}}] [HEADERS : ...] ``` Example: ``` RECEIVE HTTP:POST /api/v1/todos WITH {{todo.yaml}} RECEIVE HTTP:GET /api/v1/users/42 HEADERS Authorization: Bearer token_abc123xyz ``` Rules: * Exactly one RECEIVE per file * MUST appear before any EXPECT or EXPECT_NOT * HTTP method is required * URL is required (full URL including protocol and host) * WITH is optional for HTTP requests without a body * Body must reference an external YAML or JSON file * HEADERS is optional and supports multiple header lines with indentation * Headers are added to the HTTP request (Authorization, X-Custom-Header, etc.) * WITH must come before HEADERS if both are present ### RECEIVE KAFKA Triggers a Kafka consumer test. The proxy seeds a message into the topic so the service picks it up naturally, then waits for all EXPECT mocks to be satisfied. ``` RECEIVE KAFKA: WITH {{}} [TIMEOUT ] ``` Example: ``` RECEIVE KAFKA:order-events WITH {{payloads/order_created.yaml}} TIMEOUT 30s ``` Configure the Kafka broker in `infrastructure` and set `kafka: true`. RESPOND is not required for Kafka consumer tests. ### RECEIVE JOB Triggers a background job test. Works with three modes depending on the `job_backend.type` configured in `.linespec.yml`: **Redis-backed** (`type: redis`): The proxy seeds the job payload into the configured queue key. The worker picks it up via BRPOP/BLPOP/LPOP as it normally would. Completion is detected when all EXPECT mocks are satisfied — no status endpoint needed. ``` RECEIVE JOB WITH {{}} [TIMEOUT ] ``` **Kafka-backed** (`type: kafka`): Seeds the payload into the configured topic (same mechanism as RECEIVE KAFKA). Uses `queue` as the topic name. ``` RECEIVE JOB WITH {{}} [TIMEOUT ] ``` **Scheduled/observe-only** (`type: scheduled`): No seed, no trigger. LineSpec waits for the service's internal scheduler to fire naturally. EXPECT mocks supply any input data the job reads; EXPECT WRITE/HTTP/etc. verify what the job produces. `TIMEOUT` is required to allow the scheduler to fire. ``` RECEIVE JOB TIMEOUT ``` Configure `job_backend` once in `.linespec.yml`: ```yaml job_backend: type: redis # redis | kafka | scheduled queue: worker:jobs # Redis queue key or Kafka topic (omit for scheduled) ``` Examples: ``` # Redis-backed worker (e.g. Asynq, Sidekiq, RQ, Dramatiq) RECEIVE JOB WITH {{payloads/email_job.json}} EXPECT READ:POSTGRESQL ACCESSING_TABLES [users] VERIFY_OPERATION SELECT RETURNS {{payloads/user_row.yaml}} EXPECT HTTP:POST /api/send RETURNS {{payloads/email_sent.json}} EXPECT WRITE:POSTGRESQL ACCESSING_TABLES [email_log] VERIFY_OPERATION INSERT ``` ``` # Scheduled/cron job (observe-only, no seed) RECEIVE JOB TIMEOUT 30s EXPECT HTTP:GET /api/users RETURNS {{payloads/users_list.json}} EXPECT WRITE:POSTGRESQL ACCESSING_TABLES [users] VERIFY_OPERATION INSERT ``` Rules: * Exactly one RECEIVE per file * MUST appear before any EXPECT or EXPECT_NOT * `WITH` is required for redis and kafka types; omit for scheduled (observe-only) * `TIMEOUT` is recommended for all RECEIVE JOB tests; required for scheduled mode * RESPOND is not required for RECEIVE JOB tests * `job_backend` must be configured in `.linespec.yml`; omitting it is an error for redis/kafka types --- ## 2. EXPECT Defines an external dependency interaction that MUST occur during execution. General Syntax: ``` EXPECT [USING_SQL """ """] [USING_SQL_CONTAINS """ """] [WITH {{}}] [RETURNS {{}}] [RETURNS EMPTY] [VERIFY query CONTAINS ''] [VERIFY query NOT_CONTAINS ''] [VERIFY query MATCHES //] ``` The exact format depends on the channel type. ### SQL Matching: Semantic vs Legacy Text Matching LineSpec supports two approaches to matching SQL queries. **Semantic matching** (recommended) is stable across ORM differences; **text matching** (`USING_SQL` / `USING_SQL_CONTAINS`) is legacy and deprecated. --- #### Semantic Matching (Recommended) Semantic matching routes by **which tables a query touches** and lets you add optional verification constraints. This approach is ORM-agnostic: it does not care whether your ORM uses `$1`, `?`, or inline literals, or whether it adds `ORDER BY`, `LIMIT`, or varies column order. **Keywords:** | Keyword | Purpose | |---------|---------| | `ACCESSING_TABLES [t1, t2, ...]` | Route this mock to queries that reference exactly these tables (exact set, no extras) | | `VERIFY_OPERATION SELECT\|INSERT\|UPDATE\|DELETE` | Assert the query type | | `VERIFY_WHERE_COLUMNS [col1, col2, ...]` | Assert that all listed columns appear in the WHERE clause | | `VERIFY_WHERE` (indented block) | Assert specific column values in the WHERE clause | | `VERIFY_WRITTEN_VALUES` (indented block) | Assert column values in INSERT columns list or UPDATE SET clause | | `CALL N` (on the EXPECT line) | Tiebreaker: consumed in ascending N order when multiple mocks match the same query | **When ACCESSING_TABLES is used, the table name on the EXPECT line is optional:** ``` EXPECT READ:MYSQL ACCESSING_TABLES [users] ... ``` **VERIFY_WHERE and VERIFY_WRITTEN_VALUES values:** - Literal: `email: john@example.com` — must match exactly - Quoted: `email: "john@example.com"` — quotes are stripped - Sentinel: `token: PRESENT` — column must exist in the clause with any value - Variables: `id: ${USER_ID}` — resolved at parse time via the test's variable system **Wire-level parameter resolution:** For PostgreSQL extended query protocol, the proxy reads actual parameter values from the Bind message. This means `WHERE id = $1` and `WHERE id = 42` match identically when `$1` is bound to `42`. You never need to know whether your ORM uses `$1` placeholders or inline literals. **Specificity-wins algorithm:** When multiple mocks could match a query, the mock with the most declared VERIFY_ constraints wins. If there is still a tie, `CALL N` ordering is used (lowest N first). This allows general and specific expectations to coexist safely. **Examples:** ``` # Route by table only — one mock per table, no further verification needed EXPECT READ:MYSQL ACCESSING_TABLES [users] VERIFY_OPERATION SELECT RETURNS {{user.yaml}} # Verify a specific WHERE value — stable even if ORM adds ORDER BY or LIMIT EXPECT READ:MYSQL ACCESSING_TABLES [users] VERIFY_WHERE id: 42 RETURNS {{user.yaml}} # Verify WHERE column presence without caring about the value (e.g. auth token) EXPECT READ:MYSQL ACCESSING_TABLES [users] VERIFY_WHERE token: PRESENT RETURNS {{user.yaml}} # Verify columns in the WHERE clause — for multi-column conditions EXPECT READ:MYSQL ACCESSING_TABLES [todos] VERIFY_OPERATION SELECT VERIFY_WHERE_COLUMNS [id, user_id] RETURNS {{todo.yaml}} # Verify written values for INSERT EXPECT WRITE:MYSQL ACCESSING_TABLES [users] VERIFY_OPERATION INSERT VERIFY_WRITTEN_VALUES email: john@example.com name: John Doe WITH {{user_write.yaml}} # CALL N — disambiguate multiple identical queries by ordering EXPECT READ:MYSQL CALL 1 ACCESSING_TABLES [users] VERIFY_OPERATION SELECT RETURNS {{user.yaml}} EXPECT READ:MYSQL CALL 2 ACCESSING_TABLES [users] VERIFY_OPERATION SELECT RETURNS EMPTY # JOIN: two tables in ACCESSING_TABLES — matches queries that touch both EXPECT READ:POSTGRESQL ACCESSING_TABLES [orders, users] VERIFY_OPERATION SELECT VERIFY_WHERE_COLUMNS [user_id] RETURNS {{orders_with_user.yaml}} ``` --- #### Legacy Text Matching (Deprecated) `USING_SQL` and `USING_SQL_CONTAINS` are still parsed and functional but deprecated in favour of semantic matching. | Keyword | Match mode | |---------|-----------| | `USING_SQL` | Exact match after normalization (backticks stripped, whitespace collapsed) | | `USING_SQL_CONTAINS` | Substring match after normalization | ``` # Exact match — fragile against ORM column-order or LIMIT changes EXPECT READ:MYSQL users USING_SQL """ SELECT * FROM users WHERE id = 42 LIMIT 1 """ RETURNS {{user.yaml}} # Substring match — still fragile against parameter binding style ($1 vs ?) EXPECT READ:MYSQL users USING_SQL_CONTAINS """ WHERE users.id = 42 """ RETURNS {{user.yaml}} ``` --- --- ### EXPECT HTTP ``` EXPECT HTTP: [HEADERS : ...] [WITH {{}}] RETURNS {{}} [RESPONSE_HEADERS : ...] ``` `WITH` on an EXPECT asserts the **outbound request body** the service sends to the dependency — it is *not* the response. `RETURNS` is the mocked response. Assert that the service calls a downstream service with a specific request body: ``` EXPECT HTTP:POST http://payment-service.local/charge HEADERS Idempotency-Key: ${KEY} # matched against the actual request headers WITH {{payloads/charge_request.json}} # the request body must match this RETURNS {{payloads/charge_response.json}} # mocked response body RESPONSE_HEADERS Content-Type: application/json ``` Simpler GET with no request body: ``` EXPECT HTTP:GET http://user-service.local/users/42 HEADERS Authorization: Bearer token_abc123xyz RETURNS {{user_info.yaml}} ``` Simulating dependency failures: ``` # Simulate a network/connection failure (TCP close, no response) EXPECT HTTP:GET http://user-service.local/users/42 RETURNS ERROR # Simulate a specific error condition (label is for readability) EXPECT HTTP:POST http://temporal.local/workflows RETURNS ERROR cycle_detected # Return a non-200 HTTP status (no body) EXPECT HTTP:POST http://payment-service.local/charge RETURNS HTTP:429 # Non-200 status WITH a response body: put a `status` field in the RETURNS payload EXPECT HTTP:GET http://auth-service.local/validate RETURNS {{payloads/auth_error.json}} # auth_error.json contains `status: 401` plus the body ``` Rules: * RETURNS is required for HTTP expectations; use `{{file}}`, `ERROR`, `ERROR `, or `HTTP:NNN` * `WITH {{file}}` is optional and matches the **outbound request body**; omit it to match any body for that method/URL * `RETURNS ERROR` closes the TCP connection immediately — the service sees an `io.EOF` * `RETURNS ERROR ` does the same; `` is a label for test readability (e.g. `cycle_detected`) * `RETURNS HTTP:NNN` sends just the status code (no body). To return a non-200 **with** a body, use `RETURNS {{file}}` and include a `status:` field in that payload * HEADERS is optional; headers are matched against the actual request * `RESPONSE_HEADERS` is optional; it sets explicit headers on the mocked response. Without it, `Content-Type` is inferred from the payload file extension (`.json` → `application/json`, `.yaml`/`.yml` → `application/yaml`, `.xml` → `application/xml`) * The proxy intercepts calls to the hostname and returns the mocked response * Tests fail if the HTTP mock is defined but not invoked --- ### EXPECT READ:MYSQL **Semantic matching (recommended):** ``` EXPECT READ:MYSQL [] [CALL N] [ACCESSING_TABLES [, , ...]] [VERIFY_OPERATION SELECT] [VERIFY_WHERE_COLUMNS [, , ...]] [VERIFY_WHERE : ...] RETURNS {{}} ``` **Legacy text matching (deprecated):** ``` EXPECT READ:MYSQL [USING_SQL """ """] [USING_SQL_CONTAINS """ """] RETURNS {{}} ``` Example — semantic, verify by WHERE value: ``` EXPECT READ:MYSQL ACCESSING_TABLES [users] VERIFY_WHERE id: 42 RETURNS {{user_response.yaml}} ``` Example — semantic, verify WHERE column presence: ``` EXPECT READ:MYSQL ACCESSING_TABLES [todos] VERIFY_OPERATION SELECT VERIFY_WHERE_COLUMNS [id, user_id] RETURNS {{todo.yaml}} ``` Example — empty result (uniqueness check): ``` EXPECT READ:MYSQL ACCESSING_TABLES [users] VERIFY_OPERATION SELECT RETURNS EMPTY ``` Rules: * RETURNS is required (either a file or EMPTY) * When `ACCESSING_TABLES` is used the table name on the EXPECT line may be omitted * `ACCESSING_TABLES` requires an exact match on the full set of referenced tables — no partial matches * RETURNS EMPTY generates proper MySQL protocol response for zero rows --- ### EXPECT WRITE:MYSQL **Semantic matching (recommended):** ``` EXPECT WRITE:MYSQL [] [CALL N] [ACCESSING_TABLES [, , ...]] [VERIFY_OPERATION INSERT|UPDATE|DELETE] [VERIFY_WRITTEN_VALUES : ...] [WITH {{}}] [RETURNS {{}}] [VERIFY query CONTAINS ''] [VERIFY query NOT_CONTAINS ''] [VERIFY query MATCHES //] ``` **Legacy text matching (deprecated):** ``` EXPECT WRITE:MYSQL [USING_SQL """ """] [USING_SQL_CONTAINS """ """] [WITH {{}}] [RETURNS {{}}] [VERIFY query CONTAINS ''] [VERIFY query NOT_CONTAINS ''] [VERIFY query MATCHES //] ``` Example — semantic write with written value verification: ``` EXPECT WRITE:MYSQL ACCESSING_TABLES [users] VERIFY_OPERATION INSERT VERIFY_WRITTEN_VALUES email: john@example.com name: John Doe WITH {{user_create.yaml}} VERIFY query MATCHES /\bpassword_digest\b/ ``` Example — INSERT + UPDATE on the same table, disambiguated by CALL N: ``` EXPECT WRITE:MYSQL CALL 1 ACCESSING_TABLES [orders] VERIFY_OPERATION INSERT WITH {{order_insert.yaml}} RETURNS {{order_insert_result.yaml}} EXPECT WRITE:MYSQL CALL 2 ACCESSING_TABLES [orders] VERIFY_OPERATION UPDATE WITH {{order_status_update.yaml}} RETURNS {{order_update_result.yaml}} ``` Where `order_insert_result.yaml` specifies the OK packet values the MySQL driver will read: ```yaml affected_rows: 1 last_insert_id: 42 ``` And `order_update_result.yaml`: ```yaml affected_rows: 1 ``` Rules: * WITH is optional for write operations * When `ACCESSING_TABLES` is used the table name on the EXPECT line may be omitted * RETURNS is optional. When present, the payload must be a YAML object with optional `affected_rows` and `last_insert_id` fields. Omitting RETURNS defaults to `affected_rows=0, last_insert_id=0`. * Multiple WRITE mocks on the same table can be disambiguated by `VERIFY_OPERATION` or `CALL N` * VERIFY clauses validate the actual SQL executed at runtime --- ### EXPECT READ:POSTGRESQL Same semantic matching keywords as READ:MYSQL. PostgreSQL-specific advantage: the proxy reads actual Bind message parameter values, so `WHERE id = $1` is resolved to the bound value before matching. **Semantic matching (recommended):** ``` EXPECT READ:POSTGRESQL [] [CALL N] [ACCESSING_TABLES [, , ...]] [VERIFY_OPERATION SELECT] [VERIFY_WHERE_COLUMNS [, , ...]] [VERIFY_WHERE : ...] RETURNS {{}} ``` Example — match a parameterized query without knowing the ORM's binding style: ``` EXPECT READ:POSTGRESQL ACCESSING_TABLES [notifications] VERIFY_OPERATION SELECT VERIFY_WHERE_COLUMNS [recipient] RETURNS {{notifications.yaml}} ``` This matches `WHERE notifications.recipient = $1` (extended protocol) and `WHERE recipient = 42` (simple query) identically. **Legacy text matching (deprecated):** ``` EXPECT READ:POSTGRESQL [USING_SQL """ """] [USING_SQL_CONTAINS """ """] RETURNS {{}} ``` --- ### EXPECT WRITE:POSTGRESQL **Semantic matching (recommended):** ``` EXPECT WRITE:POSTGRESQL [] [CALL N] [ACCESSING_TABLES [, , ...]] [VERIFY_OPERATION INSERT|UPDATE|DELETE] [VERIFY_WRITTEN_VALUES : ...] [WITH {{}}] [RETURNS {{}}] [VERIFY query CONTAINS ''] [VERIFY query NOT_CONTAINS ''] [VERIFY query MATCHES //] ``` **Legacy text matching (deprecated):** ``` EXPECT WRITE:POSTGRESQL [USING_SQL """ """] [USING_SQL_CONTAINS """ """] [WITH {{}}] [RETURNS {{}}] [VERIFY query CONTAINS ''] [VERIFY query NOT_CONTAINS ''] [VERIFY query MATCHES //] ``` When `RETURNS` is provided for a write operation, the payload controls the `affected_rows` value sent in the CommandComplete tag (e.g. `"UPDATE 3"`). Omitting RETURNS defaults to `affected_rows=1`. ```yaml # write_result.yaml affected_rows: 3 ``` Note: `RETURNING` clauses in the SQL (PostgreSQL's row-returning syntax) are handled separately — the proxy returns a full result set for those, not a RETURNS payload. --- ### EXPECT READ:REDIS ``` EXPECT READ:REDIS RETURNS {{}} ``` Or for a cache miss / empty result: ``` EXPECT READ:REDIS RETURNS EMPTY ``` Example: ``` EXPECT READ:REDIS GET auth:cache:${AUTH_TOKEN} RETURNS {{payloads/cached_user.json}} EXPECT READ:REDIS GET session:${SESSION_ID} RETURNS EMPTY ``` Supported read commands: `GET`, `MGET`, `HGET`, `HGETALL`, `HMGET`, `LRANGE`, `LLEN`, `SMEMBERS`, `SISMEMBER`, `ZRANGE`, `ZRANGEBYSCORE`, `EXISTS`, `TTL`, `TYPE`, `KEYS`, `STRLEN`, `LINDEX`, `BRPOP`, `BLPOP`, `LPOP` (the blocking/pop commands are also used to seed Redis-backed background-job workers — see `RECEIVE JOB`) Rules: * `RETURNS` is required (either a file or `EMPTY`) * `RETURNS EMPTY` encodes as a Redis nil bulk string (`$-1\r\n`) — the correct response for a missing key * The interceptor speaks RESP2 and handles `PING`, `AUTH`, `SELECT`, `HELLO`, and `COMMAND` transparently without registry lookups --- ### EXPECT WRITE:REDIS ``` EXPECT WRITE:REDIS [WITH {{}}] [VERIFY command CONTAINS ''] [VERIFY command NOT_CONTAINS ''] [VERIFY command MATCHES //] [VERIFY key CONTAINS ''] [VERIFY key NOT_CONTAINS ''] [VERIFY key MATCHES //] [VERIFY value CONTAINS ''] [VERIFY value NOT_CONTAINS ''] [VERIFY value MATCHES //] ``` Example: ``` EXPECT WRITE:REDIS SET session:abc WITH {{payloads/session-data.json}} EXPECT WRITE:REDIS DEL user:123 VERIFY command CONTAINS 'DEL' VERIFY key CONTAINS 'user:' ``` Rules: * `WITH` is optional; write commands without a payload return `+OK` * `VERIFY` clauses can validate the command name, key, and/or the value argument independently * Unmatched write commands pass through and return `+OK` --- ### EXPECT READ:MONGODB ``` EXPECT READ:MONGODB RETURNS {{}} ``` Or for empty results: ``` EXPECT READ:MONGODB RETURNS EMPTY ``` Example: ``` EXPECT READ:MONGODB products RETURNS {{payloads/products_list.json}} EXPECT READ:MONGODB users RETURNS EMPTY ``` Rules: * `RETURNS` is required (either a file or `EMPTY`) * The proxy intercepts at the MongoDB wire protocol level (OP_MSG) — no changes needed to the service under test * Payload files may contain a single JSON object or a `{"rows": [...]}` array for multiple documents * JSON `"id"` fields containing a 24-character hex string are automatically mapped to `_id: ObjectID` * Unmatched queries are forwarded transparently to the upstream MongoDB container --- ### EXPECT WRITE:MONGODB ``` EXPECT WRITE:MONGODB [WITH {{}}] ``` Example: ``` EXPECT WRITE:MONGODB products WITH {{payloads/create_product_request.json}} ``` Rules: * `WITH` is optional; all matched write operations return `{n: 1, ok: 1}` (MongoDB write acknowledgement) * The interceptor matches by collection name and command type (insert, update, delete, etc.) * Unmatched write commands are forwarded to the real upstream MongoDB --- ### EXPECT GRPC ``` EXPECT GRPC:/ [WITH {{}}] RETURNS {{}} | EMPTY ``` LineSpec intercepts outbound gRPC calls using an HTTP/2 proxy. The service under test must point its gRPC client at the proxy host — no code changes to the service are required. Enable the proxy in `.linespec.yml`: ```yaml infrastructure: grpc: true dependencies: - name: user-grpc-service type: grpc host: user-grpc-service.local port: 50051 ``` Example: ``` EXPECT GRPC:users.UserService/GetUser WITH {{payloads/get_user_grpc_request.yaml}} RETURNS {{payloads/get_user_grpc_response.json}} ``` Rules: * `ServiceName/MethodName` matches the gRPC route (e.g. `UserService/GetUser` or `users.UserService/GetUser`) * `WITH` is optional; omit it to match any request body for that method * `RETURNS` is required; the proxy returns it as the gRPC response. Use `RETURNS EMPTY` for an empty (nil) response body * Test fails if the expected gRPC call is not observed #### Content-Type handling The gRPC proxy echoes the request's `Content-Type` in its response: * `application/grpc+json` (default) — payloads are JSON. The 5-byte gRPC length-prefixed frame contains a JSON body. This is the original mode and remains the default when no Content-Type is specified. * `application/grpc` — payloads are binary protobuf. When a protobuf descriptor set is configured (see below), `RETURNS` payloads written as JSON are automatically converted to binary protobuf on the wire. Without a descriptor, the raw bytes from the payload file are sent as-is. #### Upstream passthrough When a `type: grpc` dependency specifies a `host` and `port`, the proxy forwards any **unmocked** gRPC calls to that upstream backend via HTTP/2 reverse proxy. This lets you mix mocked and real gRPC backends in a single test — methods you `EXPECT` are intercepted; all others are forwarded transparently. When no upstream is configured (or `infrastructure.grpc: true` is used without gRPC dependencies), unmocked calls return `UNIMPLEMENTED` — preserving backward compatibility with the original pure-mock behavior. #### Protobuf descriptor mocks When the service under test uses native gRPC clients (not JSON), the proxy needs a compiled protobuf descriptor set (`.pb` file) to convert JSON `RETURNS` payloads into binary protobuf on the wire. Configure the descriptor set in `.linespec.yml`: ```yaml # Service-level default — applies to all gRPC dependencies grpc_descriptor_set: proto/workflow.pb dependencies: - name: workflow-service type: grpc host: temporal port: 7233 # Per-dependency override — takes precedence over the service-level default - name: user-grpc-service type: grpc host: user-grpc-service.local port: 50051 grpc_descriptor_set: proto/user.pb ``` The descriptor set is a `FileDescriptorSet` compiled with `protoc`: ```bash protoc --include_imports --descriptor_set_out=workflow.proto workflow.proto ``` Behavior: * When a descriptor is loaded and the request `Content-Type` is `application/grpc`, the proxy converts JSON `RETURNS` payloads to binary protobuf using the descriptor's message definitions * When no descriptor is configured, or when the request `Content-Type` is `application/grpc+json`, payloads are served as-is (JSON or raw bytes) * The runner merges all descriptor sets (service-level + per-dependency) into a single `FileDescriptorSet` before passing it to the proxy container --- ### VERIFY (Validation Rules) The `VERIFY` clause validates the actual query, request, message, or command intercepted at runtime. It can be attached to MySQL, PostgreSQL, HTTP, Kafka/EVENT, gRPC, and Redis EXPECT statements. Use cases include: - Security: Ensuring passwords are hashed before storage - Compliance: Verifying sensitive data is not logged in plain text - Correctness: Confirming proper SQL structure or Redis key naming conventions - Injection prevention: Validating query patterns match expected templates **Targets by channel:** | Channel | Valid VERIFY targets | |---------|----------------------| | MySQL / PostgreSQL | `query` | | HTTP | `headers.`, `body`, `url`, `path` | | Kafka / EVENT / MESSAGE | `key`, `value`, `headers.` | | gRPC | `request_body`, `metadata.` | | Redis | `command`, `key`, `value` | Operators: * `CONTAINS` — Value must include the specified string (substring match) * `NOT_CONTAINS` — Value must NOT include the specified string * `MATCHES` — Value must match the specified regex pattern (full Go regexp support) **SQL VERIFY syntax:** ``` EXPECT [USING_SQL """"""] [WITH {{}}] VERIFY query CONTAINS '' VERIFY query NOT_CONTAINS '' VERIFY query MATCHES // ``` **Redis VERIFY syntax:** ``` EXPECT WRITE:REDIS VERIFY command CONTAINS '' VERIFY key CONTAINS '' VERIFY value CONTAINS '' ``` **Best Practices:** Use `MATCHES` with word boundaries (`\b`) for precise column name matching to avoid false positives with compound column names: ``` # GOOD: Uses word boundaries to match exact column name VERIFY query MATCHES /\bpassword_digest\b/ # BAD: Would also match 'password_digest' in 'old_password_digest_column' VERIFY query CONTAINS 'password_digest' ``` Use `NOT_CONTAINS` with backtick-wrapped column names to avoid matching compound names: ``` # GOOD: Checks for exact column reference VERIFY query NOT_CONTAINS '`password`' # BAD: Would fail on 'password_digest' because it contains 'password' VERIFY query NOT_CONTAINS 'password' ``` Example — Password Hashing (Security): ``` TEST create-user-with-hashing RECEIVE HTTP:POST /api/v1/users WITH {{user_create_request.yaml}} # Ensure password is hashed before storage EXPECT WRITE:MYSQL users WITH {{user_with_hashed_password.yaml}} VERIFY query MATCHES /\bpassword_digest\b/ VERIFY query NOT_CONTAINS '`password`' RESPOND HTTP:201 ``` Example — Redis Key Convention: ``` TEST delete-user-clears-cache RECEIVE HTTP:DELETE /api/v1/users/123 EXPECT WRITE:REDIS DEL user:123 VERIFY command CONTAINS 'DEL' VERIFY key MATCHES /^user:\d+$/ RESPOND HTTP:204 ``` Example — Query Structure Validation: ``` TEST create-order-audit RECEIVE HTTP:POST /api/v1/orders WITH {{order_request.yaml}} # Ensure all inserts include created_at for audit trails EXPECT WRITE:MYSQL orders WITH {{order_data.yaml}} VERIFY query MATCHES /\bcreated_at\b/ VERIFY query MATCHES /INSERT INTO orders \([^)]+\) VALUES \([^)]+\)/ RESPOND HTTP:201 ``` Runtime Behavior: * When the proxy matches an interaction to the mock, it checks all VERIFY rules * If any rule fails, the test fails with a verification error * The actual query or command is shown in the error message for debugging * Verification happens at interception time in MySQL, PostgreSQL, and Redis proxies --- ### EXPECT EVENT / EXPECT MESSAGE Both `EVENT` and `MESSAGE` are aliases for the same functionality: ``` EXPECT EVENT: WITH {{}} EXPECT MESSAGE: WITH {{}} ``` Example: ``` EXPECT EVENT:todo-events WITH {{todo_created_event.yaml}} # Same as: EXPECT MESSAGE:todo-events WITH {{todo_created_event.yaml}} ``` Rules: * Both `EVENT:` and `MESSAGE:` prefixes work identically * WITH file should contain the message payload * Currently, the Kafka proxy passes through to the real broker --- ## 3. EXPECT_NOT Defines an external dependency interaction that must NOT occur during execution. Useful for testing query optimization and ensuring certain operations are avoided. `EXPECT_NOT` and `EXPECT NOT` (space form) are equivalent — both are accepted. Syntax: ``` EXPECT_NOT [USING_SQL """ """] [USING_SQL_CONTAINS """ """] ``` Supported channels (negative expectations are enforced for the SQL and MongoDB stores): - `READ:MYSQL ` / `WRITE:MYSQL
` - `READ:POSTGRESQL
` / `WRITE:POSTGRESQL
` - `READ:MONGODB ` / `WRITE:MONGODB ` Each asserts that the corresponding read (SELECT/find) or write (INSERT/UPDATE/DELETE) does **not** occur. Example — Testing Efficient Queries: ``` TEST efficient-user-lookup RECEIVE HTTP:GET /api/v1/users/123 # Assert that we DON'T do a full table scan EXPECT_NOT READ:MYSQL users USING_SQL """ SELECT * FROM users """ # Should use indexed lookup instead EXPECT READ:MYSQL users USING_SQL """ SELECT * FROM users WHERE id = 123 LIMIT 1 """ RETURNS {{user_response.yaml}} RESPOND HTTP:200 WITH {{user_response.yaml}} ``` Rules: * USING_SQL is optional; if provided, matches that specific query * If no USING_SQL, matches any read/write on the table/collection * Test fails if the forbidden operation is detected --- ## 4. RESPOND Defines the final response of the System Under Test. Syntax: ``` RESPOND HTTP: [WITH {{}}] [NOISE body. body.] ``` Example: ``` RESPOND HTTP:201 WITH {{saved_todo.yaml}} NOISE body.id body.created_at body.updated_at ``` Rules: * Exactly one RESPOND per file * MUST be the final statement * Status MUST be numeric (e.g., 200, 201, 400, 500) * WITH is optional for responses without a body * NOISE must appear after WITH if both are present ### NOISE (optional) Syntax: ``` RESPOND HTTP: WITH {{response.yaml}} NOISE body. body. ``` Rules: - `NOISE` must appear after `RESPOND` (and after `WITH` if present) - Each indented line names one field path to exclude from comparison - Field paths use dot notation matching the JSON response body (e.g. `body.created_at`) - `NOISE` is optional; omit it when no fields need filtering --- # Enforcement Rules The parser MUST enforce: * Exactly one RECEIVE * Exactly one RESPOND * RESPOND must be last * EXPECT/EXPECT_NOT cannot appear before RECEIVE * WITH files must exist (if specified) * RETURNS required for READ operations and HTTP expectations * No duplicate step identifiers Parsing MUST fail if rules are violated. --- # Complete Example ``` TEST create_todo_success RECEIVE HTTP:POST /api/v1/todos WITH {{todo.yaml}} HEADERS Authorization: Bearer token_abc123xyz EXPECT HTTP:GET http://user-service.local/api/v1/users/auth HEADERS Authorization: Bearer token_abc123xyz RETURNS {{user_info.yaml}} EXPECT WRITE:MYSQL todos WITH {{todo_insert.yaml}} EXPECT EVENT:todo-events WITH {{todo_created_event.yaml}} RESPOND HTTP:201 WITH {{saved_todo.yaml}} NOISE body.id body.created_at body.updated_at ``` --- # Environment Variable Interpolation LineSpec supports environment variable substitution using `${VAR_NAME}` syntax. This feature catches hardcoded secrets and ensures your application reads configuration from the environment. ## Syntax ``` ${VAR_NAME} ``` **Variable Name Rules:** - Must start with an uppercase letter (`A-Z`) - Can contain uppercase letters, digits, and underscores (`A-Z0-9_`) - Lowercase variables are treated as literal text (not interpolated) **Valid:** `${API_TOKEN}`, `${DB_HOST_1}`, `${API_VERSION}` **Invalid (treated as literal):** `${api_token}` (lowercase), `${123_VAR}` (starts with digit), `${VAR-NAME}` (hyphen not allowed) ## Where It Works Environment variables can be used in: | Location | Example | |----------|---------| | **HTTP URLs** | `http://api.${DOMAIN}.com/users` | | **HTTP Paths** | `/api/${API_VERSION}/todos` | | **HTTP Headers** | `Authorization: Bearer ${AUTH_TOKEN}` | | **SQL Queries** | `WHERE api_key = '${API_KEY}'` | | **Payload Files** | JSON/YAML files loaded via `WITH {{file.yaml}}` | ## How It Works ### Resolution Order 1. **Check Environment:** If the variable is set in the environment, use that value 2. **Generate Random:** If not set, generate a random value at test runtime 3. **Inject into Container:** Generated values are automatically injected as environment variables into your test container ### Random Value Format When a variable is not defined in the environment, LineSpec generates: ``` {lowercase_var_name}_{16_hex_chars} ``` Example: `api_token_a1b2c3d4e5f6g7h8` This ensures: - Your tests never accidentally match hardcoded secrets - The application must read from environment variables to get the correct value - Same variable used multiple times in a test gets the same generated value ## Use Cases ### Catching Hardcoded Secrets ```linespec TEST authenticate-user RECEIVE HTTP:POST /api/v1/auth WITH {{auth_request.yaml}} HEADERS Authorization: Bearer ${API_TOKEN} EXPECT HTTP:GET http://auth-service.local/validate HEADERS Authorization: Bearer ${API_TOKEN} RETURNS {{auth_response.yaml}} RESPOND HTTP:200 ``` If your application has a hardcoded API token instead of reading from `API_TOKEN`, the test will fail because the generated random value won't match. ### Dynamic Configuration ```linespec RECEIVE HTTP:GET /api/${API_VERSION}/users EXPECT READ:MYSQL users USING_SQL """SELECT * FROM users WHERE env = '${DEPLOY_ENV}'""" RETURNS {{users.yaml}} ``` ### Payload File Interpolation Variables in payload files are also interpolated: **auth_request.yaml:** ```yaml api_key: ${API_KEY} user_id: 123 ``` **Response expectation:** ```yaml # The actual API key value is substituted at test time api_key: api_key_a1b2c3d4e5f6g7h8 status: active ``` ## Limitations - **No default values:** `${VAR:-default}` syntax is not supported - **Strict naming:** Only uppercase with underscores - **No nested interpolation:** Cannot do `${${VAR}}` - **First-use defines:** The first resolution of a variable determines its value for the entire test --- # Configuration Reference (`.linespec.yml`) Every LineSpec test directory requires a `.linespec.yml` file that tells the runner how to build, start, and wire up your service and its dependencies. Below is a fully annotated example covering all supported fields. Only `service` is required; all other sections are optional. ```yaml # ───────────────────────────────────────────── # Service Under Test # ───────────────────────────────────────────── service: name: notification-service # Logical name used in container labels service_dir: notification-service # Directory containing the service source code type: web # web | worker | consumer framework: fastapi # rails | fastapi | django | express | chi | custom # Known frameworks get sensible defaults for start # command, migration command, and warmup endpoint. port: 3002 # Port the service listens on inside the container health_endpoint: /health # Path polled to confirm the service is ready # Docker build / run docker_compose: docker-compose.yml # Path to docker-compose file (relative to service_dir) build_context: . # Docker build context (relative to .linespec.yml) # Override the framework default start command. # Use ${PORT} to inject the configured port at runtime. start_command: uvicorn app.main:app --host 0.0.0.0 --port 3002 # Override the framework default migration command (optional). migration_command: alembic upgrade head # Warmup — wait for the service to accept traffic before running tests. needs_warmup: true # true | false (default: per-framework) warmup_endpoint: /health # Path to poll (overrides framework default) warmup_delay_ms: 100 # Extra delay after health check passes (ms) # Environment variables injected into the service container at test time. environment: DATABASE_URL: postgresql+asyncpg://user:pass@db:5432/mydb REDIS_URL: redis://redis-proxy:6379 KAFKA_BROKERS: kafka:29092 USER_SERVICE_URL: http://user-service:3001/api/v1/users/auth # ───────────────────────────────────────────── # Database (omit if external_db: true) # ───────────────────────────────────────────── # Single-database form (backward compatible): database: type: postgresql # mysql | postgresql | mongodb image: postgres:16-alpine port: 5432 container: db # Service name in docker-compose init_script: init.sql # SQL/JS file run on first startup to seed schema database: mydb username: myuser password: mypassword # For external databases (not managed by LineSpec): host: db.internal # External host (used when external_db: true) # Set to false to disable the protocol-level proxy for this database. # Default: true when infrastructure.database is true. proxy: true # Multi-database form — use `databases:` when a service talks to more than # one database type at the same time (e.g. MySQL + MongoDB). # Each entry gets its own real-DB container and proxy sidecar. # The `name:` field is required; `host:` defaults to the entry's name. databases: - name: mysql type: mysql image: mysql:8.4 port: 3306 database: myapp_development username: myuser password: mypassword proxy: true # Network aliases assigned automatically: # proxy → "mysql" (app connects here) # real → "real-mysql" (proxy forwards here) - name: mongo type: mongodb image: mongo:7 port: 27017 database: myapp_events username: myuser password: mypassword proxy: true # Network aliases: proxy → "mongo", real → "real-mongo" # Environment variables injected per database when using `databases:`: # # First database also receives legacy unprefixed names: # DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD (mysql) # DATABASE_URL (postgresql) # MONGODB_URI (mongodb) # # Every database receives a name-prefixed variant: # _DB_HOST, _DB_PORT, ... (mysql) # _DATABASE_URL (postgresql) # _MONGODB_URI (mongodb) # # Example: databases: [{name: mysql, ...}, {name: mongo, ...}] injects # DB_HOST=mysql, MYSQL_DB_HOST=mysql, MONGO_MONGODB_URI=mongodb://... # ───────────────────────────────────────────── # Infrastructure Flags # ───────────────────────────────────────────── infrastructure: database: true # Start and proxy a database container kafka: true # Start a Kafka container for EVENT/MESSAGE expectations redis: true # Start and proxy a Redis interceptor grpc: false # Start a gRPC proxy sidecar external_db: false # true = don't manage the DB container; connect to host above # Docker image used for protocol proxy sidecars. # Default: linespec:latest proxy_image: linespec:latest # ───────────────────────────────────────────── # Protobuf descriptor set (optional — gRPC) # ───────────────────────────────────────────── # Path to a compiled FileDescriptorSet (.pb) for JSON-to-protobuf conversion. # When set, RETURNS payloads for gRPC mocks are converted from JSON to binary # protobuf when the request Content-Type is application/grpc. # Per-dependency grpc_descriptor_set overrides this value. grpc_descriptor_set: proto/workflow.pb # ───────────────────────────────────────────── # External HTTP / gRPC / service dependencies # ───────────────────────────────────────────── dependencies: - name: user-service type: http host: user-service.local # Hostname the SUT dials port: 3001 proxy: true # Intercept calls to this host host_alias: user-svc # Optional DNS alias inside the test network headers: # Default headers added to all matched requests X-Internal-Token: secret - name: workflow-service type: grpc host: temporal # gRPC upstream hostname (unmocked calls are forwarded here) port: 7233 grpc_descriptor_set: proto/workflow.pb # Optional: overrides service-level default # ───────────────────────────────────────────── # Provenance (optional — enables git hooks) # ───────────────────────────────────────────── provenance: dir: provenance/ enforcement: warn # none | warn | strict commit_tag_required: true # require a record ID in commit messages auto_affected_scope: true # auto-populate affected_scope from git diffs run_associated_specs_on_complete: true # run associated_specs on the open→implemented transition overlap_specs_on_complete: block # completion-time overlap teeth severity: block | warn | off commit_on_status_change: true # auto-commit after open/complete/deprecate transitions manifest_url: "" # source manifest URL (set automatically by `linespec clone`) # Cross-repo provenance resolution (monorepos / shared product repos) cache_ttl_minutes: 60 # shared_repos cache freshness TTL (default: 60) shared_repos: # named remote repositories for cross-repo records - name: product url: https://github.com/acme/product-provenance ref: main # branch or tag (default: main) dir: provenance # subdirectory containing the records (default: provenance) # Voyage AI embeddings for semantic search embedding: provider: voyage index_model: voyage-4-large query_model: voyage-4-lite api_key: "${VOYAGE_API_KEY}" similarity_threshold: 0.50 index_on_complete: true # ───────────────────────────────────────────── # Container & Network Naming (optional) # Template variables: {{ .ServiceName }}, {{ .SpecName }}, {{ .Type }} # ───────────────────────────────────────────── container_naming: database_container: linespec-shared-db network_alias: real-db kafka_container: linespec-shared-kafka proxy_container: proxy-{{ .Type }}-{{ .SpecName }} app_container: app-{{ .SpecName }} migrate_container: linespec-migrate-{{ .ServiceName }} network_name: linespec-shared-net project_mount_path: /app/project # Where the spec directory is mounted registry_mount_path: /app/registry # Where mock payloads are mounted # ───────────────────────────────────────────── # Dynamic Port Allocation (optional) # ───────────────────────────────────────────── ports: dynamic_ports: true # Allocate random host ports (default: true) min_port: 20000 # Lower bound for random port range (default: 10000) max_port: 30000 # Upper bound for random port range (default: 65535) fixed_proxy_port: 0 # Set to a specific port to pin the verify sidecar (0 = dynamic) # ───────────────────────────────────────────── # Schema Discovery (optional — MySQL/PostgreSQL) # ───────────────────────────────────────────── schema_discovery: mode: auto # auto | static | none tables: # Used when mode: static - users - orders exclude_tables: # Tables to ignore in auto mode - schema_migrations - ar_internal_metadata cache_file: .linespec/schema-cache.json # ───────────────────────────────────────────── # Payload Loading (optional) # ───────────────────────────────────────────── payload: directory: payloads # Subdirectory name for payload files (default: payloads) # Note: the HTTP proxy reads the response status from a `status` field in the # RETURNS payload itself (e.g. `status: 401`). This field name is fixed. # ───────────────────────────────────────────── # Misc # ───────────────────────────────────────────── timeout_seconds: 60 # Per-test timeout in seconds (default: 180) strict_passthrough: false # true = fail on any unmatched proxy interaction ``` ## Framework defaults When `framework` is set to a known value, LineSpec supplies defaults that you can selectively override: | Framework | Default start command | Default migration command | Warmup endpoint | |-----------|----------------------|--------------------------|-----------------| | `rails` | `bundle exec rails server -b 0.0.0.0 -p ${PORT}` | `bundle exec rails db:migrate` | `/up` | | `fastapi` | `python -m uvicorn main:app --host 0.0.0.0 --port ${PORT}` | — | `/health` | | `django` | `python manage.py runserver 0.0.0.0:${PORT}` | `python manage.py migrate` | `/health` | | `express` | `npm start` | — | `/health` | | `chi` | `PORT=${PORT} go run .` | — | `/health` | | `custom` | (required — must set `start_command`) | — | `/` | ## Minimal example A minimal config for a FastAPI service with a PostgreSQL database: ```yaml service: name: my-service framework: fastapi port: 8000 database: type: postgresql image: postgres:16-alpine port: 5432 container: db database: mydb username: myuser password: mypassword infrastructure: database: true ``` --- # CLI Usage Execute a spec: ``` linespec test create_todo_success.linespec linespec test /path/to/linespecs/ ``` --- # Future Extensions (Planned) * MATCH and IGNORE rules for fuzzy matching * JSON Schema validation * Snapshot diffing * Spec linting mode * Multi-test suites * Template interpolation ({{variable}} support) --- # Philosophy LineSpec is not a natural language tool. It is a strict behavioral specification language designed to: * Be readable by humans * Be trivial to parse * Execute deterministically * Support modern microservice testing workflows No inference. No heuristics. No ambiguity.