diff --git a/.github/workflows/vocarium-ci.yml b/.github/workflows/vocarium-ci.yml new file mode 100644 index 0000000..316b452 --- /dev/null +++ b/.github/workflows/vocarium-ci.yml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: MPL-2.0 +# The M1 honest gate in CI (ADR-0002). Self-contained steps, SHA-pinned +# actions only (repo has sha_pinning_required). The gate script hard-fails +# on toolchain absence or version skew — never soften that. +name: Vocarium CI +on: + push: + branches: [main] + pull_request: + workflow_dispatch: +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + gate: + name: invoke-slice gate + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Setup Zig (exact pin from .tool-versions) + uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + with: + version: 0.16.0 + - name: Run the invoke-slice gate + run: ./tests/e2e/invoke_slice.sh diff --git a/.gitignore b/.gitignore index ccf874b..bf10088 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,7 @@ dist/ # Coaptation re-anchor basis (occasional, generated on --reanchor when red; not a baseline) .machine_readable/coaptation/receipts/reanchor-basis.a2ml + +# gate toolchain (built from pinned SHA by tests/e2e/invoke_slice.sh) +.gate-tools/ +zig-out/ diff --git a/.tool-versions b/.tool-versions index ce60c32..e43e376 100644 --- a/.tool-versions +++ b/.tool-versions @@ -5,6 +5,6 @@ # gleam 1.8.0 # elixir 1.18.0 # erlang 27.2 -# zig 0.14.0 # idris2 0.7.0 rust nightly +zig 0.16.0 diff --git a/CLAUDE.md b/CLAUDE.md index b352a59..1d30db1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,14 +54,43 @@ Deny: **Nix, Node/npm, TypeScript, Python, Go, AGPL**. (Guix, not Nix.) --- -# This repo: `rsr-template-repo` · clade `rm-rsr-template-repo` +# This repo: `vocarium` -- **Identity** — uuid `a5ea1382-a34c-5334-8a46-a2ebe904c810`; clade `rm` (secondary `gv`); born 2026-03-16; forge `hyperpolymath/rsr-template-repo`. -- **IS** — Canonical RSR-compliant repository template: scaffolding (CI/CD, AI manifests, ABI/FFI standards, container ecosystem, governance) that new hyperpolymath projects are instantiated from. -- **IS-NOT** — a project in its own right · Scaffoldia (the full-featured repo designer) · standards (the canon source this template operationalises) -- **Where it sits** — pipeline position **foundation**; chain `standards → rsr-template-repo → (every estate repo)`; coordination = `standards`. +- **Identity** — forge `hyperpolymath/vocarium`; born 2026-06-26. (The uuid/clade + fields below the fold in `.machine_readable/` still carry the mint-template's + identity and need a mint-time cure + `just claude-md` regeneration.) +- **IS** — the trope database: a store for vokeable particulars (quality, bearer, + context, record) with transformation paths, grades, warrants, use-models, and + verdicts. The storage third of `Haec → Vocarium → Hermeneia`. +- **IS-NOT** — the trope language (haec) · the checker/algebra (trope-checker — + vocarium never absorbs the grading order) · the query language (hermeneia) · + a template. +- **Where it sits** — chain `haec → vocarium → hermeneia`, verified by + `trope-checker`; coordination = `standards`. - **Constraints here** (AGENTIC) — fail-closed; evidence-per-step; no-silent-skip; rerun-after-fix; release-claim-requires-hard-pass. Never: banned langs (above), secrets, state files in repo root, AGPL. Details: `.machine_readable/bot_directives/{methodology,coverage,debt}.a2ml`. - **Golden path** (ANCHOR) — `just test && just quality` → Core tests pass; Quality gates pass; No unresolved critical security findings. - **State** — phase maintenance; maturity production; 95% complete; status active. + + + +# Vocarium architecture (M1, ADR-0002) + +**Pure Zig, wire boundary, no C.** The store engine is `src/store/` → the `voc` +executable. Its only contract is Trope IR v0.2 JSON over process I/O; the +schema at `trope-checker/schemas/trope-ir.schema.json` is the trust boundary +and `tropecheck` is the verifier. No C sources, no C-ABI structs, no bindings +— see `docs/decisions/0002-pure-zig-wire-boundary.adoc` before adding any. + +- Build/test: `zig build test` (Zig version is hard-pinned; see `.tool-versions`). +- Gate: `tests/e2e/invoke_slice.sh` — ingests the canonical fixtures, runs + `voc invoke` under two use-models, and asserts opposite tropecheck verdicts + (use-relativity) plus a rejected unwarranted edge. It hard-fails if the + toolchain is missing; never soften that. +- Store format: append-only JSONL (`{"t":"trope"|"edge"|"use_model"|"warrant"}`), + replayed into in-memory indexes on open. `"t":"revocation"` is reserved. +- Never implement the cross-kind retention order here (`neighbours(max_loss)` + stays `unsupported`) — that algebra belongs to trope-checker. +- Hermeneia integration is M2: a Rust adapter implementing + `hermeneia_store::StoreProvider` by driving `voc`. Do not link; wire only. diff --git a/Justfile b/Justfile index c6c157e..0e5ade2 100644 --- a/Justfile +++ b/Justfile @@ -87,14 +87,7 @@ import? "build/just/assess.just" # Build the project (debug mode) build *args: - @echo "Building {{project}} (debug)..." - # TODO: Replace with your build command - # Examples: - # cargo build {{args}} # Rust - # mix compile {{args}} # Elixir - # zig build {{args}} # Zig - # deno task build {{args}} # Deno/ReScript - @echo "Build complete" + zig build {{args}} # Build in release mode with optimizations build-release *args: @@ -131,14 +124,7 @@ clean-all: clean # Run all tests test *args: - @echo "Running tests..." - # TODO: Replace with your test command - # Examples: - # cargo test {{args}} - # mix test {{args}} - # zig build test {{args}} - # deno test {{args}} - @echo "Tests passed!" + zig build test {{args}} # Run tests with verbose output test-verbose: @@ -152,13 +138,12 @@ test-smoke: # Run end-to-end tests (full pipeline: build → run → verify) e2e: - @echo "Running E2E tests..." - # TODO: Replace with your E2E test command. Examples: - # bash tests/e2e.sh # Shell-based E2E - # npx playwright test # Browser E2E - # mix test test/integration/e2e_test.exs # Elixir E2E - # cargo test --test end_to_end # Rust E2E - @echo "E2E tests passed!" + ./tests/e2e/invoke_slice.sh + +# The M1 honest gate (ADR-0002): unit tests + end-to-end invoke slice against +# tropecheck at its pinned SHA. Hard-fails on toolchain absence or skew. +gate: + ./tests/e2e/invoke_slice.sh # Run aspect tests (cross-cutting concern validation) aspect: diff --git a/README.adoc b/README.adoc index 32c310f..6d1b93d 100644 --- a/README.adoc +++ b/README.adoc @@ -173,3 +173,17 @@ Vocarium is part of a wider language/database ecology: * **KRL** — knot resolution language Vocarium’s role is to hold the particularities that other systems transform, interpret, narrate, or test. + +## Status (honest, 2026-07-28) + +*Implemented (M1 invoke slice, ADR-0002):* the `voc` store engine (pure Zig, +no C) — JSONL replay, warrant-door validation, and `voc invoke` emitting a +Trope IR v0.2 Document verified end-to-end against `tropecheck` (use-relativity +proven by the gate: same trope, opposite verdicts under two use-models). +Run `just gate`. + +*Not yet implemented:* evoke/convoke/transvoke/provoke/revoke, revocation +records, neighbours/nearestness (the retention order stays in trope-checker), +Haec ingestion, the Hermeneia `StoreProvider` adapter (M2). Most docs outside +this README/EXPLAINME are un-instantiated estate template text — trust the +code and `docs/decisions/0002-pure-zig-wire-boundary.adoc` over them. diff --git a/abi.ipkg b/abi.ipkg deleted file mode 100644 index 4b62c0b..0000000 --- a/abi.ipkg +++ /dev/null @@ -1,36 +0,0 @@ --- SPDX-License-Identifier: MPL-2.0 --- Copyright (c) Jonathan D.A. Jewell --- --- Idris2 package for the formally-typed ABI seam. --- --- The seam lives in a single, case-consistent directory src/interface/Abi/ --- (uppercase) so the physical path matches the `module Abi.*` namespace on --- EVERY filesystem. Idris2 requires capitalised namespace components, and a --- lowercase abi/ directory cannot satisfy that without a case-twin symlink --- (src/interface/Abi -> abi) that collides on case-insensitive filesystems --- (macOS/Windows). One case-consistent directory has exactly one path identity --- under case-folding, so the collision is impossible by construction. --- --- A bare `idris2 --check src/interface/Abi/Foo.idr` still warns ("module name --- does not match file name") because Idris derives the expected module from the --- full path; that is expected. Use the package for a real typecheck: --- idris2 --typecheck abi.ipkg (or --build) --- --- The RSR validators accept either Abi/ (canonical, case-consistent) or a --- lowercase abi/ for downstream repos that ship lowercase — but never both. - -package abi - -version = 0.1.0 - -authors = "Jonathan D.A. Jewell" - -brief = "Formally-typed ABI/FFI seam (Idris2 type + layout proofs) for an RSR-templated repository" - -sourcedir = "src/interface" - -depends = base - -modules = Abi.Types - , Abi.Layout - , Abi.Foreign diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..6ab6108 --- /dev/null +++ b/build.zig @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "voc", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/store/main.zig"), + .target = target, + .optimize = optimize, + }), + }); + b.installArtifact(exe); + + const tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path("src/store/store.zig"), + .target = target, + .optimize = optimize, + }), + }); + const run_tests = b.addRunArtifact(tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_tests.step); +} diff --git a/docs-template/README.adoc b/docs-template/README.adoc deleted file mode 100644 index 5b7ed43..0000000 --- a/docs-template/README.adoc +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) -= Documentation Template -:revdate: 2026-05-26 -:status: SEED - -This directory is the *minimum heavy-wiki seed* for a hyperpolymath repo. - -[NOTE] -==== -*This is NOT a duplicate of `docs/`.* `docs/` holds *this* template's own, -fully-written documentation; `docs-template/` is the un-filled *seed* (with -`{{PLACEHOLDER}}` tokens and illustrative example paths) that gets copied into a -*newly scaffolded* repo's `docs/`. Example paths inside `docs-template/` -describe a hypothetical downstream project, so they intentionally do not resolve -against this template's tree (see `.hypatia-ignore` for the matching SD022 -carve-out). -==== - -== How to adopt - -. Copy `docs-template/` → `docs/` in your repo. -. Replace `{{PROJECT_NAME}}`, `{{OWNER}}`, `{{AUTHOR}}`, `{{AUTHOR_EMAIL}}`, - `{{CURRENT_YEAR}}` placeholders. -. Delete any sections that genuinely do not apply (don't leave empty - scaffolding — the 2026-05-26 doc-debt audit flagged 61 estate repos - with empty `docs/` dirs as a separate category of debt). -. Add repo-specific sections as needed. - -== What's in here - -[cols="1,2", options="header"] -|=== -| File | Purpose - -| `README.adoc` | This file. Delete after copying. -| `architecture.adoc` | System overview, component diagram, data flow. -| `usage.adoc` | End-user / consumer perspective. How to invoke / consume / depend on this repo. -| `contributing.adoc` | Developer perspective. Local-dev setup, test running, PR conventions. -| `troubleshooting.adoc` | FAQ-style. Known failure modes + recovery paths. -| `decisions/0001-template.adoc` | ADR (Architecture Decision Record) template. Copy → `0002-...`, `0003-...` etc. -|=== - -== Why these five sections - -The 2026-05-26 estate documentation-debt audit -(link:https://github.com/hyperpolymath/standards/blob/main/docs/audits/2026-05-26-estate-documentation-debt.md[standards#197]) -identified four canonical perspectives every repo should answer: - -. **What is it** — README answers (lives at repo root, not here). -. **How is it built** — `architecture.adoc`. -. **How do I use it** — `usage.adoc`. -. **How do I help build it** — `contributing.adoc`. -. **What goes wrong** — `troubleshooting.adoc`. - -ADRs are the long-running historical record — they don't fit any of -the above but are essential for explaining *why* a particular shape -was chosen. - -== What's intentionally NOT in here - -This seed deliberately omits: - -* `proof-debt.md` — only relevant to proof-bearing repos; see - link:https://github.com/hyperpolymath/standards/blob/main/docs/TRUSTED-BASE-REDUCTION-POLICY.adoc[standards#203]. -* `CHANGELOG.md` — generated automatically by - link:https://github.com/hyperpolymath/standards/blob/main/.github/workflows/changelog-reusable.yml[`changelog-reusable.yml`] - (standards#206), don't write by hand. -* `tech-debt-YYYY-MM-DD.md` — generated by the periodic estate audit. - -== "Heavily-developed wiki" bar (audit definition) - -A repo meets the audit's bar when `docs/` contains ≥ 10 substantive -markdown / asciidoc / rst files with topical organisation. Adopting -this seed gets you to ~5 — add repo-specific deep-dives (typically -one per major component or invariant) to clear the bar. diff --git a/docs-template/architecture.adoc b/docs-template/architecture.adoc deleted file mode 100644 index ae3ba26..0000000 --- a/docs-template/architecture.adoc +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -// SPDX-FileCopyrightText: {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> -= Architecture — {{PROJECT_NAME}} -:revdate: {{CURRENT_YEAR}}-MM-DD - -== System overview - -One paragraph: what this project does and what it does not do. State the -*invariant* — the property that, if violated, would make the whole project -pointless. Future maintainers will read this paragraph first. - -== Component diagram - -Replace this section with an ASCII or Mermaid diagram. Keep it under 20 -lines — anything bigger belongs in `architecture/`. - -[source] ----- -+------------------+ +------------------+ -| Component A | ---> | Component B | -+------------------+ +------------------+ - | - v -+------------------+ -| Component C | -+------------------+ ----- - -== Data flow - -For each external input, describe: - -* **Source**: where it comes from. -* **Validation**: what guarantees we enforce on entry. -* **Transformation**: high-level processing stages. -* **Sink**: where the result goes. - -== Key invariants - -Enumerate the load-bearing invariants of the system. Each should have: - -. A one-line statement. -. The code location(s) that enforce it. -. The failure mode if the invariant is violated. - -Example: - -[cols="1,2,2,2", options="header"] -|=== -| # | Invariant | Enforced at | Failure mode - -| 1 -| All HTTP requests carry a valid `X-Request-ID`. -| `src/request_id.rs` -| Logs become unjoinable; correlation breaks. - -| 2 -| The output buffer is always flushed before exit. -| `src/main.rs:88-92` (Drop impl) -| Last ~16KB of log lost on crash. -|=== - -== Dependencies - -* **Internal**: list other hyperpolymath repos this depends on. -* **External**: SHA-pinned (see `Cargo.lock` / `deno.lock` / etc.). -* **Build-time**: tools required to build (just, deno, cargo, …). - -== Out of scope - -Explicit non-goals. Things we deliberately do *not* do, with a one-line -reason for each. This section saves more time than the rest combined. - -== See also - -* link:./usage.adoc[Usage] — consumer perspective. -* link:./contributing.adoc[Contributing] — developer setup. -* link:./decisions/[ADRs] — historical record of why this shape. diff --git a/docs-template/contributing.adoc b/docs-template/contributing.adoc deleted file mode 100644 index eb22aac..0000000 --- a/docs-template/contributing.adoc +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -// SPDX-FileCopyrightText: {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> -= Contributing — {{PROJECT_NAME}} -:revdate: {{CURRENT_YEAR}}-MM-DD - -== Audience - -Developers working *on* `{{PROJECT_NAME}}`. For consumers (people calling -or depending on it) see link:./usage.adoc[usage.adoc]. - -== Local-dev setup - -Prerequisites — the minimum versions and where to get them: - -* `` v`` — ``. -* `` v`` — ``. -* GPG signing key configured (estate policy — all commits must be - signed). See - link:https://github.com/hyperpolymath/standards/blob/main/docs/secure-coding-training.md[standards/docs/secure-coding-training.md]. - -One-shot setup: - -[source,bash] ----- -git clone git@github.com:{{OWNER}}/{{PROJECT_NAME}}.git -cd {{PROJECT_NAME}} -just setup # installs deps, sets up hooks -just test # runs the full test suite ----- - -== Running tests - -* **Unit**: `just test-unit` — fast, no I/O. -* **Integration**: `just test-int` — uses real services (database, - HTTP, etc.). Estate policy: prefer real over mocked - (see `feedback_integration_tests_real_db` in maintainer's memory). -* **Property**: `just test-prop` — randomised, slower; budget - documented in `docs/proof-debt.md` if applicable. -* **Full**: `just test` — runs all of the above. - -== Code style - -We enforce style via CI (governance-reusable.yml from hyperpolymath/standards). -Locally: - -[source,bash] ----- -just fmt # auto-format -just lint # static checks ----- - -* All commits must be **GPG-signed** (CI enforces; see - link:https://github.com/hyperpolymath/standards[standards]). -* All source files must carry an **SPDX-License-Identifier** header - (CI enforces). -* Conventional commits — `feat`, `fix`, `chore`, `refactor`, `docs`, - `test`, `ci`, `revert` (CHANGELOG is auto-generated from these - via link:https://github.com/hyperpolymath/standards/blob/main/.github/workflows/changelog-reusable.yml[`changelog-reusable.yml`]). - -== Branching & PR workflow - -. Branch off `main` as `claude/` (for AI agents) or - `/` (for humans). -. Make focused, narrow commits — one logical change per commit. -. Open a PR against `main`. -. **Enable auto-merge immediately** on every PR you open - (`gh pr merge --auto --squash`) — estate standing policy - (see standards#196 audit and policies). -. CI must be green. The PR auto-merges when checks pass + reviews land. - -== Adding a new dependency - -. State the *why* in the PR body — what does this dependency unlock? -. Check provenance (maintained, audited, no malicious history). -. Pin to a SHA, not a tag. -. Update `docs/architecture.adoc#Dependencies`. - -== Adding an ADR - -When you make a non-obvious design decision, write it down: - -. Copy `docs/decisions/0001-template.adoc` → `0002-.adoc`. -. Fill in: Context, Decision, Consequences, Alternatives. -. Link the ADR from the README or relevant code as a comment. - -== Reporting issues - -* Bugs in `{{PROJECT_NAME}}`: file at `{{OWNER}}/{{PROJECT_NAME}}/issues`. -* Estate-wide concerns (policy, conventions, CI): file at - `hyperpolymath/standards/issues`. diff --git a/docs-template/decisions/0001-template.adoc b/docs-template/decisions/0001-template.adoc deleted file mode 100644 index eff8bb8..0000000 --- a/docs-template/decisions/0001-template.adoc +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -// SPDX-FileCopyrightText: {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> -= ADR-0001 — Use Architecture Decision Records -:revdate: {{CURRENT_YEAR}}-MM-DD -:status: ACCEPTED - -== Context - -We need a lightweight way to record significant architectural decisions -and the reasoning behind them, so future maintainers (and AI agents) -can understand *why* the project is shaped as it is. - -== Decision - -Adopt link:https://adr.github.io/[Architecture Decision Records (ADRs)] -in `docs/decisions/`, numbered sequentially (0001, 0002, ...). - -* Each ADR is a single `.adoc` file. -* The first ADR (this one) records the decision to use ADRs. -* Status values: PROPOSED, ACCEPTED, DEPRECATED, SUPERSEDED. -* When a decision is overturned, the new ADR records the supersession - and updates the old one's status to `SUPERSEDED by 00NN`. - -== Consequences - -. **Positive**: future readers see *why* without git-archaeology. -. **Positive**: design alternatives are documented, not just the - winning choice. -. **Positive**: ADRs are reviewable in PRs — the design discussion - happens alongside the code that implements it. -. **Negative**: maintenance burden — every non-obvious decision now - warrants an ADR. (We mitigate by keeping ADRs short; "non-obvious" - is a judgment call.) -. **Negative**: ADRs can rot. We accept this; the SUPERSEDED chain - is the recovery mechanism. - -== Alternatives considered - -. **Comments in code** — too local; doesn't capture cross-cutting - decisions. -. **Wiki / external doc** — drifts from code; not in PR review. -. **Commit messages** — too transient and not discoverable. -. **No record** — discarded; this is how every project ends up with - "I don't know why we do it this way" debt. - -== Companion ADRs - -* (None yet — this is the first ADR.) - -== References - -* MADR template — `https://adr.github.io/madr/` -* Estate convention — `hyperpolymath/standards/docs/RSR_OUTLINE.adoc` diff --git a/docs-template/troubleshooting.adoc b/docs-template/troubleshooting.adoc deleted file mode 100644 index 8c62e69..0000000 --- a/docs-template/troubleshooting.adoc +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -// SPDX-FileCopyrightText: {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> -= Troubleshooting — {{PROJECT_NAME}} -:revdate: {{CURRENT_YEAR}}-MM-DD - -This file is a *living FAQ* — known failure modes and recovery paths. -Add a new entry every time you debug something that took more than 15 -minutes; future-you (and other maintainers) will thank you. - -== Known failure modes - -=== `` - -**Symptom**: short reproduction. What does the user see? - -**Cause**: root cause (one or two sentences). - -**Fix**: -[source,bash] ----- - ----- - -**Avoidance**: how to not hit this again (config setting, doc link, etc.). - ---- - -=== `` - -(Replace this and the above example with real entries as you encounter -them.) - -== Diagnostic toolkit - -When something is wrong, run these first: - -[source,bash] ----- -just doctor # runs all available self-checks -just version # prints the version & build hash -just log-tail # last N lines of logs ----- - -== When to escalate - -. Filed an issue with: version, OS, exact command, full error output, - and the symptom in plain English. -. Linked from the issue: the relevant ADR(s) and any related issues. -. For *security*-relevant findings, see - link:./../SECURITY.md[SECURITY.md] — do **not** file public issues. - -== Where to look for more help - -* `docs/architecture.adoc` — internals. -* `docs/decisions/` — historical record of why things are this shape. -* `https://github.com/{{OWNER}}/{{PROJECT_NAME}}/issues?q=is%3Aissue+` — has anyone hit this before? -* `https://github.com/hyperpolymath/standards/issues` — for estate-wide problems. diff --git a/docs-template/usage.adoc b/docs-template/usage.adoc deleted file mode 100644 index 930c763..0000000 --- a/docs-template/usage.adoc +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -// SPDX-FileCopyrightText: {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> -= Usage — {{PROJECT_NAME}} -:revdate: {{CURRENT_YEAR}}-MM-DD - -== Audience - -This document is for *consumers* of `{{PROJECT_NAME}}` — people who depend -on it, call it, or include it. For developers working *on* it, see -link:./contributing.adoc[contributing.adoc]. - -== Quickstart - -The shortest path from zero to a working call: - -[source,bash] ----- -# 1. Install -just install # or: cargo install --path . / deno task install - -# 2. Configure -cp examples/config.example.toml ./config.toml -# Edit minimal required fields. - -# 3. Run -just run # or the equivalent for your language ----- - -Expected output: - -[source] ----- -{{PROJECT_NAME}} v0.0.0 -Listening on 127.0.0.1:8080 ----- - -== Common use cases - -For each canonical use case, give: - -. A one-line statement of the goal. -. The minimal invocation. -. Expected output / side effect. - -=== Use case 1: - -(Replace with real content.) - -=== Use case 2: - -== Configuration reference - -Document every configurable field. Keep this section authoritative — if -the code grows a new option, this table must grow too (CI can be wired -to enforce this). - -[cols="1,1,1,3", options="header"] -|=== -| Field | Type | Default | Meaning - -| `` | `` | `` | -|=== - -== Stability guarantees - -Be explicit: - -* **Stable**: API surfaces that follow SemVer (breaking change = major bump). -* **Unstable**: behind a flag / pre-1.0 / explicitly marked. -* **Internal**: documented for reference but no compatibility promise. - -== Limits & known constraints - -* Maximum supported ``: ``. -* `` is not yet implemented; track at issue `#`. -* On ``, `` behaves differently because ``. - -== See also - -* link:./architecture.adoc[Architecture] — how it works internally. -* link:./troubleshooting.adoc[Troubleshooting] — when things go wrong. diff --git a/docs/decisions/0002-pure-zig-wire-boundary.adoc b/docs/decisions/0002-pure-zig-wire-boundary.adoc new file mode 100644 index 0000000..43304be --- /dev/null +++ b/docs/decisions/0002-pure-zig-wire-boundary.adoc @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 += ADR-0002: Pure-Zig store, wire boundary, no C +:status: Accepted +:date: 2026-07-28 +:deciders: Jonathan D.A. Jewell (owner ruling), Claude (analysis) + +== Status + +Accepted (owner ruling, 2026-07-28). + +== Context + +Vocarium is the storage third of the Haec → Vocarium → Hermeneia stack. Two +first-milestone designs were produced and adversarially judged: (A) a Rust +crate implementing hermeneia's `StoreProvider` trait directly, and (B) a Zig +core exposing a C ABI with Idris2 layout proofs and unsafe Rust bindings. +The judge found (A) integrates cheapest but makes a churning Rust trait the +de facto API; (B)'s C ABI freezes struct layouts before the schema is settled +and pays a three-language maintenance tax for consumers that do not exist. + +The deciding fact: every inter-component contract in this stack is *wire*, +not *linkage* — Haec emits Trope IR JSON, trope-checker consumes Trope IR +JSON, hermeneia round-trips the same shapes. The owner ruled: **no C, +anywhere**. + +== Decision + +1. The store engine is a single *pure-Zig* executable, `voc` + (`src/store/`). No C sources, no C-ABI structs, no bindings. +2. Vocarium's only contract is **Trope IR v0.2 JSON over process I/O** + (`trope-checker/schemas/trope-ir.schema.json` is the trust boundary). + `voc invoke` emits an IR Document; `tropecheck` is the verifier. +3. Hermeneia integration is a later milestone: a small Rust adapter crate + implements `hermeneia_store::StoreProvider` by driving `voc` as a + subprocess. Hermeneia built that trait expressly so vocarium could be + substituted without its planner changing. +4. Vocarium never absorbs the grading algebra. Anything that requires the + cross-kind retention order (e.g. `neighbours(max_loss)`) is answered + `unsupported` until tropecheck exposes it — the order lives in the + checker's calculus, not here. +5. If linkage is ever genuinely forced, Zig `export fn` produces + C-convention symbols without a line of C being written. That door stays + shut until a real, named consumer needs it. + +== Consequences + +* The estate ABI/FFI template (`src/interface/`, `abi.ipkg`) is deleted, not + instantiated — it is a C-ABI shim, the wrong shape for this design. +* The Idris2 layout proofs that accompanied the template go dormant; the + proof obligations that matter here are the checker's (already discharged + in trope-checker's Idris2/Lean4/Agda estate). +* The M1 gate needs no Rust: `zig build test` + an end-to-end invoke slice + against a pinned `tropecheck-rs` build. +* JSONL storage records are kept flat (string ids, no pointers) so a future + engine change — or the M2 adapter — needs no schema change. diff --git a/examples/vocarium-ingest.jsonl b/examples/vocarium-ingest.jsonl new file mode 100644 index 0000000..0f2f5c3 --- /dev/null +++ b/examples/vocarium-ingest.jsonl @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: MPL-2.0 +# Canonical M1 fixture — the README's worked example. +# "authentic language" (de Man) paraphrased as "real language": the paraphrase +# keeps bearer/context/record but attenuates the quality by a quantified 6. +{"t":"warrant","id":"w-editorial","text":"Editorial comparison of the source quotation against the paraphrase; quality loss quantified against the de Man reading."} +{"t":"trope","id":"t_authentic","quality":"authentic language","bearer":"quoted phrase","context":"de Man / rhetoric / Romantic irony","record":"source quotation"} +{"t":"trope","id":"t_real","quality":"real language","bearer":"paraphrase","context":"casual gloss","record":"secondhand paraphrase"} +{"t":"edge","id":"e_paraphrase","effect":"attenuate","inputs":["t_authentic"],"output":"t_real","grade":{"fate":{"quality":{"k":"Attenuated","delta":6},"bearer":{"k":"Present"},"context":{"k":"Present"},"record":{"k":"Present"}},"bond":{"k":"Intact"},"merge":{"k":"Single"}},"warrant_id":"w-editorial","source":"README worked example"} +{"t":"use_model","id":"casual-note","floor":{"fate":{"quality":{"k":"Attenuated","delta":10}}}} +{"t":"use_model","id":"critical-paraphrase","floor":{"fate":{"quality":{"k":"Present"}}}} diff --git a/scripts/validate-template.sh b/scripts/validate-template.sh deleted file mode 100755 index a1608fd..0000000 --- a/scripts/validate-template.sh +++ /dev/null @@ -1,382 +0,0 @@ -#!/bin/bash -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# RSR Template Validation Script -# Verifies that a repository follows the RSR template structure and contains all required files -# -# Exit codes: -# 0 = validation passed -# 1 = validation failed with errors -# 2 = validation failed with warnings (but can proceed) - -set -euo pipefail - -REPO_ROOT="${1:-.}" -VERBOSE="${2:-0}" -ERRORS=0 -WARNINGS=0 - -# ANSI colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Helper functions -log_error() { - echo -e "${RED}ERROR${NC}: $*" >&2 - ERRORS=$((ERRORS + 1)) -} - -log_warning() { - echo -e "${YELLOW}WARN${NC}: $*" >&2 - WARNINGS=$((WARNINGS + 1)) -} - -log_info() { - echo -e "${BLUE}INFO${NC}: $*" >&2 -} - -log_pass() { - echo -e "${GREEN}PASS${NC}: $*" >&2 -} - -check_file_exists() { - local file="$1" - local description="${2:-}" - if [ -f "$REPO_ROOT/$file" ]; then - [ "$VERBOSE" = "1" ] && log_pass "File exists: $file" - return 0 - else - log_error "Required file missing: $file ${description:+(${description})}" - return 1 - fi -} - -check_dir_exists() { - local dir="$1" - local description="${2:-}" - if [ -d "$REPO_ROOT/$dir" ]; then - [ "$VERBOSE" = "1" ] && log_pass "Directory exists: $dir" - return 0 - else - log_error "Required directory missing: $dir ${description:+(${description})}" - return 1 - fi -} - -# Case-tolerant ABI seam checks: accept the canonical case-consistent -# src/interface/Abi/ (matches `module Abi.*`) OR a lowercase src/interface/abi/ -# that some downstream repos ship. Never require BOTH (that would be a case-fold -# collision on case-insensitive filesystems). -check_abi_dir_exists() { - local description="${1:-}" - if [ -d "$REPO_ROOT/src/interface/Abi" ] || [ -d "$REPO_ROOT/src/interface/abi" ]; then - [ "$VERBOSE" = "1" ] && log_pass "Directory exists: src/interface/{Abi,abi}" - return 0 - fi - log_error "Required directory missing: src/interface/Abi ${description:+(${description})}" - return 1 -} -check_abi_file_exists() { - local fname="$1" - local description="${2:-}" - if [ -f "$REPO_ROOT/src/interface/Abi/$fname" ] || [ -f "$REPO_ROOT/src/interface/abi/$fname" ]; then - [ "$VERBOSE" = "1" ] && log_pass "File exists: src/interface/{Abi,abi}/$fname" - return 0 - fi - log_error "Required file missing: src/interface/Abi/$fname ${description:+(${description})}" - return 1 -} - -has_spdx_header() { - local file="$1" - if head -10 "$file" | grep -q "SPDX-License-Identifier"; then - return 0 - fi - return 1 -} - -has_placeholder() { - local file="$1" - if grep -q "{{REPO\|{{OWNER\|{{FORGE\|{{PROJECT\|{{project\|{{AUTHOR" "$file" 2>/dev/null; then - return 0 - fi - return 1 -} - -#============================================================================== -# VALIDATION PHASE 1: CORE STRUCTURE -#============================================================================== - -echo "" -log_info "Phase 1: Core repository structure" -echo "" - -# Root files -check_file_exists "0-AI-MANIFEST.a2ml" "AI manifest (universal entry point)" -check_file_exists "README.adoc" "High-level pitch" -check_file_exists "EXPLAINME.adoc" "Developer deep-dive" -check_file_exists "LICENSE" "License file" -check_file_exists "Justfile" "Task runner" -check_file_exists "AUDIT.adoc" "Release audit gate" - -# Directories -check_dir_exists ".machine_readable" "Machine-readable metadata" -check_dir_exists ".github" "GitHub community metadata" -check_abi_dir_exists "Idris2 ABI definitions" -check_dir_exists "src/interface/ffi" "Zig FFI implementation" -check_dir_exists "src/interface/generated/abi" "Generated C headers" -check_dir_exists "docs" "Documentation" - -#============================================================================== -# VALIDATION PHASE 2: MACHINE-READABLE METADATA -#============================================================================== - -echo "" -log_info "Phase 2: Machine-readable metadata (.machine_readable/)" -echo "" - -check_file_exists ".machine_readable/6a2/STATE.a2ml" "Project state" -check_file_exists ".machine_readable/6a2/META.a2ml" "Architecture decisions" -check_file_exists ".machine_readable/6a2/ECOSYSTEM.a2ml" "Ecosystem position" -check_file_exists ".machine_readable/6a2/anchors/ANCHOR.a2ml" "Semantic boundary anchor" -check_file_exists ".machine_readable/policies/MAINTENANCE-AXES.a2ml" "Maintenance axes" - -#============================================================================== -# VALIDATION PHASE 3: REQUIRED WORKFLOWS (17 minimum) -#============================================================================== - -echo "" -log_info "Phase 3: GitHub Actions workflows" -echo "" - -REQUIRED_WORKFLOWS=( - "hypatia-scan.yml" - "codeql.yml" - "scorecard.yml" - "quality.yml" - "mirror.yml" - "instant-sync.yml" - "guix-policy.yml" - "security-policy.yml" - "wellknown-enforcement.yml" - "workflow-linter.yml" - "npm-bun-blocker.yml" - "ts-blocker.yml" - "secret-scanner.yml" -) - -# Check required workflows -for workflow in "${REQUIRED_WORKFLOWS[@]}"; do - if [ -f "$REPO_ROOT/.github/workflows/$workflow" ]; then - [ "$VERBOSE" = "1" ] && log_pass "Workflow found: $workflow" - else - log_error "Required workflow missing: $workflow" - fi -done - -# Verify all workflows have SPDX headers and proper structure -WORKFLOW_FILES=$(find "$REPO_ROOT/.github/workflows" -name "*.yml" -type f 2>/dev/null || true) -WORKFLOW_COUNT=$(echo "$WORKFLOW_FILES" | grep -c "." || true) - -if [ "$WORKFLOW_COUNT" -ge 15 ]; then - log_pass "Found $WORKFLOW_COUNT workflows (>= 15 expected)" -else - log_warning "Found only $WORKFLOW_COUNT workflows (expected >= 15)" -fi - -# Spot-check workflow files for issues -while IFS= read -r workflow_file; do - if [ -z "$workflow_file" ]; then continue; fi - - # Check for SPDX header (optional in YAML workflows, but best practice) - if ! head -5 "$workflow_file" | grep -q "SPDX-License-Identifier"; then - log_warning "Workflow missing SPDX header: $(basename "$workflow_file")" - fi - - # Check for proper YAML structure - if ! grep -q "^name:" "$workflow_file"; then - log_error "Workflow missing 'name' field: $(basename "$workflow_file")" - fi -done <<< "$WORKFLOW_FILES" - -#============================================================================== -# VALIDATION PHASE 4: ABI/FFI SOURCE FILES -#============================================================================== - -echo "" -log_info "Phase 4: Idris2 ABI and Zig FFI source files" -echo "" - -# Idris2 ABI files -check_abi_file_exists "Types.idr" "Core type definitions" -check_abi_file_exists "Layout.idr" "Memory layout specifications" -check_abi_file_exists "Foreign.idr" "FFI foreign declarations" - -# Zig FFI files -check_file_exists "src/interface/ffi/build.zig" "Zig build configuration" -check_file_exists "src/interface/ffi/src/main.zig" "Zig implementation" -check_file_exists "src/interface/ffi/test/integration_test.zig" "Integration tests" - -#============================================================================== -# VALIDATION PHASE 5: PLACEHOLDER TOKENS -#============================================================================== - -echo "" -log_info "Phase 5: Placeholder token replacement (skipped in template repo)" -echo "" - -# Note: Template repo is allowed to have placeholders -# For derived repos, we'd check that placeholders are replaced -if [ "$(basename "$REPO_ROOT")" = "rsr-template-repo" ]; then - log_pass "Skipping placeholder check for template repo" -else - # Check that key files don't have unresolved placeholders - for file in "$REPO_ROOT/README.adoc" "$REPO_ROOT/Justfile" "$REPO_ROOT/.machine_readable/6a2/STATE.a2ml"; do - if [ -f "$file" ]; then - if has_placeholder "$file"; then - log_warning "File contains unresolved placeholders: $(basename "$file")" - fi - fi - done -fi - -#============================================================================== -# VALIDATION PHASE 6: SPDX LICENSE HEADERS -#============================================================================== - -echo "" -log_info "Phase 6: SPDX License Headers" -echo "" - -# Check source files for SPDX headers (excluding build artifacts) -SOURCE_FILES=$(find "$REPO_ROOT/src" -type f \( -name "*.idr" -o -name "*.zig" \) \ - ! -path "*/.zig-cache/*" ! -path "*/zig-cache/*" 2>/dev/null || true) -SOURCE_COUNT=$(echo "$SOURCE_FILES" | grep -c "." || true) -SPDX_COUNT=0 - -while IFS= read -r src_file; do - if [ -z "$src_file" ]; then continue; fi - if has_spdx_header "$src_file"; then - SPDX_COUNT=$((SPDX_COUNT + 1)) - else - log_warning "Source file missing SPDX header: $(basename "$src_file")" - fi -done <<< "$SOURCE_FILES" - -if [ "$SOURCE_COUNT" -gt 0 ]; then - PERCENT=$((SPDX_COUNT * 100 / SOURCE_COUNT)) - log_pass "SPDX headers: $SPDX_COUNT/$SOURCE_COUNT ($PERCENT%)" - if [ "$PERCENT" -lt 100 ]; then - log_warning "Not all source files have SPDX headers" - fi -fi - -#============================================================================== -# VALIDATION PHASE 7: BUILD VERIFICATION -#============================================================================== - -echo "" -log_info "Phase 7: Build system verification" -echo "" - -# Check Zig build -if [ -f "$REPO_ROOT/src/interface/ffi/build.zig" ]; then - if command -v zig &> /dev/null; then - cd "$REPO_ROOT/src/interface/ffi" - if zig build 2>&1 | grep -q "error"; then - log_error "Zig build failed" - else - log_pass "Zig build successful" - fi - cd - > /dev/null - else - log_warning "Zig compiler not found - skipping Zig build check" - fi -else - log_error "Zig build.zig not found" -fi - -# Check Idris2. Prefer a REAL typecheck via the package (abi.ipkg sets the -# sourcedir so the `module Abi.*` namespace resolves); this catches namespace / -# path / import breakage that a bare per-file `idris2 --check` masks as a -# tolerated "module name does not match file name" warning. -if command -v idris2 &> /dev/null; then - if [ -f "$REPO_ROOT/abi.ipkg" ]; then - if (cd "$REPO_ROOT" && idris2 --typecheck abi.ipkg) > /dev/null 2>&1; then - log_pass "Idris2 ABI typechecks (abi.ipkg)" - else - log_error "Idris2 ABI does NOT typecheck (abi.ipkg)" - fi - else - # No package: fall back to a best-effort per-file syntax check (warns on - # the expected namespace/path mismatch). Look in either case of the dir. - IDS_FILES=$(find "$REPO_ROOT/src/interface/Abi" "$REPO_ROOT/src/interface/abi" -name "*.idr" -type f 2>/dev/null || true) - while IFS= read -r ids_file; do - if [ -z "$ids_file" ]; then continue; fi - if ! idris2 --check "$ids_file" 2>&1 | grep -q "Error"; then - log_pass "Idris2 syntax OK: $(basename "$ids_file")" - else - log_warning "Idris2 syntax issue: $(basename "$ids_file")" - fi - done <<< "$IDS_FILES" - fi -else - log_warning "Idris2 compiler not found - skipping Idris2 syntax checks" -fi - -#============================================================================== -# VALIDATION PHASE 8: DOCUMENTATION -#============================================================================== - -echo "" -log_info "Phase 8: Documentation requirements" -echo "" - -check_file_exists "docs/developer/ABI-FFI-README.adoc" "ABI/FFI documentation" -# TOPOLOGY may live at root or under docs/architecture/, .md or .adoc -if [ -f "$REPO_ROOT/TOPOLOGY.adoc" ] || [ -f "$REPO_ROOT/TOPOLOGY.md" ] || \ - [ -f "$REPO_ROOT/docs/architecture/TOPOLOGY.adoc" ] || [ -f "$REPO_ROOT/docs/architecture/TOPOLOGY.md" ]; then - [ "$VERBOSE" = "1" ] && log_pass "Architecture topology found" -else - log_error "Required file missing: TOPOLOGY (root or docs/architecture/, .adoc or .md)" -fi -# CONTRIBUTING.md may live at root or in .github/ (GitHub auto-discovers either) -if [ -f "$REPO_ROOT/CONTRIBUTING.md" ] || [ -f "$REPO_ROOT/.github/CONTRIBUTING.md" ]; then - [ "$VERBOSE" = "1" ] && log_pass "Contribution guide found" -else - log_error "Required file missing: CONTRIBUTING.md (root or .github/)" -fi - -# Governance can be at root or in docs/governance/ -if [ -f "$REPO_ROOT/GOVERNANCE.adoc" ] || [ -f "$REPO_ROOT/GOVERNANCE.md" ] || [ -d "$REPO_ROOT/docs/governance" ]; then - [ "$VERBOSE" = "1" ] && log_pass "Governance files found" -else - log_warning "Governance documentation not found" -fi - -#============================================================================== -# VALIDATION SUMMARY -#============================================================================== - -echo "" -echo "═══════════════════════════════════════════════════════════════════════════════" -echo "VALIDATION SUMMARY" -echo "═══════════════════════════════════════════════════════════════════════════════" -echo "" -echo -e "Errors: ${RED}${ERRORS}${NC}" -echo -e "Warnings: ${YELLOW}${WARNINGS}${NC}" -echo "" - -if [ "$ERRORS" -eq 0 ]; then - echo -e "${GREEN}✓ Validation PASSED${NC}" - [ "$WARNINGS" -gt 0 ] && echo -e " (with $WARNINGS warnings)" - exit 0 -else - echo -e "${RED}✗ Validation FAILED${NC}" - echo " Please fix the errors above." - exit 1 -fi diff --git a/src/0.1-AI-MANIFEST.a2ml b/src/0.1-AI-MANIFEST.a2ml index c92e124..0f144f8 100644 --- a/src/0.1-AI-MANIFEST.a2ml +++ b/src/0.1-AI-MANIFEST.a2ml @@ -13,7 +13,7 @@ description: | canonical_locations: core: "core/" - interface: "interface/" + store: "store/" bridges: "bridges/" contracts: "contracts/" errors: "errors/" @@ -22,6 +22,6 @@ canonical_locations: invariants: - "Core logic MUST reside in core/" - - "Verified seams MUST reside in interface/" + - "The voc store engine MUST reside in store/ (pure Zig, wire boundary, no C — ADR-0002)" - "Safety constraints MUST reside in contracts/" - "Failure dictionaries MUST reside in errors/" diff --git a/src/interface/0.2-AI-MANIFEST.a2ml b/src/interface/0.2-AI-MANIFEST.a2ml deleted file mode 100644 index 7f0f471..0000000 --- a/src/interface/0.2-AI-MANIFEST.a2ml +++ /dev/null @@ -1,24 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 ---- -### [META] -id: "interface-seams-unit" -level: 2 -parent: "../0.1-AI-MANIFEST.a2ml" - ---- -### [AI_MANIFEST] -description: | - Consolidated "Verified Interface Seams" unit. This directory unifies the - formal specification (ABI), the bridge implementation (FFI), and the - resulting artifacts (Generated). - -canonical_locations: - abi: "abi/" - ffi: "ffi/" - generated: "generated/" - -invariants: - - "ABI MUST be Idris2 (.idr)" - - "FFI MUST be Zig (.zig)" - - "Generated artifacts MUST be C-compatible" - - "The 'Truth' lives in abi/; the 'Implementation' lives in ffi/" diff --git a/src/interface/Abi/0.3-AI-MANIFEST.a2ml b/src/interface/Abi/0.3-AI-MANIFEST.a2ml deleted file mode 100644 index 91cafa0..0000000 --- a/src/interface/Abi/0.3-AI-MANIFEST.a2ml +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 ---- -### [META] -id: "abi-logic" -level: 3 -parent: "../0.2-AI-MANIFEST.a2ml" - ---- -### [AI_MANIFEST] -description: | - Specialised Level 3 logic for abi. diff --git a/src/interface/Abi/Foreign.idr b/src/interface/Abi/Foreign.idr deleted file mode 100644 index ca66b08..0000000 --- a/src/interface/Abi/Foreign.idr +++ /dev/null @@ -1,83 +0,0 @@ --- SPDX-License-Identifier: MPL-2.0 --- Copyright (c) Jonathan D.A. Jewell -||| Foreign Function Interface Bridge -||| -||| This module defines the raw FFI calls and their safe wrappers, -||| implemented in the Zig FFI layer. - -module Abi.Foreign - -import Abi.Types -import Abi.Layout - -%default total - --------------------------------------------------------------------------------- --- Library Lifecycle --------------------------------------------------------------------------------- - -||| Raw FFI call to initialize the library -%foreign "C:rsr_init,librsr" -prim__init : PrimIO Bits64 - -||| Raw FFI call to free library resources -%foreign "C:rsr_free,librsr" -prim__free : Bits64 -> PrimIO () - -||| Safe wrapper for initialization -export -init : IO (Maybe Handle) -init = do - ptr <- primIO prim__init - pure (createHandle ptr) - -||| Safe wrapper for cleanup -export -free : Handle -> IO () -free h = primIO (prim__free h.ptr) - --------------------------------------------------------------------------------- --- Core Operations --------------------------------------------------------------------------------- - -||| Raw FFI call for main processing -%foreign "C:rsr_process,librsr" -prim__process : Bits64 -> Bits32 -> PrimIO Bits32 - -||| Safe wrapper with error handling -export -process : Handle -> Bits32 -> IO (Either Result Bits32) -process h input = do - result <- primIO (prim__process h.ptr input) - if result == 0 - then pure (Left Error) - else pure (Right result) - --------------------------------------------------------------------------------- --- Status and Metrics --------------------------------------------------------------------------------- - -||| Get the current error description from the library -%foreign "C:rsr_get_error,librsr" -prim__getError : Bits64 -> PrimIO (Ptr String) - -||| Detailed error string helper -export -errorDescription : Result -> String -errorDescription Ok = "Success" -errorDescription Error = "Generic error" -errorDescription InvalidParam = "Invalid parameter" -errorDescription Busy = "Library is busy" - --------------------------------------------------------------------------------- --- Documentation --------------------------------------------------------------------------------- - -||| Summary of ABI safety properties: -||| 1. All functions are total (total keyword enforced). -||| 2. Pointers are verified non-null before being wrapped in Handle. -||| 3. Memory layouts are proven C-ABI compliant in Abi.Layout. -||| 4. FFI boundary uses explicitly tagged types from Abi.Types. -public export -abiSafetyGuarantees : String -abiSafetyGuarantees = "RSR-Template ABI: 4 proven safety properties for FFI integration" diff --git a/src/interface/Abi/Layout.idr b/src/interface/Abi/Layout.idr deleted file mode 100644 index e1f4275..0000000 --- a/src/interface/Abi/Layout.idr +++ /dev/null @@ -1,128 +0,0 @@ --- SPDX-License-Identifier: MPL-2.0 --- Copyright (c) Jonathan D.A. Jewell -||| ABI Layout Verification -||| -||| This module provides formal proofs about memory layout, alignment, -||| and padding for C-compatible structs. - -module Abi.Layout - -import Abi.Types -import Data.Vect -import Data.So - -%default total - --------------------------------------------------------------------------------- --- Alignment Invariants --------------------------------------------------------------------------------- - -||| Predicate: n divides m -public export -data Divides : (n, m : Nat) -> Type where - MkDivides : (k : Nat) -> (0 prf : m = k * n) -> Divides n m - -||| Implementation of divides for common sizes -public export -div8_24 : Divides 8 24 -div8_24 = MkDivides 3 Refl - -public export -div4_0 : Divides 4 0 -div4_0 = MkDivides 0 Refl - -public export -div8_8 : Divides 8 8 -div8_8 = MkDivides 1 Refl - -public export -div8_16 : Divides 8 16 -div8_16 = MkDivides 2 Refl - -||| Calculate padding required for an offset to meet alignment -public export -paddingFor : (offset : Nat) -> (alignment : Nat) -> Nat -paddingFor offset 0 = 0 -paddingFor offset alignment = - let m = offset `mod` alignment in - if m == 0 - then 0 - else alignment `minus` m - -||| Align a size up to the next multiple of alignment -public export -alignUp : (size : Nat) -> (alignment : Nat) -> Nat -alignUp size alignment = - size + paddingFor size alignment - --------------------------------------------------------------------------------- --- Struct Model --------------------------------------------------------------------------------- - -||| Representation of a single field in a struct -public export -record Field where - constructor MkField - name : String - offset : Nat - size : Nat - alignment : Nat - -||| Valid memory layout for a C struct -public export -record StructLayout where - constructor MkStructLayout - {n : Nat} - fields : Vect n Field - totalSize : Nat - alignment : Nat - {auto 0 aligned : Divides alignment totalSize} - --------------------------------------------------------------------------------- --- Compliance Predicates --------------------------------------------------------------------------------- - -||| Proof that all fields in a struct are correctly aligned -public export -data FieldsAligned : Vect n Field -> Type where - NoFields : FieldsAligned [] - ConsField : - (f : Field) -> - (rest : Vect n Field) -> - (0 prf : Divides f.alignment f.offset) -> - FieldsAligned rest -> - FieldsAligned (f :: rest) - -||| Predicate: Struct is C-ABI compliant -public export -data CABICompliant : StructLayout -> Type where - CABIOk : (l : StructLayout) -> - (0 prf : FieldsAligned l.fields) -> - CABICompliant l - --------------------------------------------------------------------------------- --- Example and Proofs --------------------------------------------------------------------------------- - -||| Example: struct { int32_t x; int64_t y; double z; } -||| On 64-bit Linux, this should have size 24, alignment 8. -public export -exampleLayout : StructLayout -exampleLayout = - MkStructLayout - [ MkField "x" 0 4 4 -- Bits32 at offset 0 - , MkField "y" 8 8 8 -- Bits64 at offset 8 (4 bytes padding) - , MkField "z" 16 8 8 -- Double at offset 16 - ] - 24 -- Total size: 24 bytes - 8 -- Alignment: 8 bytes - {aligned = div8_24} - -||| Proof that example layout is valid -public export -exampleLayoutValid : CABICompliant Abi.Layout.exampleLayout -exampleLayoutValid = CABIOk Abi.Layout.exampleLayout ( - ConsField (MkField "x" 0 4 4) _ div4_0 ( - ConsField (MkField "y" 8 8 8) _ div8_8 ( - ConsField (MkField "z" 16 8 8) _ div8_16 ( - NoFields)))) diff --git a/src/interface/Abi/README.adoc b/src/interface/Abi/README.adoc deleted file mode 100644 index 4a99517..0000000 --- a/src/interface/Abi/README.adoc +++ /dev/null @@ -1,3 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -= abi Logic diff --git a/src/interface/Abi/Types.idr b/src/interface/Abi/Types.idr deleted file mode 100644 index 9d30051..0000000 --- a/src/interface/Abi/Types.idr +++ /dev/null @@ -1,112 +0,0 @@ --- SPDX-License-Identifier: MPL-2.0 --- Copyright (c) Jonathan D.A. Jewell -||| ABI Type Definitions Template -||| -||| This module defines the Application Binary Interface (ABI) for this library. -||| All type definitions include formal proofs of correctness. - -module Abi.Types - -import Data.Bits -import Data.So -import Data.Vect -import Decidable.Equality - -%default total - --------------------------------------------------------------------------------- --- Platform Model --------------------------------------------------------------------------------- - -||| Target platforms for the FFI bridge -public export -data Platform = Linux | MacOS | Windows | WASM | RISCV - -||| Pointer size in bits per platform -public export -ptrSize : Platform -> Nat -ptrSize Linux = 64 -ptrSize MacOS = 64 -ptrSize Windows = 64 -ptrSize WASM = 32 -ptrSize RISCV = 64 - -||| Current target platform (detected at compile-time) -public export -thisPlatform : Platform -thisPlatform = Linux -- Simplified for template - --------------------------------------------------------------------------------- --- Core Types --------------------------------------------------------------------------------- - -||| Return codes for FFI calls -public export -data Result = Ok | Error | InvalidParam | Busy - -||| Results are decidably equal -public export -implementation DecEq Result where - decEq Ok Ok = Yes Refl - decEq Error Error = Yes Refl - decEq InvalidParam InvalidParam = Yes Refl - decEq Busy Busy = Yes Refl - decEq Ok Error = No (\case Refl impossible) - decEq Ok InvalidParam = No (\case Refl impossible) - decEq Ok Busy = No (\case Refl impossible) - decEq Error Ok = No (\case Refl impossible) - decEq Error InvalidParam = No (\case Refl impossible) - decEq Error Busy = No (\case Refl impossible) - decEq InvalidParam Ok = No (\case Refl impossible) - decEq InvalidParam Error = No (\case Refl impossible) - decEq InvalidParam Busy = No (\case Refl impossible) - decEq Busy Ok = No (\case Refl impossible) - decEq Busy Error = No (\case Refl impossible) - decEq Busy InvalidParam = No (\case Refl impossible) - -||| Opaque handle for library resources -||| Invariant: Handle pointer must be non-null -public export -record Handle where - constructor MkHandle - ptr : Bits64 - 0 prf : So (ptr /= 0) - -||| Returns Nothing if pointer is null -public export -createHandle : Bits64 -> Maybe Handle -createHandle 0 = Nothing -createHandle ptr = case decSo (ptr /= 0) of - Yes p => Just (MkHandle ptr p) - No _ => Nothing - --------------------------------------------------------------------------------- --- C-Types Mapping --------------------------------------------------------------------------------- - -||| Tagged types for C-FFI boundary -public export -data CType = CInt | CUInt | CLong | CULong | CPtrType - -||| Pointer type for platform -public export -CPtr : Platform -> CType -> Type -CPtr p _ = Bits64 -- Simplified for 64-bit template - -||| Size of C types (platform-specific) -public export -cSizeOf : (p : Platform) -> (t : CType) -> Nat -cSizeOf p CInt = 4 -cSizeOf p CUInt = 4 -cSizeOf p CLong = 8 -cSizeOf p CULong = 8 -cSizeOf p CPtrType = 8 - -||| Alignment of C types (platform-specific) -public export -cAlignOf : (p : Platform) -> (t : CType) -> Nat -cAlignOf p CInt = 4 -cAlignOf p CUInt = 4 -cAlignOf p CLong = 8 -cAlignOf p CULong = 8 -cAlignOf p CPtrType = 8 diff --git a/src/interface/README.adoc b/src/interface/README.adoc deleted file mode 100644 index 4fde708..0000000 --- a/src/interface/README.adoc +++ /dev/null @@ -1,3 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -= interface Unit diff --git a/src/interface/ffi/0.3-AI-MANIFEST.a2ml b/src/interface/ffi/0.3-AI-MANIFEST.a2ml deleted file mode 100644 index bf456ae..0000000 --- a/src/interface/ffi/0.3-AI-MANIFEST.a2ml +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 ---- -### [META] -id: "ffi-logic" -level: 3 -parent: "../0.2-AI-MANIFEST.a2ml" - ---- -### [AI_MANIFEST] -description: | - Specialised Level 3 logic for ffi. diff --git a/src/interface/ffi/README.adoc b/src/interface/ffi/README.adoc deleted file mode 100644 index ca14320..0000000 --- a/src/interface/ffi/README.adoc +++ /dev/null @@ -1,3 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -= ffi Logic diff --git a/src/interface/ffi/build.zig b/src/interface/ffi/build.zig deleted file mode 100644 index 2607c11..0000000 --- a/src/interface/ffi/build.zig +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -// -// Template FFI Build Configuration (Zig 0.15.2+) -// Note: This is a minimal build file that demonstrates Zig integration - -const std = @import("std"); - -pub fn build(b: *std.Build) void { - _ = b.standardTargetOptions(.{}); - _ = b.standardOptimizeOption(.{}); - - // In Zig 0.15+, tests are run directly with: - // zig build-exe -ftest-runner src/main.zig - // zig build-exe -ftest-runner test/integration_test.zig - // - // This minimal build file provides scaffolding for future expansion. - // Tests can be invoked via command line without explicit build.zig configuration. -} diff --git a/src/interface/ffi/src/0.4-AI-MANIFEST.a2ml b/src/interface/ffi/src/0.4-AI-MANIFEST.a2ml deleted file mode 100644 index 5b5f1b1..0000000 --- a/src/interface/ffi/src/0.4-AI-MANIFEST.a2ml +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 ---- -### [META] -id: "src-unit" -level: 4 -parent: "../0.3-AI-MANIFEST.a2ml" - ---- -### [AI_MANIFEST] -description: | - Src logic at level 4. diff --git a/src/interface/ffi/src/README.adoc b/src/interface/ffi/src/README.adoc deleted file mode 100644 index d5631f6..0000000 --- a/src/interface/ffi/src/README.adoc +++ /dev/null @@ -1,3 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -= Src Logic diff --git a/src/interface/ffi/src/main.zig b/src/interface/ffi/src/main.zig deleted file mode 100644 index f1b2633..0000000 --- a/src/interface/ffi/src/main.zig +++ /dev/null @@ -1,275 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -// {{PROJECT}} FFI Implementation -// -// This module implements the C-compatible FFI declared in src/abi/Foreign.idr -// All types and layouts must match the Idris2 ABI definitions. -// - -const std = @import("std"); - -// Version information (keep in sync with project) -const VERSION = "0.1.0"; -const BUILD_INFO = "{{PROJECT}} built with Zig " ++ @import("builtin").zig_version_string; - -/// Thread-local error storage -threadlocal var last_error: ?[]const u8 = null; - -/// Set the last error message -fn setError(msg: []const u8) void { - last_error = msg; -} - -/// Clear the last error -fn clearError() void { - last_error = null; -} - -//============================================================================== -// Core Types (must match src/abi/Types.idr) -//============================================================================== - -/// Result codes (must match Idris2 Result type) -pub const Result = enum(c_int) { - ok = 0, - @"error" = 1, - invalid_param = 2, - out_of_memory = 3, - null_pointer = 4, -}; - -/// Library handle (opaque to prevent direct access) -pub const Handle = opaque { - // Internal state hidden from C - allocator: std.mem.Allocator, - initialized: bool, - // Add your fields here -}; - -//============================================================================== -// Library Lifecycle -//============================================================================== - -/// Initialize the library -/// Returns a handle, or null on failure -export fn {{project}}_init() ?*Handle { - const allocator = std.heap.c_allocator; - - const handle = allocator.create(Handle) catch { - setError("Failed to allocate handle"); - return null; - }; - - // Initialize handle - handle.* = .{ - .allocator = allocator, - .initialized = true, - }; - - clearError(); - return handle; -} - -/// Free the library handle -export fn {{project}}_free(handle: ?*Handle) void { - const h = handle orelse return; - const allocator = h.allocator; - - // Clean up resources - h.initialized = false; - - allocator.destroy(h); - clearError(); -} - -//============================================================================== -// Core Operations -//============================================================================== - -/// Process data (example operation) -export fn {{project}}_process(handle: ?*Handle, input: u32) Result { - const h = handle orelse { - setError("Null handle"); - return .null_pointer; - }; - - if (!h.initialized) { - setError("Handle not initialized"); - return .@"error"; - } - - // Example processing logic - _ = input; - - clearError(); - return .ok; -} - -//============================================================================== -// String Operations -//============================================================================== - -/// Get a string result (example) -/// Caller must free the returned string -export fn {{project}}_get_string(handle: ?*Handle) ?[*:0]const u8 { - const h = handle orelse { - setError("Null handle"); - return null; - }; - - if (!h.initialized) { - setError("Handle not initialized"); - return null; - } - - // Example: allocate and return a string - const result = h.allocator.dupeZ(u8, "Example result") catch { - setError("Failed to allocate string"); - return null; - }; - - clearError(); - return result.ptr; -} - -/// Free a string allocated by the library -export fn {{project}}_free_string(str: ?[*:0]const u8) void { - const s = str orelse return; - const allocator = std.heap.c_allocator; - - const slice = std.mem.span(s); - allocator.free(slice); -} - -//============================================================================== -// Array/Buffer Operations -//============================================================================== - -/// Process an array of data -export fn {{project}}_process_array( - handle: ?*Handle, - buffer: ?[*]const u8, - len: u32, -) Result { - const h = handle orelse { - setError("Null handle"); - return .null_pointer; - }; - - const buf = buffer orelse { - setError("Null buffer"); - return .null_pointer; - }; - - if (!h.initialized) { - setError("Handle not initialized"); - return .@"error"; - } - - // Access the buffer - const data = buf[0..len]; - _ = data; - - // Process data here - - clearError(); - return .ok; -} - -//============================================================================== -// Error Handling -//============================================================================== - -/// Get the last error message -/// Returns null if no error -export fn {{project}}_last_error() ?[*:0]const u8 { - const err = last_error orelse return null; - - // Return C string (static storage, no need to free) - const allocator = std.heap.c_allocator; - const c_str = allocator.dupeZ(u8, err) catch return null; - return c_str.ptr; -} - -//============================================================================== -// Version Information -//============================================================================== - -/// Get the library version -export fn {{project}}_version() [*:0]const u8 { - return VERSION.ptr; -} - -/// Get build information -export fn {{project}}_build_info() [*:0]const u8 { - return BUILD_INFO.ptr; -} - -//============================================================================== -// Callback Support -//============================================================================== - -/// Callback function type (C ABI) -pub const Callback = *const fn (u64, u32) callconv(.C) u32; - -/// Register a callback -export fn {{project}}_register_callback( - handle: ?*Handle, - callback: ?Callback, -) Result { - const h = handle orelse { - setError("Null handle"); - return .null_pointer; - }; - - const cb = callback orelse { - setError("Null callback"); - return .null_pointer; - }; - - if (!h.initialized) { - setError("Handle not initialized"); - return .@"error"; - } - - // Store callback for later use - _ = cb; - - clearError(); - return .ok; -} - -//============================================================================== -// Utility Functions -//============================================================================== - -/// Check if handle is initialized -export fn {{project}}_is_initialized(handle: ?*Handle) u32 { - const h = handle orelse return 0; - return if (h.initialized) 1 else 0; -} - -//============================================================================== -// Tests -//============================================================================== - -test "lifecycle" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); - - try std.testing.expect({{project}}_is_initialized(handle) == 1); -} - -test "error handling" { - const result = {{project}}_process(null, 0); - try std.testing.expectEqual(Result.null_pointer, result); - - const err = {{project}}_last_error(); - try std.testing.expect(err != null); -} - -test "version" { - const ver = {{project}}_version(); - const ver_str = std.mem.span(ver); - try std.testing.expectEqualStrings(VERSION, ver_str); -} diff --git a/src/interface/ffi/test/0.4-AI-MANIFEST.a2ml b/src/interface/ffi/test/0.4-AI-MANIFEST.a2ml deleted file mode 100644 index e02427f..0000000 --- a/src/interface/ffi/test/0.4-AI-MANIFEST.a2ml +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 ---- -### [META] -id: "test-unit" -level: 4 -parent: "../0.3-AI-MANIFEST.a2ml" - ---- -### [AI_MANIFEST] -description: | - Test logic at level 4. diff --git a/src/interface/ffi/test/README.adoc b/src/interface/ffi/test/README.adoc deleted file mode 100644 index e6ff267..0000000 --- a/src/interface/ffi/test/README.adoc +++ /dev/null @@ -1,3 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -= Test Logic diff --git a/src/interface/ffi/test/integration_test.zig b/src/interface/ffi/test/integration_test.zig deleted file mode 100644 index 484e156..0000000 --- a/src/interface/ffi/test/integration_test.zig +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -// RSR Template FFI Integration Tests -// -// These tests verify that the Zig FFI correctly implements the Idris2 ABI. -// This is a TEMPLATE FILE — when instantiating a new project: -// 1. Replace "template" with your project name in lowercase -// 2. Link against your actual FFI implementation library -// 3. Uncomment the test functions below -// -// For now, this file contains documentation of what tests should exist. - -const std = @import("std"); - -// NOTE: When instantiated, declare the actual FFI functions here: -// extern fn mylib_init() ?*Handle; -// extern fn mylib_free(?*Handle) void; -// ... etc - -// And define Handle appropriately: -// const Handle = opaque {}; - -test "placeholder test - implementation required" { - // This test ensures the file compiles - // Actual tests depend on FFI implementation - try std.testing.expect(true); -} - -// ============================================================================== -// Example tests (uncomment when instantiated with real FFI): -// ============================================================================== -// -// test "lifecycle: create and destroy handle" { -// const handle = mylib_init() orelse return error.InitFailed; -// defer mylib_free(handle); -// } -// -// test "operations: process with valid handle" { -// const handle = mylib_init() orelse return error.InitFailed; -// defer mylib_free(handle); -// -// const result = mylib_process(handle, 42); -// try std.testing.expectEqual(@as(c_int, 0), result); -// } -// -// test "memory safety: double free is safe" { -// const handle = mylib_init() orelse return error.InitFailed; -// mylib_free(handle); -// mylib_free(handle); // Should not crash -// } -// -// test "strings: get string result from handle" { -// const handle = mylib_init() orelse return error.InitFailed; -// defer mylib_free(handle); -// -// const str = mylib_get_string(handle); -// defer if (str) |s| mylib_free_string(s); -// -// try std.testing.expect(str != null); -// } -// -// test "version: returns non-empty version string" { -// const ver = mylib_version(); -// const ver_str = std.mem.span(ver); -// try std.testing.expect(ver_str.len > 0); -// } diff --git a/src/interface/generated/0.3-AI-MANIFEST.a2ml b/src/interface/generated/0.3-AI-MANIFEST.a2ml deleted file mode 100644 index 0088b80..0000000 --- a/src/interface/generated/0.3-AI-MANIFEST.a2ml +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 ---- -### [META] -id: "generated-logic" -level: 3 -parent: "../0.2-AI-MANIFEST.a2ml" - ---- -### [AI_MANIFEST] -description: | - Specialised Level 3 logic for generated. diff --git a/src/interface/generated/README.adoc b/src/interface/generated/README.adoc deleted file mode 100644 index 9e94b29..0000000 --- a/src/interface/generated/README.adoc +++ /dev/null @@ -1,3 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -= generated Logic diff --git a/src/interface/generated/abi/.gitkeep b/src/interface/generated/abi/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/interface/generated/abi/0.4-AI-MANIFEST.a2ml b/src/interface/generated/abi/0.4-AI-MANIFEST.a2ml deleted file mode 100644 index 4eeb580..0000000 --- a/src/interface/generated/abi/0.4-AI-MANIFEST.a2ml +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 ---- -### [META] -id: "abi-unit" -level: 4 -parent: "../0.3-AI-MANIFEST.a2ml" - ---- -### [AI_MANIFEST] -description: | - Abi logic at level 4. diff --git a/src/interface/generated/abi/README.adoc b/src/interface/generated/abi/README.adoc deleted file mode 100644 index f3b3808..0000000 --- a/src/interface/generated/abi/README.adoc +++ /dev/null @@ -1,3 +0,0 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Copyright (c) Jonathan D.A. Jewell -= Abi Logic diff --git a/src/store/emit.zig b/src/store/emit.zig new file mode 100644 index 0000000..97f0123 --- /dev/null +++ b/src/store/emit.zig @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// Trope IR v0.2 Document emitter (the wire boundary, ADR-0002 §2). +// +// The document shape is fixed by trope-checker/schemas/trope-ir.schema.json: +// {version, profile, nodes[], edges[], use_model{output, floor}} +// Grades and floors are re-emitted verbatim from the store's opaque JSON +// values via a small recursive writer — vocarium never interprets them. +// Vocarium-only fields (warrant_id, source) are stripped: the IR edge schema +// is additionalProperties:false; warrants are store-side obligations. + +const std = @import("std"); +const storemod = @import("store.zig"); + +fn writeJsonString(out: *std.ArrayListUnmanaged(u8), a: std.mem.Allocator, s: []const u8) !void { + try out.append(a, '"'); + for (s) |c| { + switch (c) { + '"' => try out.appendSlice(a, "\\\""), + '\\' => try out.appendSlice(a, "\\\\"), + '\n' => try out.appendSlice(a, "\\n"), + '\r' => try out.appendSlice(a, "\\r"), + '\t' => try out.appendSlice(a, "\\t"), + else => { + if (c < 0x20) { + var buf: [6]u8 = undefined; + const hex = std.fmt.bufPrint(&buf, "\\u{x:0>4}", .{c}) catch unreachable; + try out.appendSlice(a, hex); + } else { + try out.append(a, c); + } + }, + } + } + try out.append(a, '"'); +} + +fn writeValue(out: *std.ArrayListUnmanaged(u8), a: std.mem.Allocator, v: std.json.Value) !void { + switch (v) { + .null => try out.appendSlice(a, "null"), + .bool => |b| try out.appendSlice(a, if (b) "true" else "false"), + .integer => |i| { + var buf: [24]u8 = undefined; + const s = std.fmt.bufPrint(&buf, "{d}", .{i}) catch unreachable; + try out.appendSlice(a, s); + }, + .float => |f| { + var buf: [40]u8 = undefined; + const s = std.fmt.bufPrint(&buf, "{d}", .{f}) catch unreachable; + try out.appendSlice(a, s); + }, + .number_string => |s| try out.appendSlice(a, s), + .string => |s| try writeJsonString(out, a, s), + .array => |arr| { + try out.append(a, '['); + for (arr.items, 0..) |item, i| { + if (i != 0) try out.append(a, ','); + try writeValue(out, a, item); + } + try out.append(a, ']'); + }, + .object => |obj| { + try out.append(a, '{'); + var i: usize = 0; + var it = obj.iterator(); + while (it.next()) |entry| { + if (i != 0) try out.append(a, ','); + try writeJsonString(out, a, entry.key_ptr.*); + try out.append(a, ':'); + try writeValue(out, a, entry.value_ptr.*); + i += 1; + } + try out.append(a, '}'); + }, + } +} + +/// Emit the invoke-slice Document for `target` under `um`, over the edges +/// selected by Store.pathInto. Output is a complete Trope IR v0.2 document. +pub fn emitDocument( + a: std.mem.Allocator, + store: *storemod.Store, + trope_ids: [][]const u8, + edge_indices: []usize, + um: storemod.UseModel, + target_id: []const u8, +) ![]u8 { + var out: std.ArrayListUnmanaged(u8) = .empty; + try out.appendSlice(a, + \\{"$schema":"https://github.com/hyperpolymath/trope-checker/schemas/trope-ir.schema.json","version":"0.2","profile":"prevent","nodes":[ + ); + for (trope_ids, 0..) |tid, i| { + if (i != 0) try out.append(a, ','); + const tr = store.tropes.get(tid).?; + try out.appendSlice(a, "{\"id\":"); + try writeJsonString(&out, a, tr.id); + try out.appendSlice(a, ",\"type\":\"Trope\",\"present\":["); + var buf: [4][]const u8 = undefined; + const present = tr.presentFields(&buf); + for (present, 0..) |f, j| { + if (j != 0) try out.append(a, ','); + try writeJsonString(&out, a, f); + } + try out.appendSlice(a, "]}"); + } + try out.appendSlice(a, "],\"edges\":["); + for (edge_indices, 0..) |ei, i| { + if (i != 0) try out.append(a, ','); + const e = store.edges.items[ei]; + try out.appendSlice(a, "{\"id\":"); + try writeJsonString(&out, a, e.id); + try out.appendSlice(a, ",\"effect\":"); + try writeJsonString(&out, a, e.effect); + try out.appendSlice(a, ",\"inputs\":["); + for (e.inputs, 0..) |inp, j| { + if (j != 0) try out.append(a, ','); + try writeJsonString(&out, a, inp); + } + try out.appendSlice(a, "],\"output\":"); + try writeJsonString(&out, a, e.output); + try out.appendSlice(a, ",\"grade\":"); + try writeValue(&out, a, e.grade); + try out.append(a, '}'); + } + try out.appendSlice(a, "],\"use_model\":{\"id\":"); + try writeJsonString(&out, a, um.id); + try out.appendSlice(a, ",\"output\":"); + try writeJsonString(&out, a, target_id); + try out.appendSlice(a, ",\"floor\":"); + try writeValue(&out, a, um.floor); + try out.appendSlice(a, "}}\n"); + return out.toOwnedSlice(a); +} + +test "emitted document round-trips as JSON with required keys" { + const fixture = + \\{"t":"warrant","id":"w1","text":""} + \\{"t":"trope","id":"a","quality":"authentic language","bearer":"quoted phrase","context":"ctx","record":"src"} + \\{"t":"trope","id":"b","quality":"real language","bearer":"paraphrase","context":"","record":""} + \\{"t":"edge","id":"e1","effect":"attenuate","inputs":["a"],"output":"b","grade":{"fate":{"quality":{"k":"Attenuated","delta":6},"bearer":{"k":"Present"},"context":{"k":"Present"},"record":{"k":"Present"}},"bond":{"k":"Intact"},"merge":{"k":"Single"}},"warrant_id":"w1"} + \\{"t":"use_model","id":"casual-note","floor":{"fate":{"quality":{"k":"Attenuated","delta":10}}}} + ; + var diag = storemod.Diag{}; + var s = try storemod.Store.load(std.testing.allocator, fixture, &diag); + defer s.deinit(); + const a = s.arena.allocator(); + var eidx: std.ArrayListUnmanaged(usize) = .empty; + var tids: std.ArrayListUnmanaged([]const u8) = .empty; + try s.pathInto("b", &eidx, &tids); + const doc = try emitDocument(a, &s, tids.items, eidx.items, s.use_models.get("casual-note").?, "b"); + + const parsed = try std.json.parseFromSliceLeaky(std.json.Value, a, doc, .{}); + const obj = parsed.object; + try std.testing.expectEqualStrings("0.2", obj.get("version").?.string); + try std.testing.expectEqualStrings("prevent", obj.get("profile").?.string); + try std.testing.expectEqual(@as(usize, 2), obj.get("nodes").?.array.items.len); + try std.testing.expectEqual(@as(usize, 1), obj.get("edges").?.array.items.len); + // vocarium-only fields must be stripped from IR edges + const edge0 = obj.get("edges").?.array.items[0].object; + try std.testing.expect(edge0.get("warrant_id") == null); + try std.testing.expect(edge0.get("source") == null); + // grade re-emitted verbatim + const delta = edge0.get("grade").?.object.get("fate").?.object.get("quality").?.object.get("delta").?; + try std.testing.expectEqual(@as(i64, 6), delta.integer); + // use_model targets the invoked trope + try std.testing.expectEqualStrings("b", obj.get("use_model").?.object.get("output").?.string); +} diff --git a/src/store/main.zig b/src/store/main.zig new file mode 100644 index 0000000..164cadb --- /dev/null +++ b/src/store/main.zig @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// voc — the vocarium store CLI (M1 invoke slice, ADR-0002). +// +// voc ingest +// Replay + validate; print record counts. Exit 2 on any validation +// fault (unknown refs, unwarranted edges, malformed records). +// +// voc invoke --store --use-model +// Emit the Trope IR v0.2 Document for the path into under +// the named use-model, on stdout. Pipe it to `tropecheck` for the +// verdict; voc itself renders no verdict. +// +// Exit codes mirror tropecheck conventions: +// 0 success · 2 validation fault · 3 io error · 64 usage + +const std = @import("std"); +const Io = std.Io; +const storemod = @import("store.zig"); +const emitmod = @import("emit.zig"); + +const usage_text = + \\voc — vocarium trope store (M1 invoke slice) + \\ + \\usage: + \\ voc ingest + \\ voc invoke --store --use-model + \\ +; + +fn errPrint(io: Io, comptime fmt: []const u8, args: anytype) void { + var buf: [1024]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, fmt, args) catch return; + Io.File.stderr().writeStreamingAll(io, msg) catch {}; +} + +pub fn main(init: std.process.Init) u8 { + const io = init.io; + const gpa = init.gpa; + const arena = init.arena.allocator(); + + const args = init.minimal.args.toSlice(arena) catch return 3; + if (args.len < 2) { + errPrint(io, "{s}", .{usage_text}); + return 64; + } + const cmd = args[1]; + + if (std.mem.eql(u8, cmd, "ingest")) { + if (args.len != 3) { + errPrint(io, "{s}", .{usage_text}); + return 64; + } + return runIngest(io, gpa, args[2]); + } + if (std.mem.eql(u8, cmd, "invoke")) { + return runInvoke(io, gpa, args[2..]); + } + errPrint(io, "{s}", .{usage_text}); + return 64; +} + +fn loadStore(io: Io, gpa: std.mem.Allocator, path: []const u8, diag: *storemod.Diag) ?storemod.Store { + const bytes = Io.Dir.cwd().readFileAlloc(io, path, gpa, .limited(64 * 1024 * 1024)) catch |e| { + errPrint(io, "voc: cannot read {s}: {s}\n", .{ path, @errorName(e) }); + return null; + }; + defer gpa.free(bytes); + return storemod.Store.load(gpa, bytes, diag) catch null; +} + +fn runIngest(io: Io, gpa: std.mem.Allocator, path: []const u8) u8 { + var diag = storemod.Diag{}; + var s = loadStore(io, gpa, path, &diag) orelse { + if (diag.msg.len != 0) { + errPrint(io, "voc: validation fault (line {d}): {s}\n", .{ diag.line, diag.msg }); + return 2; + } + return 3; + }; + defer s.deinit(); + errPrint(io, "voc: ok — {d} tropes, {d} edges, {d} use-models, {d} warrants\n", .{ + s.tropes.count(), s.edges.items.len, s.use_models.count(), s.warrants.count(), + }); + return 0; +} + +fn runInvoke(io: Io, gpa: std.mem.Allocator, rest: []const [:0]const u8) u8 { + var store_path: ?[]const u8 = null; + var um_id: ?[]const u8 = null; + var target: ?[]const u8 = null; + var i: usize = 0; + while (i < rest.len) : (i += 1) { + const arg = rest[i]; + if (std.mem.eql(u8, arg, "--store")) { + i += 1; + if (i >= rest.len) return 64; + store_path = rest[i]; + } else if (std.mem.eql(u8, arg, "--use-model")) { + i += 1; + if (i >= rest.len) return 64; + um_id = rest[i]; + } else if (target == null) { + target = arg; + } else { + errPrint(io, "{s}", .{usage_text}); + return 64; + } + } + const sp = store_path orelse { + errPrint(io, "{s}", .{usage_text}); + return 64; + }; + const um_name = um_id orelse { + errPrint(io, "{s}", .{usage_text}); + return 64; + }; + const target_id = target orelse { + errPrint(io, "{s}", .{usage_text}); + return 64; + }; + + var diag = storemod.Diag{}; + var s = loadStore(io, gpa, sp, &diag) orelse { + if (diag.msg.len != 0) { + errPrint(io, "voc: validation fault (line {d}): {s}\n", .{ diag.line, diag.msg }); + return 2; + } + return 3; + }; + defer s.deinit(); + + const um = s.use_models.get(um_name) orelse { + errPrint(io, "voc: unknown use-model: {s}\n", .{um_name}); + return 2; + }; + if (s.tropes.get(target_id) == null) { + errPrint(io, "voc: unknown trope: {s}\n", .{target_id}); + return 2; + } + + const a = s.arena.allocator(); + var eidx: std.ArrayListUnmanaged(usize) = .empty; + var tids: std.ArrayListUnmanaged([]const u8) = .empty; + s.pathInto(target_id, &eidx, &tids) catch return 3; + const doc = emitmod.emitDocument(a, &s, tids.items, eidx.items, um, target_id) catch return 3; + Io.File.stdout().writeStreamingAll(io, doc) catch return 3; + return 0; +} diff --git a/src/store/store.zig b/src/store/store.zig new file mode 100644 index 0000000..2ee2cfd --- /dev/null +++ b/src/store/store.zig @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Jonathan D.A. Jewell +// +// The vocarium store: append-only JSONL replayed into in-memory indexes. +// +// Records (one JSON object per line): +// {"t":"trope", "id","quality","bearer","context","record"} (strings; "" = field absent) +// {"t":"edge", "id","effect","inputs":[..],"output","grade":{..},"warrant_id","source"?} +// {"t":"use_model", "id","floor":{..}} +// {"t":"warrant", "id","text"?} +// {"t":"revocation", ...} reserved for M2; accepted at parse, rejected at validate +// +// Grades and floors are carried as opaque JSON values: vocarium stores the +// loss-shape claim, it never interprets the grading algebra (ADR-0002 §4). + +const std = @import("std"); + +pub const StoreError = error{ + Validation, // maps to exit 2 (mirrors tropecheck's validation-fault) + OutOfMemory, +}; + +pub const Trope = struct { + id: []const u8, + quality: []const u8, + bearer: []const u8, + context: []const u8, + record: []const u8, + + pub fn presentFields(self: Trope, buf: *[4][]const u8) [][]const u8 { + var n: usize = 0; + if (self.quality.len != 0) { + buf[n] = "quality"; + n += 1; + } + if (self.bearer.len != 0) { + buf[n] = "bearer"; + n += 1; + } + if (self.context.len != 0) { + buf[n] = "context"; + n += 1; + } + if (self.record.len != 0) { + buf[n] = "record"; + n += 1; + } + return buf[0..n]; + } +}; + +pub const Edge = struct { + id: []const u8, + effect: []const u8, + inputs: [][]const u8, + output: []const u8, + grade: std.json.Value, // opaque loss-shape + warrant_id: []const u8, + source: []const u8, // "" if absent +}; + +pub const UseModel = struct { + id: []const u8, + floor: std.json.Value, // opaque floor grade +}; + +const effects = [_][]const u8{ "preserve", "project", "collapse", "detach", "attenuate", "fuse" }; + +fn isEffect(s: []const u8) bool { + for (effects) |e| if (std.mem.eql(u8, e, s)) return true; + return false; +} + +/// Diagnostics for validation failures — filled in when load() returns +/// StoreError.Validation (line 0 = post-replay referential check). +pub const Diag = struct { + line: usize = 0, + msg: []const u8 = "", +}; + +pub const Store = struct { + arena: *std.heap.ArenaAllocator, + tropes: std.StringArrayHashMapUnmanaged(Trope) = .empty, + edges: std.ArrayListUnmanaged(Edge) = .empty, + use_models: std.StringArrayHashMapUnmanaged(UseModel) = .empty, + warrants: std.StringArrayHashMapUnmanaged(void) = .empty, + diag: *Diag, + + pub fn deinit(self: *Store) void { + const child = self.arena.child_allocator; + self.arena.deinit(); + child.destroy(self.arena); + } + + fn fail(self: *Store, line: usize, msg: []const u8) StoreError { + self.diag.line = line; + self.diag.msg = msg; + return StoreError.Validation; + } + + fn getString(obj: std.json.ObjectMap, key: []const u8) ?[]const u8 { + const v = obj.get(key) orelse return null; + return switch (v) { + .string => |s| s, + else => null, + }; + } + + /// Load and validate a JSONL byte stream. All memory lives in the store's + /// arena; on error the arena is torn down before returning, and `diag` + /// (caller-owned, may outlive the store) carries the fault. + pub fn load(gpa: std.mem.Allocator, bytes: []const u8, diag: *Diag) StoreError!Store { + const arena_ptr = gpa.create(std.heap.ArenaAllocator) catch return StoreError.OutOfMemory; + arena_ptr.* = std.heap.ArenaAllocator.init(gpa); + var self = Store{ .arena = arena_ptr, .diag = diag }; + const a = arena_ptr.allocator(); + errdefer self.deinit(); + + var it = std.mem.splitScalar(u8, bytes, '\n'); + var lineno: usize = 0; + while (it.next()) |raw| { + lineno += 1; + const line = std.mem.trim(u8, raw, " \t\r"); + if (line.len == 0 or line[0] == '#') continue; + const parsed = std.json.parseFromSliceLeaky(std.json.Value, a, line, .{}) catch + return self.fail(lineno, "invalid JSON"); + const obj = switch (parsed) { + .object => |o| o, + else => return self.fail(lineno, "record is not a JSON object"), + }; + const t = getString(obj, "t") orelse return self.fail(lineno, "missing record tag \"t\""); + + if (std.mem.eql(u8, t, "trope")) { + const id = getString(obj, "id") orelse return self.fail(lineno, "trope: missing id"); + if (id.len == 0) return self.fail(lineno, "trope: empty id"); + const tr = Trope{ + .id = id, + .quality = getString(obj, "quality") orelse return self.fail(lineno, "trope: quality must be a string (\"\" for absent)"), + .bearer = getString(obj, "bearer") orelse return self.fail(lineno, "trope: bearer must be a string (\"\" for absent)"), + .context = getString(obj, "context") orelse return self.fail(lineno, "trope: context must be a string (\"\" for absent)"), + .record = getString(obj, "record") orelse return self.fail(lineno, "trope: record must be a string (\"\" for absent)"), + }; + const gop = self.tropes.getOrPut(a, id) catch return StoreError.OutOfMemory; + if (gop.found_existing) return self.fail(lineno, "duplicate trope id"); + gop.value_ptr.* = tr; + } else if (std.mem.eql(u8, t, "edge")) { + const id = getString(obj, "id") orelse return self.fail(lineno, "edge: missing id"); + const effect = getString(obj, "effect") orelse return self.fail(lineno, "edge: missing effect"); + if (!isEffect(effect)) return self.fail(lineno, "edge: effect is not one of the six writable effects"); + const inputs_v = obj.get("inputs") orelse return self.fail(lineno, "edge: missing inputs"); + const inputs_arr = switch (inputs_v) { + .array => |arr| arr, + else => return self.fail(lineno, "edge: inputs must be an array"), + }; + if (inputs_arr.items.len < 1 or inputs_arr.items.len > 2) + return self.fail(lineno, "edge: inputs must have 1..2 entries"); + var inputs = a.alloc([]const u8, inputs_arr.items.len) catch return StoreError.OutOfMemory; + for (inputs_arr.items, 0..) |iv, i| { + inputs[i] = switch (iv) { + .string => |s| s, + else => return self.fail(lineno, "edge: inputs must be strings"), + }; + } + const output = getString(obj, "output") orelse return self.fail(lineno, "edge: missing output"); + const grade = obj.get("grade") orelse return self.fail(lineno, "edge: missing grade"); + if (grade != .object) return self.fail(lineno, "edge: grade must be an object"); + const warrant_id = getString(obj, "warrant_id") orelse + return self.fail(lineno, "edge: missing warrant_id (a stored loss-shape is a claim; every claim needs a warrant)"); + if (warrant_id.len == 0) + return self.fail(lineno, "edge: empty warrant_id"); + self.edges.append(a, .{ + .id = id, + .effect = effect, + .inputs = inputs, + .output = output, + .grade = grade, + .warrant_id = warrant_id, + .source = getString(obj, "source") orelse "", + }) catch return StoreError.OutOfMemory; + } else if (std.mem.eql(u8, t, "use_model")) { + const id = getString(obj, "id") orelse return self.fail(lineno, "use_model: missing id"); + const floor = obj.get("floor") orelse return self.fail(lineno, "use_model: missing floor"); + if (floor != .object) return self.fail(lineno, "use_model: floor must be an object"); + const gop = self.use_models.getOrPut(a, id) catch return StoreError.OutOfMemory; + if (gop.found_existing) return self.fail(lineno, "duplicate use_model id"); + gop.value_ptr.* = .{ .id = id, .floor = floor }; + } else if (std.mem.eql(u8, t, "warrant")) { + const id = getString(obj, "id") orelse return self.fail(lineno, "warrant: missing id"); + if (id.len == 0) return self.fail(lineno, "warrant: empty id"); + self.warrants.put(a, id, {}) catch return StoreError.OutOfMemory; + } else if (std.mem.eql(u8, t, "revocation")) { + return self.fail(lineno, "revocation records are reserved for M2 (revoke is not implemented)"); + } else { + return self.fail(lineno, "unknown record tag"); + } + } + + // Referential validation (order-independent: after full replay). + for (self.edges.items) |e| { + for (e.inputs) |inp| { + if (self.tropes.get(inp) == null) + return self.fail(0, "edge references unknown input trope"); + } + if (self.tropes.get(e.output) == null) + return self.fail(0, "edge references unknown output trope"); + if (self.warrants.get(e.warrant_id) == null) + return self.fail(0, "edge cites an unknown warrant (unwarranted loss-shape claims are refused)"); + } + return self; + } + + /// The invoke slice: collect every edge on any path INTO `target_id` + /// (reverse reachability to fixpoint). Returns edge indices in stable + /// (file) order, plus the set of involved trope ids in first-seen order. + pub fn pathInto( + self: *Store, + target_id: []const u8, + edge_idx_out: *std.ArrayListUnmanaged(usize), + trope_ids_out: *std.ArrayListUnmanaged([]const u8), + ) StoreError!void { + const a = self.arena.allocator(); + var reachable: std.StringArrayHashMapUnmanaged(void) = .empty; + reachable.put(a, target_id, {}) catch return StoreError.OutOfMemory; + var included = self.arena.child_allocator.alloc(bool, self.edges.items.len) catch return StoreError.OutOfMemory; + defer self.arena.child_allocator.free(included); + @memset(included, false); + + var changed = true; + while (changed) { + changed = false; + for (self.edges.items, 0..) |e, i| { + if (included[i]) continue; + if (reachable.get(e.output) != null) { + included[i] = true; + changed = true; + for (e.inputs) |inp| { + reachable.put(a, inp, {}) catch return StoreError.OutOfMemory; + } + } + } + } + for (self.edges.items, 0..) |_, i| { + if (included[i]) edge_idx_out.append(a, i) catch return StoreError.OutOfMemory; + } + // Involved tropes: every input/output of included edges, plus the target. + var seen: std.StringArrayHashMapUnmanaged(void) = .empty; + for (edge_idx_out.items) |i| { + const e = self.edges.items[i]; + for (e.inputs) |inp| { + const gop = seen.getOrPut(a, inp) catch return StoreError.OutOfMemory; + if (!gop.found_existing) trope_ids_out.append(a, inp) catch return StoreError.OutOfMemory; + } + const gop = seen.getOrPut(a, e.output) catch return StoreError.OutOfMemory; + if (!gop.found_existing) trope_ids_out.append(a, e.output) catch return StoreError.OutOfMemory; + } + const gop = seen.getOrPut(a, target_id) catch return StoreError.OutOfMemory; + if (!gop.found_existing) trope_ids_out.append(a, target_id) catch return StoreError.OutOfMemory; + } +}; + +// --------------------------------------------------------------------------- +// Unit tests (zig build test) +// --------------------------------------------------------------------------- + +const t_fixture = + \\{"t":"warrant","id":"w1","text":"editorial comparison"} + \\{"t":"trope","id":"a","quality":"authentic language","bearer":"quoted phrase","context":"de Man / rhetoric","record":"source quotation"} + \\{"t":"trope","id":"b","quality":"real language","bearer":"paraphrase","context":"casual gloss","record":"secondhand"} + \\{"t":"edge","id":"e1","effect":"attenuate","inputs":["a"],"output":"b","grade":{"fate":{"quality":{"k":"Attenuated","delta":6},"bearer":{"k":"Present"},"context":{"k":"Present"},"record":{"k":"Present"}},"bond":{"k":"Intact"},"merge":{"k":"Single"}},"warrant_id":"w1"} + \\{"t":"use_model","id":"casual-note","floor":{"fate":{"quality":{"k":"Attenuated","delta":10}}}} +; + +test "replay builds indexes" { + var diag = Diag{}; + var s = try Store.load(std.testing.allocator, t_fixture, &diag); + defer s.deinit(); + try std.testing.expectEqual(@as(usize, 2), s.tropes.count()); + try std.testing.expectEqual(@as(usize, 1), s.edges.items.len); + try std.testing.expectEqual(@as(usize, 1), s.use_models.count()); + try std.testing.expectEqual(@as(usize, 1), s.warrants.count()); +} + +test "presence projection skips empty fields" { + var diag = Diag{}; + var s = try Store.load(std.testing.allocator, + \\{"t":"trope","id":"x","quality":"q","bearer":"","context":"c","record":""} + , &diag); + defer s.deinit(); + const tr = s.tropes.get("x").?; + var buf: [4][]const u8 = undefined; + const present = tr.presentFields(&buf); + try std.testing.expectEqual(@as(usize, 2), present.len); + try std.testing.expectEqualStrings("quality", present[0]); + try std.testing.expectEqualStrings("context", present[1]); +} + +test "unwarranted edge is refused (validation)" { + const bad = + \\{"t":"trope","id":"a","quality":"q","bearer":"b","context":"c","record":"r"} + \\{"t":"trope","id":"b2","quality":"q","bearer":"b","context":"c","record":"r"} + \\{"t":"edge","id":"e1","effect":"preserve","inputs":["a"],"output":"b2","grade":{"fate":{}},"warrant_id":"missing"} + ; + var diag = Diag{}; + try std.testing.expectError(StoreError.Validation, Store.load(std.testing.allocator, bad, &diag)); +} + +test "edge without warrant_id is refused" { + const bad = + \\{"t":"trope","id":"a","quality":"q","bearer":"b","context":"c","record":"r"} + \\{"t":"edge","id":"e1","effect":"preserve","inputs":["a"],"output":"a","grade":{"fate":{}}} + ; + var diag = Diag{}; + try std.testing.expectError(StoreError.Validation, Store.load(std.testing.allocator, bad, &diag)); +} + +test "reverse reachability collects the chain into the target" { + const chain = + \\{"t":"warrant","id":"w","text":""} + \\{"t":"trope","id":"a","quality":"q","bearer":"b","context":"c","record":"r"} + \\{"t":"trope","id":"b","quality":"q","bearer":"b","context":"c","record":"r"} + \\{"t":"trope","id":"c","quality":"q","bearer":"b","context":"c","record":"r"} + \\{"t":"trope","id":"lone","quality":"q","bearer":"b","context":"c","record":"r"} + \\{"t":"edge","id":"e1","effect":"preserve","inputs":["a"],"output":"b","grade":{"g":1},"warrant_id":"w"} + \\{"t":"edge","id":"e2","effect":"attenuate","inputs":["b"],"output":"c","grade":{"g":1},"warrant_id":"w"} + \\{"t":"edge","id":"e3","effect":"preserve","inputs":["lone"],"output":"lone","grade":{"g":1},"warrant_id":"w"} + ; + var diag = Diag{}; + var s = try Store.load(std.testing.allocator, chain, &diag); + defer s.deinit(); + var eidx: std.ArrayListUnmanaged(usize) = .empty; + var tids: std.ArrayListUnmanaged([]const u8) = .empty; + try s.pathInto("c", &eidx, &tids); + try std.testing.expectEqual(@as(usize, 2), eidx.items.len); // e1,e2 not e3 + try std.testing.expectEqual(@as(usize, 3), tids.items.len); // a,b,c not lone +} + +test "revocation records are reserved" { + var diag = Diag{}; + try std.testing.expectError(StoreError.Validation, Store.load(std.testing.allocator, + \\{"t":"revocation","id":"e1"} + , &diag)); +} diff --git a/tests/aspect_tests.sh b/tests/aspect_tests.sh deleted file mode 100755 index 028b2c0..0000000 --- a/tests/aspect_tests.sh +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# RSR Standard Aspect Test Template -# -# Aspect tests validate cross-cutting architectural invariants that span -# the entire codebase. These are NOT functional tests — they verify that -# coding standards, safety rules, and structural contracts hold. -# -# Usage: -# bash tests/aspect_tests.sh -# just aspect -# -# Standard aspects (enable what applies to your project): -# 1. SPDX compliance — all source files have license headers -# 2. Dangerous patterns — no believe_me, assert_total, sorry, unsafeCoerce, etc. -# 3. ABI/FFI contract — declarations match exports -# 4. Thread safety — mutex in FFI modules -# 5. Error handling — no panic/unreachable in production paths - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$PROJECT_DIR" - -PASS=0 -FAIL=0 -WARN=0 - -green() { printf '\033[32m%s\033[0m\n' "$*"; } -red() { printf '\033[31m%s\033[0m\n' "$*"; } -yellow(){ printf '\033[33m%s\033[0m\n' "$*"; } -bold() { printf '\033[1m%s\033[0m\n' "$*"; } - -pass() { green " PASS: $1"; PASS=$((PASS + 1)); } -fail() { red " FAIL: $1"; FAIL=$((FAIL + 1)); } -warn() { yellow " WARN: $1"; WARN=$((WARN + 1)); } - -echo "═══════════════════════════════════════════════════════════════" -echo " {{PROJECT}} — Aspect Tests (Cross-Cutting Concerns)" -echo "═══════════════════════════════════════════════════════════════" -echo "" - -# ═══════════════════════════════════════════════════════════════════════ -# Aspect 1: SPDX License Headers -# ═══════════════════════════════════════════════════════════════════════ -bold "Aspect 1: SPDX license headers" - -MISSING_SPDX=0 -while IFS= read -r -d '' f; do - if ! head -5 "$f" | grep -q "SPDX-License-Identifier"; then - warn "Missing SPDX header: $f" - MISSING_SPDX=$((MISSING_SPDX + 1)) - fi -done < <(find src/ -type f \( -name "*.rs" -o -name "*.zig" -o -name "*.res" -o -name "*.ex" -o -name "*.exs" -o -name "*.gleam" -o -name "*.idr" -o -name "*.sh" \) -print0 2>/dev/null) - -if [ "$MISSING_SPDX" -eq 0 ]; then - pass "All source files have SPDX headers" -else - fail "$MISSING_SPDX files missing SPDX headers" -fi - -# ═══════════════════════════════════════════════════════════════════════ -# Aspect 2: Dangerous Patterns (BANNED) -# ═══════════════════════════════════════════════════════════════════════ -bold "Aspect 2: Dangerous patterns" - -# Idris2 dangerous patterns -DANGEROUS_IDRIS=$(grep -rn 'believe_me\|assert_total\|really_believe_me' src/abi/ 2>/dev/null | grep -v "^Binary" | grep -v "test" || true) -if [ -n "$DANGEROUS_IDRIS" ]; then - fail "Dangerous Idris2 patterns found:" - echo "$DANGEROUS_IDRIS" | head -5 -else - pass "No dangerous Idris2 patterns (believe_me, assert_total)" -fi - -# Coq/Lean dangerous patterns -DANGEROUS_PROOF=$(grep -rn '\bAdmitted\b\|\bsorry\b\|\bunsafeCoerce\b\|\bObj\.magic\b' src/ verification/ 2>/dev/null | grep -v "test" | grep -v "comment" || true) -if [ -n "$DANGEROUS_PROOF" ]; then - fail "Dangerous proof patterns found:" - echo "$DANGEROUS_PROOF" | head -5 -else - pass "No dangerous proof patterns (Admitted, sorry, unsafeCoerce)" -fi - -# ═══════════════════════════════════════════════════════════════════════ -# Aspect 3: ABI/FFI Contract (if applicable) -# ═══════════════════════════════════════════════════════════════════════ -# Uncomment if your project has Idris2 ABI + Zig FFI: - -# bold "Aspect 3: ABI/FFI contract" -# if [ -d "src/abi" ] && [ -d "ffi/zig" ]; then -# # Check that every exported function in Idris2 ABI has a Zig FFI implementation -# ABI_EXPORTS=$(grep -h 'export' src/abi/*.idr 2>/dev/null | wc -l) -# FFI_EXPORTS=$(grep -h 'pub export fn' ffi/zig/src/*.zig 2>/dev/null | wc -l) -# if [ "$ABI_EXPORTS" -gt 0 ] && [ "$FFI_EXPORTS" -gt 0 ]; then -# pass "ABI ($ABI_EXPORTS exports) and FFI ($FFI_EXPORTS exports) both present" -# else -# fail "ABI/FFI mismatch: $ABI_EXPORTS ABI exports, $FFI_EXPORTS FFI exports" -# fi -# else -# pass "ABI/FFI not applicable (no src/abi or ffi/zig)" -# fi - -# ═══════════════════════════════════════════════════════════════════════ -# Aspect 4: Error Handling (no raw panic in production code) -# ═══════════════════════════════════════════════════════════════════════ -# Uncomment for Rust projects: - -# bold "Aspect 4: Error handling" -# UNWRAP_COUNT=$(grep -rn '\.unwrap()' src/ 2>/dev/null | grep -v "test" | grep -v "example" | wc -l) -# if [ "$UNWRAP_COUNT" -gt 20 ]; then -# warn "$UNWRAP_COUNT .unwrap() calls in src/ — consider replacing with ? or expect()" -# else -# pass "Acceptable unwrap count: $UNWRAP_COUNT" -# fi - -# ═══════════════════════════════════════════════════════════════════════ -# Summary -# ═══════════════════════════════════════════════════════════════════════ -echo "" -echo "═══════════════════════════════════════════════════════════════" -printf " Results: " -green "PASS=$PASS" | tr -d '\n' -echo -n " " -if [ "$FAIL" -gt 0 ]; then red "FAIL=$FAIL" | tr -d '\n'; else echo -n "FAIL=0"; fi -echo -n " " -if [ "$WARN" -gt 0 ]; then yellow "WARN=$WARN"; else echo "WARN=0"; fi -echo "" -echo "═══════════════════════════════════════════════════════════════" - -exit "$FAIL" diff --git a/tests/e2e/fixtures/unwarranted.jsonl b/tests/e2e/fixtures/unwarranted.jsonl new file mode 100644 index 0000000..f041b2d --- /dev/null +++ b/tests/e2e/fixtures/unwarranted.jsonl @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: MPL-2.0 +# Negative fixture: the edge cites a warrant that does not exist in the store. +# An unwarranted loss-shape claim must be refused at the store door (exit 2) — +# EXPLAINME's falsifiability list: the design fails if the system "silently +# accepts unwarranted substitutions as preserves". +{"t":"trope","id":"t_a","quality":"passed away","bearer":"obituary phrase","context":"condolence register","record":"family notice"} +{"t":"trope","id":"t_b","quality":"croaked","bearer":"slang phrase","context":"barroom register","record":"retelling"} +{"t":"edge","id":"e_substitution","effect":"preserve","inputs":["t_a"],"output":"t_b","grade":{"fate":{"quality":{"k":"Present"},"bearer":{"k":"Present"},"context":{"k":"Present"},"record":{"k":"Present"}},"bond":{"k":"Intact"},"merge":{"k":"Single"}},"warrant_id":"w-nonexistent"} +{"t":"use_model","id":"casual-note","floor":{"fate":{"quality":{"k":"Attenuated","delta":10}}}} diff --git a/tests/e2e/invoke_slice.sh b/tests/e2e/invoke_slice.sh new file mode 100755 index 0000000..9b18872 --- /dev/null +++ b/tests/e2e/invoke_slice.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# The M1 honest gate (ADR-0002): prove the invoke slice end-to-end. +# +# What this proves, in order: +# 0. The toolchain is exactly what we pinned (hard-fail if absent/wrong — +# proof-gates-cannot-fail doctrine: a gate that silently skips is worse +# than no gate). +# 1. Unit tests pass (zig build test). +# 2. The canonical store ingests cleanly. +# 3. USE-RELATIVITY: the same stored trope yields OPPOSITE tropecheck +# verdicts under two use-models (casual-note -> exit 0 AND +# critical-paraphrase -> exit 1, witness edge named). Neither an empty +# store, a stubbed method, nor a broken emitter can pass both. +# 4. An unwarranted edge is refused at the store door (exit 2). +set -euo pipefail +cd "$(dirname "$0")/../.." + +ZIG_EXPECTED="$(awk '$1=="zig"{print $2}' .tool-versions)" +TROPECHECK_PIN="69221ad6bec0c9286cb5c917edfad281e7e0089e" + +fail() { echo "GATE FAIL: $*" >&2; exit 1; } + +# --- 0. tool-pin hard asserts (BEFORE any test) ----------------------------- +command -v zig >/dev/null || fail "zig not installed (need exactly ${ZIG_EXPECTED})" +ZIG_ACTUAL="$(zig version)" +[ "$ZIG_ACTUAL" = "$ZIG_EXPECTED" ] || fail "zig version skew: have ${ZIG_ACTUAL}, .tool-versions pins ${ZIG_EXPECTED}" + +if [ -z "${TROPECHECK_BIN:-}" ]; then + command -v cargo >/dev/null || fail "cargo not installed (needed to build tropecheck-rs at pin ${TROPECHECK_PIN})" + command -v git >/dev/null || fail "git not installed" + TC_DIR=".gate-tools/trope-checker" + if [ ! -x "${TC_DIR}/src/rust/target/release/tropecheck-rs" ] || \ + [ "$(git -C "${TC_DIR}" rev-parse HEAD 2>/dev/null)" != "${TROPECHECK_PIN}" ]; then + rm -rf "${TC_DIR}" + git clone -q https://github.com/hyperpolymath/trope-checker "${TC_DIR}" || fail "cannot clone trope-checker" + git -C "${TC_DIR}" checkout -q "${TROPECHECK_PIN}" || fail "cannot checkout tropecheck pin ${TROPECHECK_PIN}" + cargo build --quiet --release --manifest-path "${TC_DIR}/src/rust/Cargo.toml" || fail "tropecheck-rs build failed" + fi + TROPECHECK_BIN="${TC_DIR}/src/rust/target/release/tropecheck-rs" +fi +[ -x "$TROPECHECK_BIN" ] || fail "tropecheck binary not executable: ${TROPECHECK_BIN}" + +# --- 1. unit tests ---------------------------------------------------------- +zig build test || fail "zig build test failed" +zig build || fail "zig build failed" +VOC=zig-out/bin/voc +[ -x "$VOC" ] || fail "voc binary missing after build" + +# --- 2. canonical ingest ---------------------------------------------------- +"$VOC" ingest examples/vocarium-ingest.jsonl || fail "canonical fixture failed ingest (exit $?)" + +# --- 3. use-relativity ------------------------------------------------------ +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +"$VOC" invoke --store examples/vocarium-ingest.jsonl --use-model casual-note t_real > "$TMP/casual.json" \ + || fail "voc invoke (casual-note) failed" +if "$TROPECHECK_BIN" "$TMP/casual.json" > "$TMP/casual.out"; then + grep -q '^p-sufficient' "$TMP/casual.out" || fail "casual-note: exit 0 but verdict line missing" +else + fail "casual-note must be p-sufficient (exit 0), got exit $?" +fi + +"$VOC" invoke --store examples/vocarium-ingest.jsonl --use-model critical-paraphrase t_real > "$TMP/critical.json" \ + || fail "voc invoke (critical-paraphrase) failed" +if "$TROPECHECK_BIN" "$TMP/critical.json" > "$TMP/critical.out"; then + fail "critical-paraphrase must be p-insufficient (exit 1), got exit 0 — use-relativity is broken" +else + rc=$? + [ "$rc" -eq 1 ] || fail "critical-paraphrase: expected exit 1, got ${rc}" + grep -q 'witness=e_paraphrase' "$TMP/critical.out" || fail "insufficient verdict lacks the witness edge" +fi + +# --- 4. unwarranted edge refused ------------------------------------------- +if "$VOC" invoke --store tests/e2e/fixtures/unwarranted.jsonl --use-model casual-note t_b > /dev/null 2> "$TMP/neg.err"; then + fail "unwarranted edge was ACCEPTED — the store door is open" +else + rc=$? + [ "$rc" -eq 2 ] || fail "unwarranted edge: expected validation exit 2, got ${rc}" + grep -q 'unknown warrant' "$TMP/neg.err" || fail "refusal did not name the warrant fault" +fi + +echo "GATE PASS: invoke slice proven (use-relativity + warrant door + unit tests)" diff --git a/tests/e2e/template_instantiation_test.sh b/tests/e2e/template_instantiation_test.sh deleted file mode 100755 index f991d5d..0000000 --- a/tests/e2e/template_instantiation_test.sh +++ /dev/null @@ -1,268 +0,0 @@ -#!/bin/bash -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# E2E Test: Template Instantiation -# Verifies that the template can be cloned and instantiated into a working project -# -# This test: -# 1. Clones the template to a temp directory -# 2. Replaces all placeholder tokens with test values -# 3. Validates the resulting repository structure -# 4. Verifies builds work after instantiation -# 5. Cleans up - -set -euo pipefail - -# Test configuration -TEMPLATE_ROOT="${1:-.}" -TEST_DIR="${TMPDIR:-/tmp}/rsr-template-test-$$" -TEST_REPO_NAME="test-instantiated-repo" -TEST_OWNER="test-owner" -TEST_FORGE="github" -TEST_AUTHOR="Test Author" -TEST_AUTHOR_EMAIL="test@example.com" -TEST_PROJECT_NAME="Test Project" -TEST_DESCRIPTION="A test project instantiated from the RSR template" -TEST_PRIMARY_LANGUAGE="Rust" - -# ANSI colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Helper functions -log_step() { - echo "" - echo -e "${BLUE}→${NC} $*" -} - -log_pass() { - echo -e "${GREEN}✓${NC} $*" -} - -log_error() { - echo -e "${RED}✗${NC} $*" >&2 -} - -cleanup() { - if [ -d "$TEST_DIR" ]; then - log_step "Cleaning up test directory: $TEST_DIR" - rm -rf "$TEST_DIR" - log_pass "Cleanup complete" - fi -} - -trap cleanup EXIT - -#============================================================================== -# PHASE 1: SETUP -#============================================================================== - -echo "" -echo "═══════════════════════════════════════════════════════════════════════════════" -echo "E2E TEST: Template Instantiation" -echo "═══════════════════════════════════════════════════════════════════════════════" -echo "" - -log_step "Creating test directory: $TEST_DIR" -mkdir -p "$TEST_DIR" -log_pass "Test directory created" - -#============================================================================== -# PHASE 2: CLONE TEMPLATE -#============================================================================== - -log_step "Cloning template from $TEMPLATE_ROOT" - -# Copy template to test location (simulating git clone) -TEST_REPO_PATH="$TEST_DIR/$TEST_REPO_NAME" -cp -r "$TEMPLATE_ROOT" "$TEST_REPO_PATH" -log_pass "Template cloned to $TEST_REPO_PATH" - -# Remove .git directory for clean state -if [ -d "$TEST_REPO_PATH/.git" ]; then - rm -rf "$TEST_REPO_PATH/.git" - log_pass ".git directory removed (fresh clone)" -fi - -#============================================================================== -# PHASE 3: PLACEHOLDER REPLACEMENT -#============================================================================== - -log_step "Replacing placeholder tokens" - -# Function to replace all occurrences of a placeholder in a file -replace_placeholder() { - local file="$1" - local placeholder="$2" - local value="$3" - - if [ ! -f "$file" ]; then - return 0 - fi - - # Use sed to replace (platform-portable) - if grep -q "$placeholder" "$file" 2>/dev/null; then - sed -i "s|$placeholder|$value|g" "$file" - echo " Replaced $placeholder in $(basename "$file")" - fi -} - -# Replace in all text files -find "$TEST_REPO_PATH" -type f \ - \( -name "*.md" -o -name "*.adoc" -o -name "*.a2ml" -o -name "*.zig" -o -name "*.idr" \ - -o -name "Justfile" -o -name "Containerfile" -o -name "*.yml" -o -name "*.yaml" \ - -o -name "*.json" -o -name "*.scm" -o -name "contractile" \) \ - -exec bash -c ' - file="$1" - placeholder_pairs=( - "{{REPO}}|$TEST_REPO_NAME" - "{{OWNER}}|$TEST_OWNER" - "{{FORGE}}|$TEST_FORGE" - "{{PROJECT}}|$TEST_PROJECT_NAME" - "{{project}}|'"${TEST_REPO_NAME//-/_}"'" - "{{REPO_DESCRIPTION}}|$TEST_DESCRIPTION" - "{{PRIMARY_LANGUAGE}}|$TEST_PRIMARY_LANGUAGE" - "{{AUTHOR}}|$TEST_AUTHOR" - "{{AUTHOR_EMAIL}}|$TEST_AUTHOR_EMAIL" - "{{CURRENT_DATE}}|2026-04-04" - ) - - for pair in "${placeholder_pairs[@]}"; do - IFS="|" read -r placeholder value <<< "$pair" - if grep -q "$placeholder" "$file" 2>/dev/null; then - sed -i "s|$placeholder|$value|g" "$file" - fi - done - ' _ "$file" - -log_pass "All placeholder tokens replaced" - -#============================================================================== -# PHASE 4: VALIDATE STRUCTURE -#============================================================================== - -log_step "Validating instantiated repository structure" - -# Run validation script on the instantiated repo -if [ -f "$TEMPLATE_ROOT/scripts/validate-template.sh" ]; then - bash "$TEMPLATE_ROOT/scripts/validate-template.sh" "$TEST_REPO_PATH" 0 - log_pass "Repository structure validation passed" -else - log_error "Validation script not found" - exit 1 -fi - -#============================================================================== -# PHASE 5: VERIFY BUILD -#============================================================================== - -log_step "Verifying build system works after instantiation" - -if [ -f "$TEST_REPO_PATH/src/interface/ffi/build.zig" ]; then - if command -v zig &> /dev/null; then - cd "$TEST_REPO_PATH/src/interface/ffi" - if zig build 2>&1; then - log_pass "Zig build successful" - else - log_error "Zig build failed" - exit 1 - fi - cd - > /dev/null - else - log_error "Zig compiler not found - cannot verify build" - exit 1 - fi -fi - -#============================================================================== -# PHASE 6: VERIFY NO REMAINING PLACEHOLDERS -#============================================================================== - -log_step "Checking for remaining placeholders" - -REMAINING_PLACEHOLDERS=$( - find "$TEST_REPO_PATH" -type f \ - \( -name "*.md" -o -name "*.adoc" -o -name "*.a2ml" -o -name "*.zig" -o -name "*.idr" \ - -o -name "Justfile" -o -name "*.yml" \) \ - -exec grep -l "{{[A-Z_]*}}" {} \; 2>/dev/null || true -) - -if [ -z "$REMAINING_PLACEHOLDERS" ]; then - log_pass "No remaining placeholders found" -else - log_error "Found remaining placeholders in:" - echo "$REMAINING_PLACEHOLDERS" | sed 's/^/ /' - exit 1 -fi - -#============================================================================== -# PHASE 7: VERIFY CRITICAL FILES ARE NOT TEMPLATES -#============================================================================== - -log_step "Verifying critical files have been instantiated" - -CRITICAL_FILES=( - "README.adoc" - "EXPLAINME.adoc" - "Justfile" -) - -for file in "${CRITICAL_FILES[@]}"; do - if [ -f "$TEST_REPO_PATH/$file" ]; then - # Check that it's not just a template (contains some actual content) - if grep -q "$TEST_PROJECT_NAME\|$TEST_AUTHOR\|$TEST_REPO_NAME" "$TEST_REPO_PATH/$file" 2>/dev/null || \ - [ $(wc -l < "$TEST_REPO_PATH/$file") -gt 10 ]; then - log_pass "File instantiated: $file" - else - log_error "File appears to be a template: $file" - exit 1 - fi - else - log_error "Critical file missing: $file" - exit 1 - fi -done - -#============================================================================== -# PHASE 8: VERIFY METADATA -#============================================================================== - -log_step "Verifying machine-readable metadata" - -METADATA_FILES=( - ".machine_readable/6a2/STATE.a2ml" - ".machine_readable/6a2/META.a2ml" -) - -for file in "${METADATA_FILES[@]}"; do - if [ -f "$TEST_REPO_PATH/$file" ]; then - log_pass "Metadata file exists: $file" - else - log_error "Metadata file missing: $file" - exit 1 - fi -done - -#============================================================================== -# SUMMARY -#============================================================================== - -echo "" -echo "═══════════════════════════════════════════════════════════════════════════════" -echo -e "${GREEN}✓ E2E TEMPLATE INSTANTIATION TEST PASSED${NC}" -echo "═══════════════════════════════════════════════════════════════════════════════" -echo "" -echo "Summary:" -echo " - Template cloned successfully" -echo " - All placeholders replaced" -echo " - Repository structure valid" -echo " - Build system works" -echo " - No remaining placeholders" -echo " - Metadata intact" -echo "" -echo "Test repository: $TEST_REPO_PATH (will be cleaned up)" -echo ""