diff --git a/dist/namespace-registry.json b/dist/namespace-registry.json index 3850352..b705b4f 100644 --- a/dist/namespace-registry.json +++ b/dist/namespace-registry.json @@ -30,6 +30,7 @@ "STDHEX", "STDHTTP", "STDJSON", + "STDKV", "STDLOG", "STDMATH", "STDMOCK", diff --git a/dist/seam-snapshot.json b/dist/seam-snapshot.json index d7f0cfb..49f6838 100644 --- a/dist/seam-snapshot.json +++ b/dist/seam-snapshot.json @@ -29,6 +29,111 @@ } ] }, + "STDKV": { + "contract_version": 1, + "entry_points": [ + { + "args": [ + { + "doc": "collection id", + "name": "coll", + "type": "string" + }, + { + "doc": "record key", + "name": "key", + "type": "string" + } + ], + "label": "$$exists^STDKV(coll, key)", + "raises": [], + "returns": { + "doc": "1 iff the record exists; 0 otherwise", + "type": "bool" + } + }, + { + "args": [ + { + "doc": "collection id", + "name": "coll", + "type": "string" + }, + { + "doc": "record key", + "name": "key", + "type": "string" + }, + { + "doc": "field id within the record", + "name": "field", + "type": "string" + }, + { + "doc": "value returned when the field is unset", + "name": "default", + "type": "string" + } + ], + "label": "$$get^STDKV(coll, key, field, default)", + "raises": [], + "returns": { + "doc": "the stored value, or `default` when unset", + "type": "string" + } + }, + { + "args": [ + { + "doc": "collection id", + "name": "coll", + "type": "string" + }, + { + "doc": "record key", + "name": "key", + "type": "string" + } + ], + "label": "$$kill^STDKV(coll, key)", + "raises": [], + "returns": { + "doc": "1 (idempotent — absent record is a no-op)", + "type": "bool" + } + }, + { + "args": [ + { + "doc": "collection id (VSLFS: a FileMan file number)", + "name": "coll", + "type": "string" + }, + { + "doc": "record key (VSLFS: an IENS)", + "name": "key", + "type": "string" + }, + { + "doc": "field id within the record (VSLFS: a field number)", + "name": "field", + "type": "string" + }, + { + "doc": "the value to store (raw bytes; byte-faithful)", + "name": "value", + "type": "string" + } + ], + "label": "$$set^STDKV(coll, key, field, value)", + "raises": [], + "returns": { + "doc": "1 on success", + "type": "bool" + } + } + ] + }, "STDNET": { "contract_version": 1, "entry_points": [ diff --git a/dist/skill/SKILL.md b/dist/skill/SKILL.md index 93003f4..5e77a5c 100644 --- a/dist/skill/SKILL.md +++ b/dist/skill/SKILL.md @@ -16,7 +16,7 @@ Generated from m-stdlib's `dist/stdlib-manifest.json` — every public module + label, the canonical-idiom library, and the full U-STD* error surface, all rendered for AI / agent context loading. -**Catalogue:** 34 modules, 298 public labels, +**Catalogue:** 35 modules, 302 public labels, 44 error codes. ## When to use this skill @@ -53,6 +53,7 @@ often replace bespoke per-site reinventions. - **`STDHEX`** — RFC-4648 §8 hex encoding (lowercase by default). - **`STDHTTP`** — HTTP/1.1 client (track H3, target tag v0.4.0). - **`STDJSON`** — RFC 8259 JSON parser + serialiser. +- **`STDKV`** — minimal keyed record-store seam (the S1 storage contract). - **`STDLOG`** — structured key=value logger (v0.0.4). - **`STDMATH`** — Numeric helpers (clamp / min / max / sum / count / mean over arrays). - **`STDMOCK`** — opt-in test-time call interception (mock registry). diff --git a/dist/skill/manifest-index.md b/dist/skill/manifest-index.md index 1be7181..f4cbeef 100644 --- a/dist/skill/manifest-index.md +++ b/dist/skill/manifest-index.md @@ -1,6 +1,6 @@ # m-stdlib — manifest index -m-stdlib v0.8.0; 34 modules; 298 public labels. +m-stdlib v0.8.0; 35 modules; 302 public labels. Generated from `dist/stdlib-manifest.json`. One entry per module with every public label: signature on the left, synopsis on the @@ -293,6 +293,15 @@ RFC 8259 JSON parser + serialiser. _raises: `U-STDJSON-ENCODE`, `U-STDJSON-PARSE`_ +## `STDKV` + +minimal keyed record-store seam (the S1 storage contract). + +- `$$exists^STDKV(coll, key)` — Return 1 iff record (coll,key) has any field set; else 0. +- `$$get^STDKV(coll, key, field, default)` — Read the value at (coll,key,field); else `default`. +- `$$kill^STDKV(coll, key)` — Remove the whole record (coll,key); return 1 (idempotent). +- `$$set^STDKV(coll, key, field, value)` — Store `value` at (coll,key,field); return 1. + ## `STDLOG` structured key=value logger (v0.0.4). diff --git a/dist/stdlib-manifest.json b/dist/stdlib-manifest.json index 57a503e..b89c36a 100644 --- a/dist/stdlib-manifest.json +++ b/dist/stdlib-manifest.json @@ -6952,6 +6952,204 @@ }, "tier": "core" }, + "STDKV": { + "synopsis": "m-stdlib — minimal keyed record-store seam (the S1 storage contract).", + "description": "A portable record store: $$set/$$get/$$exists/$$kill address a value by\n(collection, key, field). It is the engine-neutral half of the MSL/VSL\nstorage seam (S1) — v-stdlib's VSLFS binds this same four-verb signature\nto VistA's FileMan DBS (GETS^DIQ / UPDATE^DIE / …) above the waterline,\nmapping collection->file, key->iens, field->field number. On a bare\nengine STDKV is its own reference back end (a process-private global),\nso the seam is real and testable with no VistA present.\n\nThis is a record store, NOT a filesystem — STDFS stays path/byte I/O.\nThe contract is deliberately minimal: only the four verbs the storage\nacceptance needs. Any non-storage logic stays in the caller; the seam\ncarries no parsing/formatting.\n\nPublic extrinsics (all functions — call with $$, never `do`):\n $$set^STDKV(coll,key,field,value) — store a field value -> 1\n $$get^STDKV(coll,key,field,default) — read a field value, else default\n $$exists^STDKV(coll,key) — 1 iff the record has any field\n $$kill^STDKV(coll,key) — remove a whole record -> 1\n\nReference back end: per-process records live in\n^STDLIB($job,\"kv\",coll,key,field)=value (process-private; no VistA, no\ncross-run bleed). The VSLFS adapter swaps this for FileMan; the\nsignature is identical, so callers are unchanged across back ends.", + "errors": [], + "labels": { + "set": { + "form": "extrinsic", + "signature": "$$set^STDKV(coll, key, field, value)", + "synopsis": "Store `value` at (coll,key,field); return 1.", + "params": [ + { + "name": "coll", + "type": "string", + "doc": "collection id (VSLFS: a FileMan file number)" + }, + { + "name": "key", + "type": "string", + "doc": "record key (VSLFS: an IENS)" + }, + { + "name": "field", + "type": "string", + "doc": "field id within the record (VSLFS: a field number)" + }, + { + "name": "value", + "type": "string", + "doc": "the value to store (raw bytes; byte-faithful)" + } + ], + "returns": { + "type": "bool", + "doc": "1 on success" + }, + "raises": [], + "raised_in_body": [], + "examples": [ + "set ok=$$set^STDKV(\"demo.cfg\",1,\".01\",\"hello\")" + ], + "since": "v0.9.0", + "stable": "stable", + "see_also": [ + "$$get^STDKV", + "$$kill^STDKV" + ], + "deprecated": "", + "seam": { + "name": "STDKV", + "contract_version": 1 + }, + "description": "", + "source": { + "file": "src/STDKV.m", + "line": 31 + } + }, + "get": { + "form": "extrinsic", + "signature": "$$get^STDKV(coll, key, field, default)", + "synopsis": "Read the value at (coll,key,field); else `default`.", + "params": [ + { + "name": "coll", + "type": "string", + "doc": "collection id" + }, + { + "name": "key", + "type": "string", + "doc": "record key" + }, + { + "name": "field", + "type": "string", + "doc": "field id within the record" + }, + { + "name": "default", + "type": "string", + "doc": "value returned when the field is unset" + } + ], + "returns": { + "type": "string", + "doc": "the stored value, or `default` when unset" + }, + "raises": [], + "raised_in_body": [], + "examples": [ + "set v=$$get^STDKV(\"demo.cfg\",1,\".01\",\"none\")" + ], + "since": "v0.9.0", + "stable": "stable", + "see_also": [ + "$$set^STDKV" + ], + "deprecated": "", + "seam": { + "name": "STDKV", + "contract_version": 1 + }, + "description": "", + "source": { + "file": "src/STDKV.m", + "line": 45 + } + }, + "exists": { + "form": "extrinsic", + "signature": "$$exists^STDKV(coll, key)", + "synopsis": "Return 1 iff record (coll,key) has any field set; else 0.", + "params": [ + { + "name": "coll", + "type": "string", + "doc": "collection id" + }, + { + "name": "key", + "type": "string", + "doc": "record key" + } + ], + "returns": { + "type": "bool", + "doc": "1 iff the record exists; 0 otherwise" + }, + "raises": [], + "raised_in_body": [], + "examples": [ + "set there=$$exists^STDKV(\"demo.cfg\",1)" + ], + "since": "v0.9.0", + "stable": "stable", + "see_also": [ + "$$kill^STDKV" + ], + "deprecated": "", + "seam": { + "name": "STDKV", + "contract_version": 1 + }, + "description": "", + "source": { + "file": "src/STDKV.m", + "line": 58 + } + }, + "kill": { + "form": "extrinsic", + "signature": "$$kill^STDKV(coll, key)", + "synopsis": "Remove the whole record (coll,key); return 1 (idempotent).", + "params": [ + { + "name": "coll", + "type": "string", + "doc": "collection id" + }, + { + "name": "key", + "type": "string", + "doc": "record key" + } + ], + "returns": { + "type": "bool", + "doc": "1 (idempotent — absent record is a no-op)" + }, + "raises": [], + "raised_in_body": [], + "examples": [ + "set ok=$$kill^STDKV(\"demo.cfg\",1)" + ], + "since": "v0.9.0", + "stable": "stable", + "see_also": [ + "$$set^STDKV", + "$$exists^STDKV" + ], + "deprecated": "", + "seam": { + "name": "STDKV", + "contract_version": 1 + }, + "description": "", + "source": { + "file": "src/STDKV.m", + "line": 69 + } + } + }, + "source": { + "file": "src/STDKV.m", + "line": 1 + }, + "tier": "core" + }, "STDLOG": { "synopsis": "m-stdlib — structured key=value logger (v0.0.4).", "description": "m-lint: disable-file=M-MOD-024\nM-MOD-024 false positives: the linter parses YDB OPEN deviceparams\n(e.g. APPEND) in writeLine() as local-variable reads. Same cause as\nSTDCSV's file-wide disable; tracked in TOOLCHAIN-FINDINGS.md.\n\nPublic entry points:\n DEBUG^STDLOG(event,k1,v1,...,k5,v5) — up to 5 kv pairs\n INFO^STDLOG(event,...)\n WARN^STDLOG(event,...)\n ERROR^STDLOG(event,...)\n FATAL^STDLOG(event,...)\n LEVEL^STDLOG(threshold) — \"DEBUG\"|\"INFO\"|\"WARN\"|\"ERROR\"|\"FATAL\"\n SINK^STDLOG(target) — \"stderr\"|\"stdout\"|\"global\"|\"global:^GREF\"\n FORMAT^STDLOG(name) — \"kv\" (default) | \"json\"\n\nOutput line format (kv, default):\n level= event= k=v k=v ...\n\nOutput line format (json):\n {\"ts\":\"\",\"level\":\"\",\"event\":\"\",\"k\":\"v\",...}\n All values are emitted as JSON strings (preserves the kv contract\n that values are opaque text). Built via $$encode^STDJSON, so the\n line is byte-exactly conformant RFC 8259.\n\nValue escaping (kv): a value with no space, '=', '\"', or '\\' is\nemitted raw. Otherwise it is wrapped in double quotes, with embedded\n'\\' doubled to '\\\\' and embedded '\"' escaped to '\\\"'.\n\nDefaults: threshold=INFO, sink=stderr, format=kv. Configuration is\nprocess-local (held under ^STDLIB($job,\"stdlog\",\"...\")).\n\nErrors set $ECODE to one of:\n ,U-STDLOG-INVALID-LEVEL,\n ,U-STDLOG-INVALID-SINK,\n ,U-STDLOG-INVALID-FORMAT,\n\nTimestamp source: $$now^STDDATE() (millisecond-precision ISO-8601\nUTC ending in Z). v0.0.4 shipped an inline helper; track L4b\n(this commit) bumps to STDDATE now that v0.0.5 is in.", @@ -12307,6 +12505,111 @@ } ] }, + "STDKV": { + "contract_version": 1, + "entry_points": [ + { + "label": "$$exists^STDKV(coll, key)", + "args": [ + { + "name": "coll", + "type": "string", + "doc": "collection id" + }, + { + "name": "key", + "type": "string", + "doc": "record key" + } + ], + "returns": { + "type": "bool", + "doc": "1 iff the record exists; 0 otherwise" + }, + "raises": [] + }, + { + "label": "$$get^STDKV(coll, key, field, default)", + "args": [ + { + "name": "coll", + "type": "string", + "doc": "collection id" + }, + { + "name": "key", + "type": "string", + "doc": "record key" + }, + { + "name": "field", + "type": "string", + "doc": "field id within the record" + }, + { + "name": "default", + "type": "string", + "doc": "value returned when the field is unset" + } + ], + "returns": { + "type": "string", + "doc": "the stored value, or `default` when unset" + }, + "raises": [] + }, + { + "label": "$$kill^STDKV(coll, key)", + "args": [ + { + "name": "coll", + "type": "string", + "doc": "collection id" + }, + { + "name": "key", + "type": "string", + "doc": "record key" + } + ], + "returns": { + "type": "bool", + "doc": "1 (idempotent — absent record is a no-op)" + }, + "raises": [] + }, + { + "label": "$$set^STDKV(coll, key, field, value)", + "args": [ + { + "name": "coll", + "type": "string", + "doc": "collection id (VSLFS: a FileMan file number)" + }, + { + "name": "key", + "type": "string", + "doc": "record key (VSLFS: an IENS)" + }, + { + "name": "field", + "type": "string", + "doc": "field id within the record (VSLFS: a field number)" + }, + { + "name": "value", + "type": "string", + "doc": "the value to store (raw bytes; byte-faithful)" + } + ], + "returns": { + "type": "bool", + "doc": "1 on success" + }, + "raises": [] + } + ] + }, "STDNET": { "contract_version": 1, "entry_points": [ diff --git a/docs/memory/MEMORY.md b/docs/memory/MEMORY.md index 7889564..9f93d8a 100644 --- a/docs/memory/MEMORY.md +++ b/docs/memory/MEMORY.md @@ -17,4 +17,5 @@ One line per memory file. Content lives in the files, not here. - [s3-connector-design](s3-connector-design.md) — the m-stdlib S3 connector design (STDS3/STDSIGV4 portable + VSLS3 VistA log sink); SigV4↔STDCRYPTO mapping, the §6.2 log-egress worked example; doc at docs/plans/m-stdlib-s3-design.md. - [v-cli-platform](v-cli-platform.md) — the `v` CLI platform for VistA developer tools: the m-*/v-* naming scheme (split by scope, not language), single `v` CLI with plain-noun domains (v pkg/db/config/…), per-domain command contract + generated registry + shared Go template; m-kids refiles as the `v pkg` domain (repo v-pkg). Doc at docs/plans/v-cli-platform.md. - [vista-library-promotion-plan](vista-library-promotion-plan.md) — plan to promote reuse of VistA's de-facto library: a discoverability/blessing/enforcement problem (not absence-of-library); one generated `vista-lib-registry.json` projects into cheatsheet + `v lib` CLI + reinvention lint + LSP; MVP = L0 registry + L1; v-family tooling, complementary to VSL. Doc at docs/plans/vista-library-promotion-plan.md. +- [m3-stdkv-storage-seam](m3-stdkv-storage-seam.md) — VSL/MSL M3 Lane A: STDKV, the portable keyed record-store leaf (S1 storage seam). NEW minimal MSL module (resolved Q3: a record store, NOT a STDFS sub-API) — `$$set/$$get/$$exists/$$kill` over `(coll,key,field)`; global-backed `^STDLIB($job,"kv",…)` reference impl, **dual-engine GREEN 12/12** (YDB+IRIS, no `$ZVERSION` arm), byte-exact. `@seam STDKV` (4 verbs, bump-forcer green NEW seam); all `$$` (no `do`). NOT in the KIDS base (like STDNET). Owed: tag MSL `v0.9.0` → Lane B (`VSLFS` in v-stdlib). - [m2-stdnet-socket-seam](m2-stdnet-socket-seam.md) — VSL/MSL M2 Lane A: STDNET, the portable raw-TCP socket leaf (S4 seam). YottaDB loopback echo GREEN 9/9 on m-test-engine; `@seam STDNET` (6 verbs, bump-forcer green); optional tier; IRIS deferred (soft-skip, `$$available`=0). Hard-won YDB socket idiom (`/WAIT` auto-accepts, no `/ACCEPT` → LOCALSOCKREQ; collection-of-sockets enables single-process loopback; save/restore `$IO`); the `~/m-work` mount trick to read engine `$ZSTATUS` past the runner's `0/0` swallow. Owed: tag MSL `v0.8.0` → Lane B (`VSLIO` in v-stdlib). diff --git a/docs/memory/m3-stdkv-storage-seam.md b/docs/memory/m3-stdkv-storage-seam.md new file mode 100644 index 0000000..a318eaf --- /dev/null +++ b/docs/memory/m3-stdkv-storage-seam.md @@ -0,0 +1,68 @@ +--- +name: m3-stdkv-storage-seam +description: VSL/MSL M3 Lane A — STDKV, the portable keyed record-store leaf (S1 storage seam). New minimal MSL module ($$set/$$get/$$exists/$$kill over (coll,key,field)), global-backed reference impl, dual-engine GREEN 12/12 (YDB+IRIS), @seam STDKV emitted (4 verbs, bump-forcer green NEW seam). VSLFS (v-stdlib) binds it to FileMan DBS. Tag v0.9.0 owed. +metadata: + type: project +--- + +# STDKV — the portable storage leaf (VSL/MSL M3 Lane A, 2026-06-16) + +The leaf-first MSL half of **M3** (the `VSLFS` storage seam, S1). Branch +`m3-stdkv` off `master` (v0.8.0). **Resolved design Q3:** the storage seam is a +**new minimal MSL module**, NOT a sub-API of `STDFS` — a record store is +conceptually distinct from filesystem path/byte I/O, and overloading `STDFS` +would muddy its seam. STDFS stays filesystem-only. + +## The seam (`@seam STDKV` — 4 verbs, contract_version 1) +A record store addressed by **(collection, key, field)** — all four verbs are +extrinsic functions (call with `$$`, never `do`): +- `$$set^STDKV(coll,key,field,value)` → 1 (store) +- `$$get^STDKV(coll,key,field,default)` → value or default +- `$$exists^STDKV(coll,key)` → 1/0 (record has any field) +- `$$kill^STDKV(coll,key)` → 1 (remove whole record, idempotent) + +Reference back end = process-private `^STDLIB($job,"kv",coll,key,field)=value` +(no VistA, no cross-run bleed, byte-faithful). The kickoff only needs the seam +"real and testable on a bare engine"; persistence/FileMan is VSLFS's job. The +four verbs all carry `; doc: @seam STDKV` → `seams.STDKV` in +stdlib-manifest.json + dist/seam-snapshot.json; **bump-forcer green (NEW seam, +no bump)**. The doc-comment maps the generic names to FileMan for the adapter: +coll→file number, key→IENS, field→field number (so VSLFS mirrors the signature). + +## Why all four verbs are `$$` (and the test consequence) +The plan's notation is `$$get/$$set/$$exists/$$kill`, and a `quit ` label +(set/kill return 1) **cannot be invoked with `do`** (raises in M, same trap STDNET +hit with `write`/`close`). So STDKVTST consumes every return: +`set x=$$set^STDKV(...)` / `set x=$$kill^STDKV(...)`, never `do set^STDKV(...)`. + +## Acceptance — dual-engine GREEN 12/12 +7 tests / 12 assertions: set→get round-trip, default-when-unset (absent +record + absent field), exists lifecycle, kill removes record, multi-field +independence, **byte fidelity** (`$char(0)`/`$char(9)`/`$char(200)`/`$char(255)` +round-trips exact), set-returns-1. GREEN on BOTH `m test --engine ydb --docker +m-test-engine --chset m` AND `--engine iris --docker foia-t12 --namespace VISTA`, +`--routines src tests/STDKVTST.m`. **No `$ZVERSION` arm** — pure global I/O is +portable as-is (unlike STDNET's socket device). It's a CORE suite (deterministic, +no callouts) — in `make test` (46 suites, 2448/2448, no regression). + +## Gates / packaging +- `make manifest seams skill doctest namespaces frontmatter` regenerated; no + STDKVDOCTST (the `set`-style @examples are not Pattern-A `write … ; "…"`, so + doctest gen skips them — intentional, keeps the side-effecting seam out of + doctests). +- All engine-free gates exit 0: fmt/lint (0 error-severity) · check-manifest · + check-seams (3 seams) · check-namespaces (35 routines) · check-icr (0 — STDKV + is engine-neutral, no L4 calls) · check-citations · check-docs-prose · + **check-kids** · `m arch check .` layer **m**, G2 clean (no VistA symbols — the + whole point of the leaf). +- **NOT added to the MSL KIDS base** (`kids/std.build.json`, 17 curated + routines) — STDNET isn't either; whether STDKV ships in the VistA base is an + M5/VSLBLD packaging decision, not this leaf's scope. +- module-tracker row 35 added; docs/modules/stdkv.md added. + +## What's owed (Lane B + release) +- **Tag MSL `v0.9.0`** (user action) carrying `seams.STDKV` so v-stdlib's + `VSLFS` (Lane B) can re-pin `msl_ref` v0.8.0→v0.9.0 and bind it over FileMan + DBS (`GETS^DIQ`/`$$GET1^DIQ`/`UPDATE^DIE`/`FILE^DIE`/`FIND1^DIC`). +- Branch `m3-stdkv` unmerged; merge + tag stay user actions. Companion to + [[m2-stdnet-socket-seam]] (the S4 leaf-first rhythm this copies). diff --git a/docs/modules/stdkv.md b/docs/modules/stdkv.md new file mode 100644 index 0000000..7fe855f --- /dev/null +++ b/docs/modules/stdkv.md @@ -0,0 +1,70 @@ +--- +module: STDKV +tag: v0.9.0 +phase: M3 — VSL/MSL S1 storage seam +stable: stable +since: v0.9.0 +synopsis: 'minimal keyed record-store seam (the S1 storage contract)' +labels: ['exists', 'get', 'kill', 'set'] +errors: [] +conformance: [] +see_also: ['STDFS'] +--- + +# `STDKV` — minimal keyed record-store seam + +`STDKV` is the portable storage seam (**S1** of the MSL⟷VSL coordination plan): +a tiny record store that addresses a value by **(collection, key, field)**. It is +the engine-neutral half of the storage seam that the VistA `VSLFS` adapter binds +to FileMan's Database Server (DBS) API (`GETS^DIQ` / `$$GET1^DIQ` / `UPDATE^DIE` +/ `FILE^DIE` / `FIND1^DIC`) above the m/v waterline — mapping +`collection → file`, `key → IENS`, `field → field number`. + +On a bare engine `STDKV` is **its own reference back end** (a process-private +global), so the seam is real and testable with no VistA present. The `VSLFS` +adapter swaps that back end for FileMan while keeping the four-verb signature +identical, so callers are unchanged across back ends. + +This is a **record store, not a filesystem** — `STDFS` stays path/byte I/O. The +contract is deliberately minimal: only the four verbs the storage acceptance +needs, and the seam carries no parsing/formatting (that stays in the caller). + +## Public API + +All four verbs are extrinsic functions — call them with `$$`, never `do` (a +`$$`-style `quit ` label cannot be invoked with `do`). The side-effecting +verbs (`$$set`/`$$kill`) return `1`. + +| Call | Purpose | +|---|---| +| `$$set^STDKV(coll,key,field,value)` | store `value` at `(coll,key,field)` → `1` | +| `$$get^STDKV(coll,key,field,default)` | read the field value, or `default` when unset | +| `$$exists^STDKV(coll,key)` | `1` iff the record `(coll,key)` has any field set | +| `$$kill^STDKV(coll,key)` | remove a whole record (idempotent) → `1` | + +### Round-trip (the shape `STDKVTST` proves) + +```m +set ok=$$set^STDKV("demo.cfg",1,".01","hello") ; store +write $$get^STDKV("demo.cfg",1,".01","none") ; "hello" +write $$exists^STDKV("demo.cfg",1) ; 1 +set ok=$$kill^STDKV("demo.cfg",1) ; remove +write $$exists^STDKV("demo.cfg",1) ; 0 +write $$get^STDKV("demo.cfg",1,".01","none") ; "none" (default) +``` + +## Engine support & portability + +Fully portable — pure global I/O, no engine-specific arms. Records live in the +process-private `^STDLIB($job,"kv",coll,key,field)`, so runs never bleed into one +another and parallel suites are safe. Values are stored verbatim, so binary / +control-byte values round-trip byte-exact under `ydb_chset=M`. Verified GREEN on +**both** YottaDB (`m-test-engine`) and IRIS (`foia-t12`), 12/12 assertions. + +## The seam + +The four verbs carry `; doc: @seam STDKV`, so they project into the `seams` block +of `dist/stdlib-manifest.json` (and `dist/seam-snapshot.json`) as the +`contract_version: 1` storage contract. `v-stdlib`'s `VSLFS` pins this contract +(via `dist/msl-seam-pin.json`) and binds it to FileMan DBS; a signature change +here without a `contract_version` bump is a red gate (the seam bump-forcer). diff --git a/docs/tracking/changelog.md b/docs/tracking/changelog.md index 9743fc0..e25f6f4 100644 --- a/docs/tracking/changelog.md +++ b/docs/tracking/changelog.md @@ -1,7 +1,7 @@ --- created: 2026-04-30 -last_modified: 2026-06-15 -revisions: 39 +last_modified: 2026-06-16 +revisions: 40 doc_type: [CHANGELOG] --- @@ -18,6 +18,31 @@ tickets, per-module History sections, discoveries register). The deep implementation narrative lives in those sources, not duplicated here. See [`README.md`](README.md) § Bucket 4 for the rationale. +## [v0.9.0] — 2026-06-16 + +**The portable storage seam — `STDKV` (VSL/MSL M3 Lane A).** This release adds +the **third `@seam`** and a new minimal module: `STDKV`, a keyed record store +addressing a value by `(collection, key, field)` with four extrinsic verbs — +`$$set` / `$$get` / `$$exists` / `$$kill`. It resolves design Q3 — the storage +seam is its own module, **not** a `STDFS` sub-API (a record store is distinct +from filesystem path/byte I/O), so `STDFS` stays filesystem-only. On a bare +engine `STDKV` is its own reference back end (a process-private +`^STDLIB($job,"kv",…)` global); `v-stdlib`'s `VSLFS` adapter (M3, the S1 seam) +binds the same four-verb signature to VistA's FileMan DBS above the m/v +waterline (`coll`→file, `key`→IENS, `field`→field number). All four verbs carry +`; doc: @seam STDKV`, so the `seams.STDKV` block (`contract_version: 1`, 4 entry +points) now appears in [`dist/stdlib-manifest.json`](../../dist/stdlib-manifest.json) +and its [`dist/seam-snapshot.json`](../../dist/seam-snapshot.json) projection; +the bump-forcer is green (a newly-introduced seam needs no `contract_version` +bump). `v-stdlib` re-pins this git ref (`msl_ref` v0.8.0→v0.9.0). + +- **Dual-engine GREEN 12/12** — `STDKVTST` passes on both YottaDB + (`m-test-engine`) and IRIS (`foia-t12`), including byte-fidelity (control and + high bytes round-trip exact). No `$ZVERSION` arm — pure global I/O is portable + as-is. CORE suite: full `make test` is 46 suites / 2448 assertions, no + regression. +- Module-tracker row 35 + [`docs/modules/stdkv.md`](../modules/stdkv.md). + ## [v0.8.0] — 2026-06-16 **The portable socket seam — `STDNET` (VSL/MSL M2 Lane A).** This release adds diff --git a/docs/tracking/module-tracker.md b/docs/tracking/module-tracker.md index 8636fa1..4d9f816 100644 --- a/docs/tracking/module-tracker.md +++ b/docs/tracking/module-tracker.md @@ -119,6 +119,7 @@ current state. | [x] | P3 | H3 | 32 | [`STDHTTP`](../modules/stdhttp.md) | `v0.4.0` | 4d | none (options) | STDURL; `$&stdhttp.fn → libcurl`; A6 | HTTP/1.1 client + pure-M wire-format helpers (+ IRIS-native arm: `%Net.HttpRequest`) | 🟡 C14 | | [ ] | — | T1 | 33 | [`STDHARN`](../modules/stdharn.md) | — | 3d | P2 `^%MONLBL` coverage (STDCOV) · P4 STDWATCH hooks | STDASSERT (no-halt orchestration mode) | Resident pure-M test/coverage harness — frames `^STDASSERT` suites for m-cli 5.1 (server-side delegation) | ✅ `internal/harness` (P0–P1) | | [ ] | M2 | S4 | 34 | [`STDNET`](../modules/stdnet.md) | `v0.8.0` (pending) | 8–14d | **IRIS leg** (`|TCP|` device + `JOB`'d-connector loopback; live-verify on m-test-iris) | none (engine-native `SOCKET` device; `$ZVERSION["IRIS"` arms) | Portable raw-TCP socket primitives (`listen`/`accept`/`connect`/`read`/`write`/`close`); **YottaDB loopback echo GREEN 9/9**, IRIS soft-skips (`$$available`=0). `@seam STDNET` (VSL/MSL S4) | `VSLIO` (v-stdlib, M2) binds it over `^%ZIS`/`CALL^%ZISTCP` + TLS | +| [x] | M3 | S1 | 35 | [`STDKV`](../modules/stdkv.md) | `v0.9.0` (pending) | 1d | none (completed) | none (pure global-backed reference store; no engine arms) | Minimal keyed record-store seam — `$$set`/`$$get`/`$$exists`/`$$kill` over `(coll,key,field)`; **dual-engine GREEN 12/12** (YDB + IRIS), byte-exact. `@seam STDKV` (VSL/MSL S1) | `VSLFS` (v-stdlib, M3) binds it to FileMan DBS (`GETS^DIQ`/`UPDATE^DIE`/…) | **Aggregate.** ~108d shipped across all 32 landed modules (sum of the Effort column above). **Full engine suite green on `main` diff --git a/src/STDKV.m b/src/STDKV.m new file mode 100644 index 0000000..e2712b0 --- /dev/null +++ b/src/STDKV.m @@ -0,0 +1,79 @@ +STDKV ; m-stdlib — minimal keyed record-store seam (the S1 storage contract). + ; + ; A portable record store: $$set/$$get/$$exists/$$kill address a value by + ; (collection, key, field). It is the engine-neutral half of the MSL/VSL + ; storage seam (S1) — v-stdlib's VSLFS binds this same four-verb signature + ; to VistA's FileMan DBS (GETS^DIQ / UPDATE^DIE / …) above the waterline, + ; mapping collection->file, key->iens, field->field number. On a bare + ; engine STDKV is its own reference back end (a process-private global), + ; so the seam is real and testable with no VistA present. + ; + ; This is a record store, NOT a filesystem — STDFS stays path/byte I/O. + ; The contract is deliberately minimal: only the four verbs the storage + ; acceptance needs. Any non-storage logic stays in the caller; the seam + ; carries no parsing/formatting. + ; + ; Public extrinsics (all functions — call with $$, never `do`): + ; $$set^STDKV(coll,key,field,value) — store a field value -> 1 + ; $$get^STDKV(coll,key,field,default) — read a field value, else default + ; $$exists^STDKV(coll,key) — 1 iff the record has any field + ; $$kill^STDKV(coll,key) — remove a whole record -> 1 + ; + ; Reference back end: per-process records live in + ; ^STDLIB($job,"kv",coll,key,field)=value (process-private; no VistA, no + ; cross-run bleed). The VSLFS adapter swaps this for FileMan; the + ; signature is identical, so callers are unchanged across back ends. + ; + quit + ; + ; ---------- the storage seam (4 verbs) ---------- + ; +set(coll,key,field,value) ; Store `value` at (coll,key,field); return 1. + ; doc: @param coll string collection id (VSLFS: a FileMan file number) + ; doc: @param key string record key (VSLFS: an IENS) + ; doc: @param field string field id within the record (VSLFS: a field number) + ; doc: @param value string the value to store (raw bytes; byte-faithful) + ; doc: @returns bool 1 on success + ; doc: @example set ok=$$set^STDKV("demo.cfg",1,".01","hello") + ; doc: @since v0.9.0 + ; doc: @stable stable + ; doc: @seam STDKV + ; doc: @see $$get^STDKV, $$kill^STDKV + set ^STDLIB($job,"kv",coll,key,field)=value + quit 1 + ; +get(coll,key,field,default) ; Read the value at (coll,key,field); else `default`. + ; doc: @param coll string collection id + ; doc: @param key string record key + ; doc: @param field string field id within the record + ; doc: @param default string value returned when the field is unset + ; doc: @returns string the stored value, or `default` when unset + ; doc: @example set v=$$get^STDKV("demo.cfg",1,".01","none") + ; doc: @since v0.9.0 + ; doc: @stable stable + ; doc: @seam STDKV + ; doc: @see $$set^STDKV + quit $get(^STDLIB($job,"kv",coll,key,field),default) + ; +exists(coll,key) ; Return 1 iff record (coll,key) has any field set; else 0. + ; doc: @param coll string collection id + ; doc: @param key string record key + ; doc: @returns bool 1 iff the record exists; 0 otherwise + ; doc: @example set there=$$exists^STDKV("demo.cfg",1) + ; doc: @since v0.9.0 + ; doc: @stable stable + ; doc: @seam STDKV + ; doc: @see $$kill^STDKV + quit $select($data(^STDLIB($job,"kv",coll,key)):1,1:0) + ; +kill(coll,key) ; Remove the whole record (coll,key); return 1 (idempotent). + ; doc: @param coll string collection id + ; doc: @param key string record key + ; doc: @returns bool 1 (idempotent — absent record is a no-op) + ; doc: @example set ok=$$kill^STDKV("demo.cfg",1) + ; doc: @since v0.9.0 + ; doc: @stable stable + ; doc: @seam STDKV + ; doc: @see $$set^STDKV, $$exists^STDKV + kill ^STDLIB($job,"kv",coll,key) + quit 1 diff --git a/tests/STDKVTST.m b/tests/STDKVTST.m new file mode 100644 index 0000000..22a2ab2 --- /dev/null +++ b/tests/STDKVTST.m @@ -0,0 +1,90 @@ +STDKVTST ; m-stdlib — STDKV (keyed record-store seam) test suite. + ; Exercises the portable record-store reference implementation on a bare + ; engine (global-backed, no VistA): $$set/$$get/$$exists/$$kill over a + ; (collection, key, field) address. This is the MSL S1 storage seam that + ; v-stdlib's VSLFS binds to FileMan DBS above the waterline. + ; + ; All four verbs are extrinsic functions; callers use $$ (a $$-style + ; `quit ` label cannot be invoked with `do`). Side-effecting + ; verbs ($$set/$$kill) return 1; the tests consume the return. + new pass,fail + do start^STDASSERT(.pass,.fail) + ; + do tSetGetRoundtrip(.pass,.fail) + do tGetDefaultWhenUnset(.pass,.fail) + do tExistsLifecycle(.pass,.fail) + do tKillRemovesRecord(.pass,.fail) + do tMultiFieldIndependent(.pass,.fail) + do tByteFidelity(.pass,.fail) + do tSetReturnsTrue(.pass,.fail) + ; + do report^STDASSERT(pass,fail) + quit + ; +tSetGetRoundtrip(pass,fail) ;@TEST "$$set then $$get returns the stored value byte-identical" + new c,x + set c="STDKVTST.rt" + set x=$$kill^STDKV(c,1) + set x=$$set^STDKV(c,1,".01","Hello, world") + do eq^STDASSERT(.pass,.fail,$$get^STDKV(c,1,".01","MISS"),"Hello, world","stored value reads back") + set x=$$kill^STDKV(c,1) + quit + ; +tGetDefaultWhenUnset(pass,fail) ;@TEST "$$get returns the default for an absent field/record" + new c,x + set c="STDKVTST.def" + set x=$$kill^STDKV(c,1) + do eq^STDASSERT(.pass,.fail,$$get^STDKV(c,1,".01","fallback"),"fallback","absent record yields default") + set x=$$set^STDKV(c,1,".01","x") + do eq^STDASSERT(.pass,.fail,$$get^STDKV(c,1,".02","fallback"),"fallback","absent field yields default") + set x=$$kill^STDKV(c,1) + quit + ; +tExistsLifecycle(pass,fail) ;@TEST "$$exists is false before a set, true after" + new c,x + set c="STDKVTST.ex" + set x=$$kill^STDKV(c,7) + do eq^STDASSERT(.pass,.fail,$$exists^STDKV(c,7),0,"record absent before set") + set x=$$set^STDKV(c,7,".01","present") + do eq^STDASSERT(.pass,.fail,$$exists^STDKV(c,7),1,"record present after set") + set x=$$kill^STDKV(c,7) + quit + ; +tKillRemovesRecord(pass,fail) ;@TEST "$$kill removes the record so $$exists is false and $$get returns default" + new c,x + set c="STDKVTST.kill" + set x=$$set^STDKV(c,3,".01","doomed") + do eq^STDASSERT(.pass,.fail,$$exists^STDKV(c,3),1,"record exists pre-kill") + set x=$$kill^STDKV(c,3) + do eq^STDASSERT(.pass,.fail,$$exists^STDKV(c,3),0,"record gone post-kill") + do eq^STDASSERT(.pass,.fail,$$get^STDKV(c,3,".01","gone"),"gone","field gone post-kill") + quit + ; +tMultiFieldIndependent(pass,fail) ;@TEST "fields on one record store and read independently" + new c,x + set c="STDKVTST.mf" + set x=$$kill^STDKV(c,1) + set x=$$set^STDKV(c,1,".01","name") + set x=$$set^STDKV(c,1,".02","value") + do eq^STDASSERT(.pass,.fail,$$get^STDKV(c,1,".01","MISS"),"name","field .01 intact") + do eq^STDASSERT(.pass,.fail,$$get^STDKV(c,1,".02","MISS"),"value","field .02 intact") + set x=$$kill^STDKV(c,1) + quit + ; +tByteFidelity(pass,fail) ;@TEST "values with control and high bytes round-trip byte-exact" + new c,x,v + set c="STDKVTST.bytes" + set v=$char(0)_"a"_$char(9)_$char(200)_$char(255) + set x=$$kill^STDKV(c,1) + set x=$$set^STDKV(c,1,".01",v) + do eq^STDASSERT(.pass,.fail,$$get^STDKV(c,1,".01",""),v,"byte-exact round-trip") + set x=$$kill^STDKV(c,1) + quit + ; +tSetReturnsTrue(pass,fail) ;@TEST "$$set returns 1 on success" + new c,x + set c="STDKVTST.ret" + set x=$$kill^STDKV(c,1) + do eq^STDASSERT(.pass,.fail,$$set^STDKV(c,1,".01","ok"),1,"$$set returns 1") + set x=$$kill^STDKV(c,1) + quit