Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8d1a3a7
m-iris M0: rename from irissync + driver contract seam + remote spike
rafael5 Jun 4, 2026
9180e1b
m-iris M1: lifecycle + health probes + doctor (remote/attach)
rafael5 Jun 4, 2026
2d13d46
m-iris: switch onto m-driver-sdk (Phase-0)
rafael5 Jun 4, 2026
6fada56
m-iris: pin published m-driver-sdk (drop local replace)
rafael5 Jun 4, 2026
5668831
deps: pin m-driver-sdk v0.1.0 (tagged release)
rafael5 Jun 4, 2026
21cedda
refactor: adopt SDK-owned M1 payload shapes; pin m-driver-sdk v0.2.0
rafael5 Jun 4, 2026
a502071
m-iris: fix real-IRIS compatibility (validated against IRIS 2026.1)
rafael5 Jun 4, 2026
b0531ed
m-iris M2 (sync axis complete): diff + rm + push --from + bare-name -…
rafael5 Jun 6, 2026
8c2f010
m-iris: fix two real-IRIS-2026.1 bugs the fake tier missed (Stat 404,…
rafael5 Jun 6, 2026
5b06728
coordination: repo CLAUDE.md + in-repo memory/tracker (driver-effort …
rafael5 Jun 6, 2026
5585d3b
irisdriver: public mdriver.Transport facade for m-cli/VistaEngine
rafael5 Jun 11, 2026
427f797
meta: make `meta version` contract-conformant ({driver,engine,contrac…
rafael5 Jun 11, 2026
ee0d80b
clikit: mirror ResultExit + fix doctor envelope/exit (byte-identical …
rafael5 Jun 11, 2026
8b8ed76
test: update doctor tests for the ResultExit pattern
rafael5 Jun 11, 2026
33019b7
feat(exec): wire exec axis over the remote runner; close VSL M0a T0a.…
rafael5 Jun 12, 2026
95820e1
feat(exec): wire exec abort over the remote runner (live-proven on m-…
rafael5 Jun 12, 2026
b4915e9
feat(transport): docker/local `iris session` transport — closes M3 (c…
rafael5 Jun 12, 2026
353d8b9
feat(data): M4 data axis get/set/kill/query on all transports (confor…
rafael5 Jun 12, 2026
31fa0f6
docs(tracker): record M8 conformance 16/16 green on remote+docker (M8…
rafael5 Jun 12, 2026
49a5b00
fix(remote): GetOut handles wide-char (Unicode >255) output via UTF-8
rafael5 Jun 13, 2026
f59fed8
chore(arch): declare layer m (waterline G1)
rafael5 Jun 14, 2026
694fa48
ci(arch): adopt the reusable m/v waterline G1 gate
rafael5 Jun 14, 2026
795a630
ci: disable schema-check (driver binary, no `schema` command)
rafael5 Jun 14, 2026
da0f153
chore(lint): fix golangci-lint findings to land on main (Phase A)
rafael5 Jun 14, 2026
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ on:
jobs:
ci:
uses: vista-cloud-dev/.github/.github/workflows/go-ci.yml@main
with:
schema-check: false # driver binary — no m-cli-style `schema` command
arch:
uses: vista-cloud-dev/.github/.github/workflows/arch-waterline.yml@main
48 changes: 48 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# m-iris — IRIS engine driver (D1). Repo rules.

Adds to the org rules (`~/vista-cloud-dev/CLAUDE.md`) and the user global
(`~/.claude/CLAUDE.md`). Where this file says **EXCEPTION**, it *overrides* those
for this repo (the user authorized driver-effort carve-outs, 2026-06-06).

This is a **driver spike** session — one of the three coordinated repos
(`m-driver-sdk` ⟷ `m-iris` ⟷ `m-ydb`). Read [[coordination-model]]
(`docs/m-engine-drivers/coordination-model.md` in the `docs` repo) once per fresh
session that touches the driver effort.

## Lane — what this session owns
- **Owns / may push: `m-iris` only**, on branch **`m-iris-driver`** (never `main`).
- **Never edit `m-driver-sdk`** here, and never push `m-driver-sdk` / `m-ydb` /
`docs`. Those belong to the coordinator session. (Editing m-cli is out of scope
entirely until the D3 cutover.)

## The SDK is pinned — do not touch it mid-spike
- Consume `github.com/vista-cloud-dev/m-driver-sdk` at the **pinned tagged version**
in `go.mod` (currently **v0.2.0**). No `replace` directives, no pseudo-versions.
- If you need a new shared shape (a type m-cli will read, or that m-ydb must match):
**do NOT bump the SDK from here.** Stub it locally, record `needs SDK: <shape>`
in this repo's memory, and surface it for a coordinator session to batch into the
next SDK release. Re-pin only when the coordinator tags a new version.
- `caps` stays **honest** (advertise only wired verbs). The neutral contract +
envelope shapes are the m-cli surface; they change only via the SDK/contract,
which you don't edit here — so you cannot drift the surface.

## Increment Protocol — EXCEPTIONS for this repo
Run the org Increment Protocol (persist memory → update tracker → commit+push) at
every verified increment, automatically, **but**:
- **EXCEPTION (memory):** m-iris memory lives in **`./docs/memory/`** (this repo),
committed here with the code. Do **NOT** write `~/claude/memory` and do **NOT**
write the `docs` repo's `docs/memory/` (that is shared coordination memory,
coordinator-owned). The harness recall path for an m-iris session is symlinked to
`./docs/memory/`.
- **EXCEPTION (tracker):** update **`./docs/m-iris-tracker.md`** (this repo), not the
shared `docs/m-engine-drivers/driver-implementation-plan.md` §5 — the coordinator
rolls the shared plan up at milestone boundaries. This keeps parallel iris/ydb
spikes from clashing on the `docs` repo.
- **Commit+push:** `m-iris` branch `m-iris-driver` only. Gates first:
`go test -race ./...`, `go vet`, `gofmt`, and `make test-it` against the live IRIS
(`m-test-iris`) for any Atelier-touching change.

## Real-engine validation
Validate every milestone slice against real IRIS (`make test-it`, IRIS CE 2026.1,
`m-test-iris`) — the fake tier alone misses server-shape bugs (see this repo's
memory: the 404 / PutDoc-result.status findings).
19 changes: 17 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# -trimpath, version stamped via -ldflags, cross-compile matrix, lint, test,
# schema.

BIN ?= irissync
PKG := github.com/vista-cloud-dev/irissync
BIN ?= m-iris
PKG := github.com/vista-cloud-dev/m-iris
LDPKG := $(PKG)/clikit
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo none)
Expand Down Expand Up @@ -33,6 +33,21 @@ lint:
test:
CGO_ENABLED=1 go test $(GOFLAGS) -race -cover ./...

# Real-engine integration tier (gated). Needs a disposable IRIS container with
# Atelier reachable. Defaults target the local m-test-iris (CE on port 52774);
# override IRIS_* to point elsewhere. NEVER point at the shared vista-iris.
IRIS_BASE_URL ?= http://localhost:52774/api/atelier/v1/
IRIS_NAMESPACE ?= USER
IRIS_USER ?= _SYSTEM
IRIS_PASSWORD ?= testsys
IRIS_CONTAINER ?= m-test-iris
IRIS_INSTANCE ?= IRIS
test-it:
M_IRIS_IT=1 M_IRIS_BASE_URL=$(IRIS_BASE_URL) M_IRIS_NAMESPACE=$(IRIS_NAMESPACE) \
M_IRIS_USER=$(IRIS_USER) M_IRIS_PASSWORD=$(IRIS_PASSWORD) \
M_IRIS_CONTAINER=$(IRIS_CONTAINER) M_IRIS_IRIS_INSTANCE=$(IRIS_INSTANCE) \
go test $(GOFLAGS) -count=1 -run RealEngine . ./internal/remote/ ./internal/session/ -v

tidy:
go mod tidy

Expand Down
37 changes: 37 additions & 0 deletions clikit/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ type Envelope struct {
Data any `json:"data,omitempty"`
Diagnostics []Diagnostic `json:"diagnostics,omitempty"`
Error *Error `json:"error,omitempty"`
EngineError *EngineError `json:"engineError,omitempty"`
}

// EngineError is the driver-contract §7 structured engine fault. On any
// compile/runtime fault, exec/cover verbs set ok=false AND surface this as a
// sibling of error, so a RED suite shows the real cause (a <NOROUTINE> at a
// line) rather than passed:0, failed:0. Mnemonic carries the IRIS <…> /
// %YDB-E-… code.
type EngineError struct {
Routine string `json:"routine,omitempty"`
Line int `json:"line,omitempty"`
Mnemonic string `json:"mnemonic,omitempty"`
Text string `json:"text,omitempty"`
}

// Diagnostic is one lint/diagnostic finding (the editor↔CI shared shape).
Expand Down Expand Up @@ -47,6 +60,7 @@ type Context struct {
th theme
gl Glyph
unicode bool
exit int // process exit recorded by ResultExit; Run returns it
}

// NewContext resolves the format/color for this invocation from the globals
Expand Down Expand Up @@ -103,6 +117,29 @@ func (c *Context) Result(data any, text func()) error {
return nil
}

// ResultExit renders a command result that carries a deliberate exit code (and
// thus ok = exit==0): in JSON mode the data envelope with that exit/ok to
// stdout, otherwise the text closure. Unlike Fail — an error written to stderr
// with no data — this is for verbs whose payload IS the result even on a
// non-zero outcome (doctor: a failed check is still a full report; lint /
// roundtrip / status drift). The command returns the (nil) error from here and
// nothing else; Run reads ExitCode() for the process code, so the stdout
// envelope's exit always equals the process exit (driver-contract §2).
func (c *Context) ResultExit(data any, exit int, text func()) error {
c.exit = exit
if c.JSON() {
return c.emit(Envelope{SchemaVersion: SchemaVersion, Command: c.Command, OK: exit == ExitOK, Exit: exit, Data: data})
}
if text != nil {
text()
}
return nil
}

// ExitCode is the process exit a command recorded via ResultExit (ExitOK if it
// used the plain Result path). Run uses it when a command returns no error.
func (c *Context) ExitCode() int { return c.exit }

// Diagnostics renders a result that carries lint-style findings.
func (c *Context) Diagnostics(data any, diags []Diagnostic, text func()) error {
if c.JSON() {
Expand Down
67 changes: 67 additions & 0 deletions clikit/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package clikit

import (
"bytes"
"encoding/json"
"testing"
)

// ResultExit emits the data envelope with a deliberate exit code so the stdout
// envelope's exit matches the process exit (the driver-contract §2 invariant the
// conformance suite enforces). This is the "data + non-zero exit" path for verbs
// like doctor whose payload is still a full result on a non-zero outcome.

func TestResultExit_JSONEnvelopeMatchesExit(t *testing.T) {
var buf bytes.Buffer
c := &Context{Stdout: &buf, Format: FormatJSON, Command: "meta doctor"}
type doc struct {
N int `json:"n"`
}
if err := c.ResultExit(doc{N: 3}, ExitUnreachable, nil); err != nil {
t.Fatalf("ResultExit: %v", err)
}
if c.ExitCode() != ExitUnreachable {
t.Errorf("ExitCode() = %d, want %d", c.ExitCode(), ExitUnreachable)
}
var env Envelope
if err := json.Unmarshal(buf.Bytes(), &env); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if env.OK {
t.Error("ok must be false for a non-zero exit")
}
if env.Exit != ExitUnreachable {
t.Errorf("envelope.exit = %d, want %d", env.Exit, ExitUnreachable)
}
if env.Data == nil {
t.Error("data must be present (the payload is the result)")
}
}

func TestResultExit_ZeroExitIsOK(t *testing.T) {
var buf bytes.Buffer
c := &Context{Stdout: &buf, Format: FormatJSON, Command: "meta doctor"}
if err := c.ResultExit(struct{}{}, ExitOK, nil); err != nil {
t.Fatalf("ResultExit: %v", err)
}
if c.ExitCode() != ExitOK {
t.Errorf("ExitCode() = %d, want 0", c.ExitCode())
}
var env Envelope
if err := json.Unmarshal(buf.Bytes(), &env); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !env.OK || env.Exit != ExitOK {
t.Errorf("env = {ok:%v exit:%d}, want ok+exit0", env.OK, env.Exit)
}
}

// A plain Result leaves the exit at 0 (back-compat: Run returns ExitOK).
func TestResult_DefaultExitZero(t *testing.T) {
var buf bytes.Buffer
c := &Context{Stdout: &buf, Format: FormatJSON, Command: "x"}
_ = c.Result(struct{}{}, nil)
if c.ExitCode() != ExitOK {
t.Errorf("ExitCode() = %d, want 0", c.ExitCode())
}
}
32 changes: 25 additions & 7 deletions clikit/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ import (
"fmt"
)

// Exit codes — the toolchain-wide ladder (spec §3.3). Every CLI uses these.
// Exit codes — the m engine-driver contract ladder (driver-contract.md §2).
// Every m-iris verb returns one of these; m-cli branches on the code.
//
// 0 ok · 2 usage · 3 gate/tests-failed · 4 conflict/refusal ·
// 5 runtime · 6 engine-unreachable · 7 unsupported (verb/transport)
const (
ExitOK = 0 // success
ExitRuntime = 1 // runtime error (IO / engine / parse)
ExitUsage = 2 // usage error (bad flags/args)
ExitCheck = 3 // --check / lint found findings or drift
ExitRefused = 4 // engine-bound op refused (no engine / substrate unavailable)
ExitOK = 0 // success
ExitUsage = 2 // usage error (bad flags/args)
ExitCheck = 3 // gate: --check/lint findings, drift, or tests failed
ExitRefused = 4 // conflict / refusal (lock held, conflict-check, prune scope)
ExitRuntime = 5 // runtime error (IO / engine fault / parse)
ExitUnreachable = 6 // engine unreachable (no connectivity / auth)
ExitUnsupported = 7 // verb or transport not available on this engine — query caps first
)

// Error is the deterministic, machine-parseable error object. Commands return
Expand All @@ -21,6 +27,11 @@ type Error struct {
Exit int `json:"exit"`
Message string `json:"message"`
Hint string `json:"hint,omitempty"`

// Engine, when set, is surfaced at envelope.engineError (a sibling of
// error, never nested) — the §7 structured engine fault behind a failed
// exec/cover verb.
Engine *EngineError `json:"-"`
}

func (e *Error) Error() string { return e.Message }
Expand All @@ -30,6 +41,13 @@ func Fail(exit int, code, message, hint string) *Error {
return &Error{Code: code, Exit: exit, Message: message, Hint: hint}
}

// FailEngine is Fail plus a §7 engine fault, surfaced at envelope.engineError.
// Use it for exec/cover faults so the real cause (routine, line, mnemonic)
// reaches m-cli alongside the deterministic error code.
func FailEngine(exit int, code, message, hint string, eng *EngineError) *Error {
return &Error{Code: code, Exit: exit, Message: message, Hint: hint, Engine: eng}
}

// exitOf maps any error to an exit code (clikit.Error keeps its own).
func exitOf(err error) int {
var e *Error
Expand All @@ -47,7 +65,7 @@ func RenderError(c *Context, err error) {
e = &Error{Code: "RUNTIME", Exit: ExitRuntime, Message: err.Error()}
}
if c.JSON() {
_ = writeJSON(c.Stderr, Envelope{SchemaVersion: SchemaVersion, Command: c.Command, OK: false, Exit: e.Exit, Error: e})
_ = writeJSON(c.Stderr, Envelope{SchemaVersion: SchemaVersion, Command: c.Command, OK: false, Exit: e.Exit, Error: e, EngineError: e.Engine})
return
}
fmt.Fprintf(c.Stderr, "%s %s\n", c.th.err.render(c.Color, c.gl.Err+" Error:"), e.Message)
Expand Down
4 changes: 3 additions & 1 deletion clikit/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,7 @@ func Run(name, description string, cli any, g *Globals, extra ...kong.Option) in
RenderError(cc, err)
return exitOf(err)
}
return ExitOK
// A command may have recorded a deliberate non-zero exit via ResultExit
// (its data envelope is already on stdout); otherwise this is ExitOK.
return cc.ExitCode()
}
2 changes: 1 addition & 1 deletion clikit/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "runtime"

// Build metadata, injected at link time:
//
// go build -ldflags "-X github.com/vista-cloud-dev/irissync/clikit.Version=$VER \
// go build -ldflags "-X github.com/vista-cloud-dev/m-iris/clikit.Version=$VER \
// -X …/clikit.Commit=$SHA -X …/clikit.Date=$DATE"
var (
Version = "dev"
Expand Down
29 changes: 18 additions & 11 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import (
"sync"
"time"

"github.com/vista-cloud-dev/irissync/clikit"
"github.com/vista-cloud-dev/irissync/internal/atelier"
"github.com/vista-cloud-dev/irissync/internal/config"
"github.com/vista-cloud-dev/irissync/internal/manifest"
"github.com/vista-cloud-dev/irissync/internal/mirror"
"github.com/vista-cloud-dev/m-iris/clikit"
"github.com/vista-cloud-dev/m-iris/internal/atelier"
"github.com/vista-cloud-dev/m-iris/internal/config"
"github.com/vista-cloud-dev/m-iris/internal/manifest"
"github.com/vista-cloud-dev/m-iris/internal/mirror"
)

// --- list --------------------------------------------------------------------
Expand Down Expand Up @@ -301,7 +301,7 @@ func (statusCmd) Run(cc *clikit.Context, conn *config.Conn) error {
renderDiff(cc, d)
}, d.Drift(), "DRIFT",
fmt.Sprintf("%d new, %d changed, %d deleted — mirror out of sync", len(d.New), len(d.Changed), len(d.Deleted)),
"run 'irissync pull' to update the mirror")
"run 'm-iris sync pull' to update the mirror")
}

// --- verify ------------------------------------------------------------------
Expand All @@ -327,7 +327,7 @@ func (verifyCmd) Run(cc *clikit.Context, conn *config.Conn) error {
}
if man == nil {
return clikit.Fail(clikit.ExitRuntime, "NO_MANIFEST",
"no manifest at "+layout.ManifestPath()+"; run 'irissync pull' first", "")
"no manifest at "+layout.ManifestPath()+"; run 'm-iris sync pull' first", "")
}

names, err := scopeManifest(man, conn.Filter, conn.Package)
Expand Down Expand Up @@ -368,7 +368,7 @@ func (verifyCmd) Run(cc *clikit.Context, conn *config.Conn) error {
}
}, drift, "MISMATCH",
fmt.Sprintf("%d mismatched, %d missing — mirror does not match the manifest", len(mismatch), len(missing)),
"re-run 'irissync pull' or investigate tampering")
"re-run 'm-iris sync pull' or investigate tampering")
}

// --- shared helpers ----------------------------------------------------------
Expand Down Expand Up @@ -399,7 +399,7 @@ func runtimeErr(err error) error {
}

func usageErr(err error) error {
return clikit.Fail(clikit.ExitUsage, "BAD_CONFIG", err.Error(), "set flags or IRISSYNC_* env vars")
return clikit.Fail(clikit.ExitUsage, "BAD_CONFIG", err.Error(), "set flags or M_IRIS_* env vars")
}

// selectDocs filters a docnames listing by package prefix and glob filter.
Expand Down Expand Up @@ -437,13 +437,15 @@ func scopeManifest(man *manifest.Manifest, glob, pkg string) ([]string, error) {
}

// match reports whether docname passes the package prefix and glob filter.
// An empty pkg/glob matches everything.
// An empty pkg/glob matches everything. The --filter glob is matched against the
// extension-stripped bare name (driver-contract §5.2, parity with m-ydb), so
// "DG*"/"DGREG" select DGREG.mac but "*.mac" never matches.
func match(docname, glob, pkg string) (bool, error) {
if pkg != "" && !strings.HasPrefix(docname, pkg) {
return false, nil
}
if glob != "" {
ok, err := path.Match(glob, docname)
ok, err := path.Match(glob, bareName(docname))
if err != nil {
return false, fmt.Errorf("invalid --filter %q: %w", glob, err)
}
Expand All @@ -452,6 +454,11 @@ func match(docname, glob, pkg string) (bool, error) {
return true, nil
}

// bareName strips a routine's type extension: "DGREG.mac" → "DGREG".
func bareName(docname string) string {
return strings.TrimSuffix(docname, path.Ext(docname))
}

func docNames(docs []atelier.DocName) []string {
names := make([]string, 0, len(docs))
for _, d := range docs {
Expand Down
Loading
Loading