Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ All notable changes to tagged releases are documented here.
republished under a second `0.1.0-draft` tag. The release gate already treats a full-length commit
digest as an immutable reference; the pin moves back to a tag once a specification version
carrying those identifiers is published.
- Pin the machine contract in tests: `outputVersion`, `tool.name`, the six exit classes, and the
four envelope members every JSON payload carries. These were previously enforced only by
convention, so a rename or an accidental version bump passed silently.

## 0.0.1 - 2026-07-23

Expand Down
36 changes: 5 additions & 31 deletions internal/cli/app.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package cli

import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
Expand All @@ -16,6 +13,7 @@ import (
"github.com/Judgment-Pack/judgment-pack-runtime/internal/artifacts"
"github.com/Judgment-Pack/judgment-pack-runtime/internal/carrier"
"github.com/Judgment-Pack/judgment-pack-runtime/internal/conformance"
"github.com/Judgment-Pack/judgment-pack-runtime/internal/describe"
"github.com/Judgment-Pack/judgment-pack-runtime/internal/display"
"github.com/Judgment-Pack/judgment-pack-runtime/internal/fssecure"
"github.com/Judgment-Pack/judgment-pack-runtime/internal/result"
Expand Down Expand Up @@ -121,14 +119,7 @@ func (a *App) versionCommand() *cobra.Command {
if err != nil {
return a.operational("version", format, result.ExitInternal, "JPS-ARTIFACT-INTEGRITY", "Bundled artifact metadata is unavailable.")
}
output := result.Version{
OutputVersion: result.OutputVersion,
Tool: result.CurrentTool(),
Command: "version",
Status: "valid",
SupportedSpecs: artifacts.SupportedVersions(),
ArtifactProvenance: set.Lock().Source.Kind,
}
output := describe.Runtime(set, "version")
if format == "json" {
if err := writeJSON(a.out, output); err != nil {
return &handledExit{code: result.ExitIO}
Expand Down Expand Up @@ -271,23 +262,11 @@ func (a *App) schemaCommand() *cobra.Command {
}
writtenTo = writeTarget
}
var schemaDocument map[string]any
if err := json.Unmarshal(schemaBytes, &schemaDocument); err != nil {
output, err := describe.Schema(set, args[0], "spec schema", schemaBytes)
if err != nil {
return a.operational("spec schema", format, result.ExitInternal, "JPS-ARTIFACT-SCHEMA", "Bundled schema metadata is invalid.")
}
sum := sha256.Sum256(schemaBytes)
output := result.Schema{
OutputVersion: result.OutputVersion,
Tool: result.CurrentTool(),
Command: "spec schema",
Status: "valid",
SpecVersion: args[0],
SchemaID: stringFrom(schemaDocument["$id"]),
Bytes: len(schemaBytes),
SHA256: hex.EncodeToString(sum[:]),
Provenance: set.Lock().Source.Kind,
WrittenTo: writtenTo,
}
output.WrittenTo = writtenTo
if err := a.renderSchema(format, output); err != nil {
return &handledExit{code: result.ExitIO}
}
Expand Down Expand Up @@ -376,11 +355,6 @@ func validationExit(status string) int {
}
}

func stringFrom(value any) string {
text, _ := value.(string)
return text
}

func requestedFormat(args []string) string {
for index, argument := range args {
if argument == "--format=json" {
Expand Down
63 changes: 63 additions & 0 deletions internal/describe/describe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Package describe composes machine-readable descriptions of this runtime and
// the specification artifacts embedded in it.
//
// It holds no CLI, transport, or presentation concerns, so every surface that
// reports these values -- the CLI today, other adapters later -- produces the
// same payload rather than a per-surface reimplementation of it.
package describe

import (
"crypto/sha256"
"encoding/hex"
"encoding/json"

"github.com/Judgment-Pack/judgment-pack-runtime/internal/artifacts"
"github.com/Judgment-Pack/judgment-pack-runtime/internal/result"
)

// Runtime composes the runtime-description payload for a loaded artifact set.
//
// command names the operation reporting the payload so a surface can identify
// itself without forking the shape of the result.
func Runtime(set *artifacts.Set, command string) result.Version {
return result.Version{
OutputVersion: result.OutputVersion,
Tool: result.CurrentTool(),
Command: command,
Status: "valid",
SupportedSpecs: artifacts.SupportedVersions(),
ArtifactProvenance: set.Lock().Source.Kind,
}
}

// Schema composes metadata describing the exact bundled schema bytes of
// specVersion: the schema's declared $id, its size, and its SHA-256.
//
// schemaBytes must be the original bundled bytes, not a re-encoding, because
// the reported size and digest describe those exact bytes. An error is returned
// only when they do not decode as a JSON object. Callers that write the schema
// out set WrittenTo on the returned value themselves; it is a property of the
// caller's action, not of the artifact.
func Schema(set *artifacts.Set, specVersion, command string, schemaBytes []byte) (result.Schema, error) {
var document map[string]any
if err := json.Unmarshal(schemaBytes, &document); err != nil {
return result.Schema{}, err
}
sum := sha256.Sum256(schemaBytes)
return result.Schema{
OutputVersion: result.OutputVersion,
Tool: result.CurrentTool(),
Command: command,
Status: "valid",
SpecVersion: specVersion,
SchemaID: stringFrom(document["$id"]),
Bytes: len(schemaBytes),
SHA256: hex.EncodeToString(sum[:]),
Provenance: set.Lock().Source.Kind,
}, nil
}

func stringFrom(value any) string {
text, _ := value.(string)
return text
}
82 changes: 82 additions & 0 deletions internal/describe/describe_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package describe

import (
"crypto/sha256"
"encoding/hex"
"testing"

"github.com/Judgment-Pack/judgment-pack-runtime/internal/artifacts"
"github.com/Judgment-Pack/judgment-pack-runtime/internal/result"
)

func loadSet(t *testing.T) *artifacts.Set {
t.Helper()
set, err := artifacts.Load(artifacts.DraftVersion)
if err != nil {
t.Fatal(err)
}
return set
}

func TestRuntimeReportsBundledProvenance(t *testing.T) {
set := loadSet(t)
described := Runtime(set, "version")

if described.OutputVersion != result.OutputVersion {
t.Errorf("outputVersion = %q, want %q", described.OutputVersion, result.OutputVersion)
}
if described.Command != "version" {
t.Errorf("command = %q, want %q", described.Command, "version")
}
if described.Status != "valid" {
t.Errorf("status = %q, want %q", described.Status, "valid")
}
if described.ArtifactProvenance != set.Lock().Source.Kind {
t.Errorf("provenance = %q, want the lock's %q", described.ArtifactProvenance, set.Lock().Source.Kind)
}
if len(described.SupportedSpecs) == 0 {
t.Error("supported specification versions must not be empty")
}
}

// The reported size and digest must describe the exact bundled bytes. A caller
// that re-encoded the schema before describing it would produce a digest that
// no longer identifies the artifact the lock verifies.
func TestSchemaDescribesTheExactBundledBytes(t *testing.T) {
set := loadSet(t)
schemaBytes, err := set.Schema()
if err != nil {
t.Fatal(err)
}

described, err := Schema(set, artifacts.DraftVersion, "spec schema", schemaBytes)
if err != nil {
t.Fatal(err)
}

if described.Bytes != len(schemaBytes) {
t.Errorf("bytes = %d, want %d", described.Bytes, len(schemaBytes))
}
sum := sha256.Sum256(schemaBytes)
if want := hex.EncodeToString(sum[:]); described.SHA256 != want {
t.Errorf("sha256 = %q, want %q", described.SHA256, want)
}
if described.SpecVersion != artifacts.DraftVersion {
t.Errorf("specVersion = %q, want %q", described.SpecVersion, artifacts.DraftVersion)
}
if described.SchemaID == "" {
t.Error("schemaId must be reported from the schema's own $id")
}
// WrittenTo describes a caller's action, not the artifact, so this package
// must never populate it.
if described.WrittenTo != "" {
t.Errorf("writtenTo = %q, want empty", described.WrittenTo)
}
}

func TestSchemaRejectsNonObjectBytes(t *testing.T) {
set := loadSet(t)
if _, err := Schema(set, artifacts.DraftVersion, "spec schema", []byte("not a schema")); err == nil {
t.Fatal("expected an error for bytes that are not a JSON object")
}
}
13 changes: 13 additions & 0 deletions internal/mcp/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Package mcp will expose this runtime over the Model Context Protocol.
//
// It is a transport adapter only: it maps MCP tool and resource calls onto the
// existing validation, conformance, and describe packages and returns their
// versioned results. It evaluates no condition, resolves no outcome, and adds
// no judgment behavior of its own -- an MCP client reaches exactly the same
// core the CLI reaches, over JSON-RPC instead of argv.
//
// The adapter is intended to run as an "mcp" subcommand of the single
// judgment-pack binary rather than as a separate program.
//
// This package is a scaffold and currently has no implementation.
package mcp
71 changes: 71 additions & 0 deletions internal/result/result_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package result

import (
"encoding/json"
"testing"
)

// The values below are the machine-facing contract: consumers branch on the
// exit status and on outputVersion. Changing any of them is a compatibility
// break that must be a deliberate, documented decision rather than a side
// effect of an unrelated edit, so they are pinned by literal here.

func TestOutputVersionIsPinned(t *testing.T) {
if OutputVersion != "1" {
t.Fatalf("outputVersion is part of the machine contract: got %q, want %q", OutputVersion, "1")
}
if CLIName != "judgment-pack" {
t.Fatalf("tool.name is part of the machine contract: got %q", CLIName)
}
}

func TestExitClassesArePinned(t *testing.T) {
for _, pinned := range []struct {
name string
got int
want int
}{
{"ExitSuccess", ExitSuccess, 0},
{"ExitInvalid", ExitInvalid, 1},
{"ExitUnsupported", ExitUnsupported, 2},
{"ExitInvocation", ExitInvocation, 3},
{"ExitIO", ExitIO, 4},
{"ExitInternal", ExitInternal, 5},
} {
if pinned.got != pinned.want {
t.Errorf("%s is part of the machine contract: got %d, want %d", pinned.name, pinned.got, pinned.want)
}
}
}

// Every payload carries the same four identifying members. A rename of any of
// them is invisible to Go's type checker but breaks every consumer, so assert
// the emitted JSON rather than the struct.
func TestEveryPayloadCarriesTheCommonEnvelope(t *testing.T) {
for _, payload := range []struct {
name string
value any
}{
{"Validation", Validation{OutputVersion: OutputVersion, Tool: CurrentTool(), Command: "spec validate", Status: "valid"}},
{"Suite", Suite{OutputVersion: OutputVersion, Tool: CurrentTool(), Command: "spec test-conformance", Status: "valid"}},
{"Schema", Schema{OutputVersion: OutputVersion, Tool: CurrentTool(), Command: "spec schema", Status: "valid"}},
{"Version", Version{OutputVersion: OutputVersion, Tool: CurrentTool(), Command: "version", Status: "valid"}},
} {
encoded, err := json.Marshal(payload.value)
if err != nil {
t.Fatalf("%s: %v", payload.name, err)
}
var decoded map[string]any
if err := json.Unmarshal(encoded, &decoded); err != nil {
t.Fatalf("%s: %v", payload.name, err)
}
for _, member := range []string{"outputVersion", "tool", "command", "status"} {
if _, present := decoded[member]; !present {
t.Errorf("%s payload is missing the %q member: %s", payload.name, member, encoded)
}
}
if decoded["outputVersion"] != "1" {
t.Errorf("%s payload reports outputVersion %v, want \"1\"", payload.name, decoded["outputVersion"])
}
}
}
13 changes: 13 additions & 0 deletions sdk/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Package sdk is the reserved home for a public, in-process Go composition API
// over the judgment-pack runtime -- the seam that lets a Go program embed
// validation directly instead of invoking the binary.
//
// It is distinct from a client library in another language: non-Go consumers
// reach the runtime over a wire protocol (the future HTTP or MCP adapters) or
// by invoking the binary, not by importing this package.
//
// Stability: experimental. While the project is pre-1.0 this package exports
// nothing and carries no compatibility guarantee; do not depend on it yet.
//
// This package is a scaffold and currently has no implementation.
package sdk