diff --git a/AGENTS.md b/AGENTS.md index 7107def..fe9d556 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -169,12 +169,14 @@ make doctest # → tests/STDDOCTST.m for every module with Pattern- make frontmatter # re-syncs YAML frontmatter on docs/modules/std*.md ``` -The Phase 0 `repo.meta.json` (`dist/repo.meta.json`) is hand-edited — -not regenerated — and is covered by `make check-manifest` (see below). +The root `repo.meta.json` is hand-edited — not regenerated — and is +covered by `make check-manifest` (see below). It lives at the repo ROOT +(the standardized "one meta artifact, one location"); `m arch check` +reads it root-first and validates its shape. ## Verify -These commands match `dist/repo.meta.json`'s `verification_commands` +These commands match `repo.meta.json`'s `verification_commands` and are what an agent should run to confirm a change in this repo: ```bash @@ -189,10 +191,10 @@ make check-manifest # drift gate: dist/ matches src/ AND repo.meta.json is co `dist/skill/`, or `tests/STD*DOCTST.m`.** They are regenerated from `src/STD*.m` doc-comments by `make manifest`, `make skill`, and `make doctest`. CI's drift gates will reject any direct edit. -- **`dist/repo.meta.json` is the one file under `dist/` that is - hand-edited.** Bump `verified_on` to today's date whenever you - touch it; `make check-manifest` asserts that `dist/` (including - `repo.meta.json`) is committed and clean. +- **The root `repo.meta.json` is the hand-edited meta artifact** (the + rest of `dist/` is generated). Bump `verified_on` to today's date + whenever you touch it; `make check-manifest` asserts it is committed + and clean. - **Do not introduce GT.M support.** AGPL-3.0 YottaDB and IRIS only — GT.M is deliberately out of scope. - **Do not duplicate utilities into m-cli.** m-stdlib has architectural @@ -209,8 +211,9 @@ elsewhere — m-stdlib's top-level layout: | Path | Contents | |---|---| +| `repo.meta.json` (root) | Hand-edited repo meta (id/layer/language/verification_commands); `m arch check` reads it root-first | | `docs/` | Tracking (changelog, module tracker, discoveries, parallel tracks), per-module API reference (`modules/`), plans, guides, testing writeups | -| `dist/` | Phase 0 `repo.meta.json` + `stdlib-manifest.json` / `errors.json` / `skill/` (drift gates) | +| `dist/` | Generated drift-gated artifacts — `stdlib-manifest.json` / `errors.json` / `skill/` | | `examples/` | Demo M source (e.g. `stdargs-demo.m`) | | `templates/` | Project scaffolds (e.g. `m-vista-test-suite/`) | | `scripts/` | Shell helpers | diff --git a/Makefile b/Makefile index bf57e37..01375ec 100644 --- a/Makefile +++ b/Makefile @@ -155,16 +155,16 @@ manifest-check: manifest || { echo "ERROR: dist/errors.json out of date — run 'make manifest' and commit."; exit 1; } @echo "manifest: clean" -# `check-manifest` is the Phase 0 tier-1 drift gate. It composes the -# existing manifest-check (generated payloads) with an assertion that -# the hand-edited dist/repo.meta.json is tracked and clean. The org -# catalog generator fetches dist/repo.meta.json by raw URL — a missing -# or stale file would break Phase 0's smoke test in .github. +# `check-manifest` is the tier-1 drift gate. It composes the existing +# manifest-check (generated payloads) with an assertion that the hand-edited +# repo.meta.json is tracked and clean. The meta lives at the repo ROOT (the +# standardized "one meta artifact, one location"); `m arch check` reads it +# root-first and validates its shape (id/layer/language/verification_commands). check-manifest: manifest-check - @git ls-files --error-unmatch dist/repo.meta.json >/dev/null 2>&1 \ - || { echo "ERROR: dist/repo.meta.json is not tracked — 'git add dist/repo.meta.json' and commit."; exit 1; } - @git diff --exit-code -- dist/repo.meta.json \ - || { echo "ERROR: dist/repo.meta.json has uncommitted changes — review and commit."; exit 1; } + @git ls-files --error-unmatch repo.meta.json >/dev/null 2>&1 \ + || { echo "ERROR: repo.meta.json is not tracked — 'git add repo.meta.json' and commit."; exit 1; } + @git diff --exit-code -- repo.meta.json \ + || { echo "ERROR: repo.meta.json has uncommitted changes — review and commit."; exit 1; } @echo "check-manifest: clean" # `make frontmatter` re-syncs YAML frontmatter on every docs/modules/std*.md diff --git a/dist/errors.json b/dist/errors.json index f840b4a..908d167 100644 --- a/dist/errors.json +++ b/dist/errors.json @@ -319,5 +319,12 @@ "loadJson" ], "module": "STDSEED" + }, + "U-STDSEED-NO-FILER": { + "labels": [ + "load", + "loadJson" + ], + "module": "STDSEED" } } diff --git a/dist/skill/SKILL.md b/dist/skill/SKILL.md index f0ceea9..04ade59 100644 --- a/dist/skill/SKILL.md +++ b/dist/skill/SKILL.md @@ -17,7 +17,7 @@ module + label, the canonical-idiom library, and the full U-STD* error surface, all rendered for AI / agent context loading. **Catalogue:** 33 modules, 290 public labels, -43 error codes. +44 error codes. ## When to use this skill diff --git a/dist/skill/error-codes.md b/dist/skill/error-codes.md index 1a49f3a..e56c670 100644 --- a/dist/skill/error-codes.md +++ b/dist/skill/error-codes.md @@ -1,6 +1,6 @@ # m-stdlib — error codes -m-stdlib v0.5.0; 43 error codes across 14 modules. +m-stdlib v0.5.0; 44 error codes across 14 modules. Inverted index over the manifest's `@raises` arrays. Every `,U-STDxxx-NAME,` code an m-stdlib label sets via `set $ecode=` @@ -91,4 +91,5 @@ that needs to disambiguate sources, this is the lookup table. - **`U-STDSEED-INVALID-MANIFEST`** — raised by: `loadJson` - **`U-STDSEED-MISSING-FIELD`** — raised by: `load`, `validate` - **`U-STDSEED-MISSING-FILE`** — raised by: `load`, `validate`, `loadJson` +- **`U-STDSEED-NO-FILER`** — raised by: `load`, `loadJson` diff --git a/dist/skill/manifest-index.md b/dist/skill/manifest-index.md index 72e9e70..b9d492d 100644 --- a/dist/skill/manifest-index.md +++ b/dist/skill/manifest-index.md @@ -385,12 +385,12 @@ _raises: `U-STDREGEX-BAD-PATTERN`, `U-STDREGEX-NO-MATCH`, `U-STDREGEX-UNSUPPORTE declarative test data loader (v0.1.3). - `do clear^STDSEED(path)` — Drop bookkeeping for `path`. Idempotent. -- `do load^STDSEED(path, filer)` — Load manifest at `path` via `filer` (default FILE^DIE). -- `do loadJson^STDSEED(jsonText, filer)` — Load JSON-array manifest via `filer`. +- `do load^STDSEED(path, filer)` — Load manifest at `path` via the required `filer`. +- `do loadJson^STDSEED(jsonText, filer)` — Load JSON-array manifest via the required `filer`. - `$$loaded^STDSEED(path)` — Predicate — 1 iff `path` is currently loaded. - `$$validate^STDSEED(path)` — Parse-only check — return 1 on success; raise on syntax error. -_raises: `U-STDSEED-FILE-NOT-FOUND`, `U-STDSEED-FILER-ERROR`, `U-STDSEED-INVALID-JSON`, `U-STDSEED-INVALID-MANIFEST`, `U-STDSEED-MISSING-FIELD`, `U-STDSEED-MISSING-FILE`_ +_raises: `U-STDSEED-FILE-NOT-FOUND`, `U-STDSEED-FILER-ERROR`, `U-STDSEED-INVALID-JSON`, `U-STDSEED-INVALID-MANIFEST`, `U-STDSEED-MISSING-FIELD`, `U-STDSEED-MISSING-FILE`, `U-STDSEED-NO-FILER`_ ## `STDSEMVER` diff --git a/dist/stdlib-manifest.json b/dist/stdlib-manifest.json index 1cb764c..883878a 100644 --- a/dist/stdlib-manifest.json +++ b/dist/stdlib-manifest.json @@ -8920,8 +8920,9 @@ }, "STDSEED": { "synopsis": "m-stdlib — declarative test data loader (v0.1.3).", - "description": "Public API:\n load^STDSEED(path,filer) ; load TSV manifest via `filer`\n (default: fileViaDie -> FILE^DIE)\n $$loaded^STDSEED(path) ; 1 iff path is currently tracked\n clear^STDSEED(path) ; drop bookkeeping for path\n $$validate^STDSEED(path) ; 1 if manifest parses; raises on syntax\n loadJson^STDSEED(jsonText,filer) ; load JSON-array manifest via `filer`\n (each element: {\"file\":..,\"fields\":{..}})\n\nManifest format (TSV, one row per record):\n \\t=\\t=...\nLines beginning with '#' are comments. Whitespace-only lines skip.\n\nFiler hook: the optional `filer` argument is a \"tag^routine\"\nreference invoked once per row as\n do @filer@(file,.fda,.iens)\nwith fda(file,\"+1,\",field)=value and `iens` an output IEN. The\ndefault — when filer is empty — calls FILE^DIE; that path\nassumes FileMan is loaded in the runtime environment.\n\nState per loaded path lives under ^STDLIB($job,\"stdseed\",path,...).\nclear() drops it. STDSEED does NOT open its own transaction;\ncallers wanting rollback semantics wrap in STDFIX (v0.1.1+).\n\nErrors set $ECODE to one of:\n ,U-STDSEED-FILE-NOT-FOUND,\n ,U-STDSEED-MISSING-FILE,\n ,U-STDSEED-MISSING-FIELD,\n ,U-STDSEED-FILER-ERROR,\n ,U-STDSEED-INVALID-JSON,\n ,U-STDSEED-INVALID-MANIFEST,", + "description": "Public API:\n load^STDSEED(path,filer) ; load TSV manifest via `filer`\n $$loaded^STDSEED(path) ; 1 iff path is currently tracked\n clear^STDSEED(path) ; drop bookkeeping for path\n $$validate^STDSEED(path) ; 1 if manifest parses; raises on syntax\n loadJson^STDSEED(jsonText,filer) ; load JSON-array manifest via `filer`\n (each element: {\"file\":..,\"fields\":{..}})\n\nManifest format (TSV, one row per record):\n \\t=\\t=...\nLines beginning with '#' are comments. Whitespace-only lines skip.\n\nFiler hook: the `filer` argument is REQUIRED — a \"tag^routine\"\nreference invoked once per row as\n do @filer@(file,.fda,.iens)\nwith fda(file,\"+1,\",field)=value and `iens` an output IEN. STDSEED is\nengine-neutral (m layer, the m/v waterline) and ships no FileMan\ndefault; a missing filer raises U-STDSEED-NO-FILER. A VistA caller\ninjects a FileMan-backed filer from the v layer (v-stdlib).\n\nState per loaded path lives under ^STDLIB($job,\"stdseed\",path,...).\nclear() drops it. STDSEED does NOT open its own transaction;\ncallers wanting rollback semantics wrap in STDFIX (v0.1.1+).\n\nErrors set $ECODE to one of:\n ,U-STDSEED-NO-FILER,\n ,U-STDSEED-FILE-NOT-FOUND,\n ,U-STDSEED-MISSING-FILE,\n ,U-STDSEED-MISSING-FIELD,\n ,U-STDSEED-FILER-ERROR,\n ,U-STDSEED-INVALID-JSON,\n ,U-STDSEED-INVALID-MANIFEST,", "errors": [ + "U-STDSEED-NO-FILER", "U-STDSEED-FILE-NOT-FOUND", "U-STDSEED-MISSING-FILE", "U-STDSEED-MISSING-FIELD", @@ -8933,7 +8934,7 @@ "load": { "form": "procedure", "signature": "do load^STDSEED(path, filer)", - "synopsis": "Load manifest at `path` via `filer` (default FILE^DIE).", + "synopsis": "Load manifest at `path` via the required `filer`.", "params": [ { "name": "path", @@ -8943,11 +8944,15 @@ { "name": "filer", "type": "string", - "doc": "M call-site \"label^routine\" (default fileViaDie^STDSEED)" + "doc": "REQUIRED M call-site \"label^routine\" filer" } ], "returns": null, "raises": [ + { + "code": "U-STDSEED-NO-FILER", + "doc": "filer argument was empty" + }, { "code": "U-STDSEED-FILE-NOT-FOUND", "doc": "could not open `path`" @@ -8965,9 +8970,11 @@ "doc": "filer raised; propagated as a STDSEED code" } ], - "raised_in_body": [], + "raised_in_body": [ + "U-STDSEED-NO-FILER" + ], "examples": [ - "do load^STDSEED(\"/etc/seed.tsv\")" + "do load^STDSEED(\"/etc/seed.tsv\",\"myFiler^MYAPP\")" ], "since": "v0.1.3", "stable": "stable", @@ -8977,10 +8984,10 @@ "do clear^STDSEED" ], "deprecated": "", - "description": "Each parsed row is dispatched once.", + "description": "Each parsed row is dispatched once. `filer` is required —\nSTDSEED is engine-neutral and ships no FileMan default.", "source": { "file": "src/STDSEED.m", - "line": 39 + "line": 40 } }, "loaded": { @@ -9013,7 +9020,7 @@ "description": "", "source": { "file": "src/STDSEED.m", - "line": 57 + "line": 60 } }, "clear": { @@ -9043,7 +9050,7 @@ "description": "Best-effort — does not delete already-filed records (caller's\nresponsibility, typically via STDFIX rollback).", "source": { "file": "src/STDSEED.m", - "line": 66 + "line": 69 } }, "validate": { @@ -9088,13 +9095,13 @@ "description": "Never invokes the filer. Use as a pre-flight before load().", "source": { "file": "src/STDSEED.m", - "line": 77 + "line": 80 } }, "loadJson": { "form": "procedure", "signature": "do loadJson^STDSEED(jsonText, filer)", - "synopsis": "Load JSON-array manifest via `filer`.", + "synopsis": "Load JSON-array manifest via the required `filer`.", "params": [ { "name": "jsonText", @@ -9104,11 +9111,15 @@ { "name": "filer", "type": "string", - "doc": "M call-site \"label^routine\" (default fileViaDie^STDSEED)" + "doc": "REQUIRED M call-site \"label^routine\" filer" } ], "returns": null, "raises": [ + { + "code": "U-STDSEED-NO-FILER", + "doc": "filer argument was empty" + }, { "code": "U-STDSEED-INVALID-JSON", "doc": "jsonText does not parse as JSON" @@ -9127,11 +9138,12 @@ } ], "raised_in_body": [ + "U-STDSEED-NO-FILER", "U-STDSEED-INVALID-JSON", "U-STDSEED-INVALID-MANIFEST" ], "examples": [ - "do loadJson^STDSEED(\"[{\"\"file\"\":\"\"PATIENT\"\",\"\"fields\"\":{\"\".01\"\":\"\"Smith\"\"}}]\")" + "do loadJson^STDSEED(\"[{\"\"file\"\":\"\"PATIENT\"\",\"\"fields\"\":{\"\".01\"\":\"\"Smith\"\"}}]\",\"myFiler^MYAPP\")" ], "since": "v0.2.0", "stable": "stable", @@ -9143,7 +9155,7 @@ "description": "", "source": { "file": "src/STDSEED.m", - "line": 91 + "line": 94 } } }, @@ -11631,6 +11643,13 @@ "groups" ] }, + "U-STDSEED-NO-FILER": { + "module": "STDSEED", + "labels": [ + "load", + "loadJson" + ] + }, "U-STDSEED-FILE-NOT-FOUND": { "module": "STDSEED", "labels": [ diff --git a/docs/memory/MEMORY.md b/docs/memory/MEMORY.md index 14881bc..0a56975 100644 --- a/docs/memory/MEMORY.md +++ b/docs/memory/MEMORY.md @@ -4,6 +4,8 @@ One line per memory file. Content lives in the files, not here. - [iris-native-backends](iris-native-backends.md) — PR #1: the 3 optional modules' IRIS dispatch arm uses the **inlined `$zversion["IRIS"` probe** (not a public engine helper — that part of the PR was dropped as superseded); dual-engine local-test runbook (**YDB needs `--chset m`**, rebuild `/tmp/m` for the flag); `m-test-iris` embedded-Python is non-functional so STDCOMPRESS-IRIS is unverifiable locally; how the stale PR was landed without merge/rebase/force-push (forward-commit-to-master-tree). - [dual-engine-validation-2026-06-14](dual-engine-validation-2026-06-14.md) — 2026-06-14 full sweep: YDB 49/49 (2585 assertions, 0 fail), IRIS 45/49 (crypto cluster hangs/aborts — B2 IRIS-backends PR unmerged on this branch). Durable IRIS-runner gotchas: no per-suite timeout; killing a hung iris docker-exec corrupts staging → false 0/0 until `docker restart`; stdout reports `ok:true` even on a failed run (trust shell exit / stderr). Coverage gate is aggregate, not per-module. +- [stdseed-g2-engine-neutral](stdseed-g2-engine-neutral.md) — STDSEED made strictly engine-neutral for **G2** (Option B, 2026-06-14): FileMan default filer `fileViaDie` removed, `filer` now required (`U-STDSEED-NO-FILER`); G2 stays a pure deny-list (no exception pragma). Owed: re-home `fileViaDie^VSLSEED` in v-stdlib. Verified 37/37 both engines. +- [meta-at-root](meta-at-root.md) — m-stdlib's repo meta moved `dist/repo.meta.json` → **root `repo.meta.json`** (Phase B item 1, 2026-06-15); `make check-manifest` checks root; the stale "catalog generator" comment removed (no such generator exists). Rides the `stdseed-engine-neutral-g2` branch. - [waterline-g1-gate](waterline-g1-gate.md) — the m/v waterline **G1 gate** (`m arch check` in m-cli) — dependency-direction (v→m only); how `layer` is declared (dist/ meta vs root `repo.meta.json` for m-cli's gitignored dist/), check-manifest doesn't schema-validate the meta, and the v-cli registry-regen `go mod tidy` dep. Built s12 (loose end C). - [t0b2-msl-kids-base](t0b2-msl-kids-base.md) — VSL T0b.2 (MSL KIDS-install-as-green): **YDB leg GREEN — 15/15 test-in-place** after the m-ydb gbldir (`e5dcf85`) + v-pkg streamed-install (`aa1991f`) fixes. **IRIS leg (s9):** `raises^STDASSERT` **now ported to IRIS** (try/catch `irisRaises` branch; YDB byte-identical) → STDFMT/STDARGS clean + STDASSERTTST 40/40 both engines + STDUUID P2 gone; remaining IRIS crashes are **non-raises**. **(s10):** file I/O made dual-engine — STDFS portable facade (`$$openRead/Write/Append`+`readLn`) + STDOS.env IRIS arm + 5 consumers migrated; **STDFSTST 50/50 both engines, YDB full 2098/0**. **But the consumer SUITES still don't go green on IRIS** — separate non-file blockers (STDJSON **byte-mode** parser, STDCSV **`@cb@` indirection**, **wide-char** descriptions). file-I/O ≠ green suites; see §s10. Full 15/15 needs byte-mode + callback-idiom + wide-char work (out of file-I/O scope). ≤8-char-name decision keeps STDASSERT/STDSEMVER as a rename follow-up. - [vsl-doc-gaps-v0.2](vsl-doc-gaps-v0.2.md) — how the VistA Standard Library architecture doc's §12 VDL gaps resolved at v0.2; the vdocs `XU:XU:UG` over-collapse defect that blocks gold-promotion of the Kernel feature guides. diff --git a/docs/memory/meta-at-root.md b/docs/memory/meta-at-root.md new file mode 100644 index 0000000..1237705 --- /dev/null +++ b/docs/memory/meta-at-root.md @@ -0,0 +1,23 @@ +--- +name: meta-at-root +description: m-stdlib's repo meta moved from dist/repo.meta.json to root repo.meta.json (Phase B item 1) +metadata: + type: project +--- + +The hand-edited repo meta moved **`dist/repo.meta.json` → root `repo.meta.json`** +(Phase B item 1, 2026-06-15) — the standardized "one meta artifact, one +location", now that `m arch check` reads the meta **root-first** and validates +its shape (id/layer/language/verification_commands; `Gate:"META"`). + +- `make check-manifest` now asserts the **root** `repo.meta.json` is tracked + + clean (Makefile updated); the rest of `dist/` stays generated/drift-gated. +- The "org catalog generator fetches `dist/repo.meta.json` by raw URL" note in + the old Makefile comment was **stale** — no catalog generator exists in the + `.github` repo (only `go-ci.yml` + `arch-waterline.yml`); the future scheduled + **meta-gate** reads root metas. Comment removed. +- Landed on the `stdseed-engine-neutral-g2` branch (same m-stdlib Phase B PR as + the STDSEED G2 fix — keeps the branch fully waterline-clean). `verified_on` + stays today. Verified: `m arch check` 0 violations; `make check-manifest` clean. + +Supersedes the "dist/ meta" half of [[waterline-g1-gate]]. See [[stdseed-g2-engine-neutral]]. diff --git a/docs/memory/stdseed-g2-engine-neutral.md b/docs/memory/stdseed-g2-engine-neutral.md new file mode 100644 index 0000000..a7caa37 --- /dev/null +++ b/docs/memory/stdseed-g2-engine-neutral.md @@ -0,0 +1,47 @@ +--- +name: stdseed-g2-engine-neutral +description: STDSEED made strictly engine-neutral for the m/v waterline G2 gate — the FileMan default filer removed; filer now required +metadata: + type: project +--- + +The m/v waterline **G2** gate (no VistA symbols below the line) had exactly +one real below-waterline reference across m-stdlib: STDSEED's bundled default +filer `fileViaDie` → `do FILE^DIE` (FileMan). Everything else STDSEED does is +already engine-neutral — the filer is injected by string (`xecute "do "_filer_"(...)"`), +so the loader core never names FileMan. + +**Decision (2026-06-14, user, Option B):** remove the FileMan default from the +`m` layer rather than allow it as an annotated G2 exception — so **G2 stays a +pure deny-list with no escape-hatch/pragma mechanism** (avoids the first +documented below-waterline exception becoming precedent). Rationale was +strengthened by a caller audit: **no real caller used the no-filer default** — +every STDSEEDTST test injects a stub filer; the only no-filer usage was a quoted +example inside the `m-vista-test-suite` template (itself a v-flavored artifact). + +**What changed in m-stdlib (branch `stdseed-engine-neutral-g2`, off master):** +- `src/STDSEED.m`: deleted `fileViaDie`; `load`/`loadJson` now raise the new + `,U-STDSEED-NO-FILER,` when `filer=""` (checked *first*, before file read / + JSON parse). Header + `@param`/`@raises`/`@example` doc updated; the orphan + `U-STDSEED-FILER-DIE-ERROR` code is gone. +- `tests/STDSEEDTST.m`: added `tLoadWithoutFilerRaises` + `tLoadJsonWithoutFilerRaises` + (TDD-red first). **Gotcha:** three pre-existing loadJson error-path tests + (`tLoadJsonInvalidJsonRaises` / `NonArrayRoot` / `MissingFileKey`) had relied + on the default to reach the parse logic — they now must inject a filer + (`capture^STDSEEDTST`) or they red on NO-FILER before validating. FileMan + mentions scrubbed from the test comments too (keep the m-layer `.m` clean). +- dist/ regenerated (manifest/errors/skill — `U-STDSEED-NO-FILER` registered); + `docs/modules/stdseed.md` History + `docs/tracking/module-tracker.md` row updated. + +**Verified:** STDSEEDTST 37/37 on **both** engines (YDB m-test-engine + IRIS +m-test-iris); full core suite 49 suites / 2098 / 0 on YDB; lint 0 errors; fmt +clean; STDSEED coverage 98.8% (up — dead untested filer gone); generators idempotent. + +**Owed (v-stdlib, next increment):** re-home the FileMan filer as +**`fileViaDie^VSLSEED`** (layer v) — the module doc + this design already commit +to that name. **Do NOT name `^VSL*` in any m-stdlib `.m` comment** — the G1 +M-ref scan (`\^VSL...`) is comment-unaware and would flag m-stdlib; the m-layer +example uses the neutral placeholder `myFiler^MYAPP`. + +Part of Phase B (standardize the substrate) — the prerequisite that lets G2 be +wired clean. See [[waterline-g1-gate]] and [[t0b2-msl-kids-base]]. diff --git a/docs/modules/stdseed.md b/docs/modules/stdseed.md index 71f39bb..d1d54bb 100644 --- a/docs/modules/stdseed.md +++ b/docs/modules/stdseed.md @@ -6,7 +6,7 @@ stable: stable since: v0.1.3 synopsis: 'declarative test data loader (v0.1.3)' labels: ['clear', 'load', 'loadJson', 'loaded', 'validate'] -errors: ['U-STDSEED-FILE-NOT-FOUND', 'U-STDSEED-FILER-ERROR', 'U-STDSEED-INVALID-JSON', 'U-STDSEED-INVALID-MANIFEST', 'U-STDSEED-MISSING-FIELD', 'U-STDSEED-MISSING-FILE'] +errors: ['U-STDSEED-FILE-NOT-FOUND', 'U-STDSEED-FILER-ERROR', 'U-STDSEED-INVALID-JSON', 'U-STDSEED-INVALID-MANIFEST', 'U-STDSEED-MISSING-FIELD', 'U-STDSEED-MISSING-FILE', 'U-STDSEED-NO-FILER'] conformance: [] see_also: ['STDJSON'] created: 2026-05-05 @@ -19,9 +19,12 @@ doc_type: [REFERENCE] Loads a TSV manifest of FileMan records into the runtime environment so a test can run against a known fixture set. Each row is dispatched -to a *filer* — by default `FILE^DIE` (FileMan's filing API), but any -`tag^routine` reference will do, which keeps the parser testable -outside a VistA host. +to a *filer* — a caller-supplied `tag^routine` reference. STDSEED is +**engine-neutral** (the `m` layer of the m/v waterline): it ships **no +FileMan default**, so the `filer` argument is **required**. A VistA +caller injects a FileMan-backed filer (wrapping `FILE^DIE`) from the +`v` layer (`v-stdlib`); a unit test injects a stub filer. This keeps +the parser testable on a bare M engine with no VistA host. ## Public API @@ -31,12 +34,12 @@ outside a VistA host. | `loaded` | `$$loaded^STDSEED(path)` | `1` iff `path` is currently tracked. | | `clear` | `do clear^STDSEED(path)` | — (drops `STDSEED`'s bookkeeping for `path`; idempotent). | | `validate` | `$$validate^STDSEED(path)` | `1` if every row parses cleanly; raises `$ECODE` on syntax error. | -| `loadJson` | `do loadJson^STDSEED(jsonText)` | — (stub; raises `,U-STDSEED-NOT-IMPLEMENTED,` until STDJSON ships). | +| `loadJson` | `do loadJson^STDSEED(jsonText,filer)` | — (mutates the database via `filer`; parses a JSON-array manifest). | -The `filer` argument is optional. When empty, the default -`fileViaDie^STDSEED` is used; that label calls `FILE^DIE` and -reports any `^TMP("DIERR",$J)` error back through `$ECODE`. Any -custom filer is invoked once per row as +The `filer` argument is **required** — STDSEED ships no FileMan +default (the m/v waterline keeps `FILE^DIE` out of the `m` layer). An +empty `filer` raises `,U-STDSEED-NO-FILER,`. The filer is invoked once +per row as ```m do @filer@(file,.fda,.iens) @@ -65,8 +68,8 @@ records the returned IEN under `^STDLIB($job,"stdseed",path,...)` so ## Examples ```m -; happy path — load against real FileMan -do load^STDSEED("/data/seed/widgets.tsv") +; against real FileMan — inject a v-layer FileMan filer (v-stdlib) +do load^STDSEED("/data/seed/widgets.tsv","fileViaDie^VSLSEED") write $$loaded^STDSEED("/data/seed/widgets.tsv"),! ; 1 do clear^STDSEED("/data/seed/widgets.tsv") ; drops bookkeeping @@ -87,16 +90,13 @@ capture(file,fda,iens) quit ``` -The default filer (`fileViaDie^STDSEED`) wraps `FILE^DIE`. The -suite uses stub filers exclusively, so `fileViaDie` ships with the -real-FileMan path uncovered (10/11 = 90.9% per-module coverage). -Real-environment validation requires a FileMan-bearing YDB endpoint -(e.g. vista-meta with the dataset loaded) and an STDFIX-wrapped -test that exercises FILE^DIE inside a rollback boundary; the -integration test is queued behind the v0.1.4 cycle. The label -itself compiles and is observably correct against any FileMan -host (manual smoke runs against vista-meta succeed); the gap is -test-coverage, not implementation. (Tracker T8.) +A FileMan-backed filer is **not** part of STDSEED — it lives in the +`v` layer (`v-stdlib`), wrapping `FILE^DIE`, and is injected by the +VistA caller. This is the m/v waterline G2 decision (2026-06-14): the +engine-neutral `m` layer carries no VistA symbol. Real-environment +validation (filing inside an STDFIX rollback boundary against a +FileMan-bearing endpoint) is therefore a `v-stdlib` concern; STDSEED's +own suite injects stub filers exclusively and is fully covered. ## Transactions @@ -112,12 +112,13 @@ manual rollback. | `$ECODE` | When | |---|---| +| `,U-STDSEED-NO-FILER,` | `load()` / `loadJson()` called with an empty `filer` (it is required — no FileMan default) | | `,U-STDSEED-FILE-NOT-FOUND,` | `path` cannot be opened readonly | -| `,U-STDSEED-MISSING-FILE,` | A row's first column is empty | +| `,U-STDSEED-MISSING-FILE,` | A row's first column (TSV) or a JSON element's `file` is empty | | `,U-STDSEED-MISSING-FIELD,` | A `field=value` pair has no `=` | | `,U-STDSEED-FILER-ERROR,` | The filer set `$ECODE`; STDSEED relays the failure | -| `,U-STDSEED-FILER-DIE-ERROR,` | The default filer saw `^TMP("DIERR",$J)` populated | -| `,U-STDSEED-NOT-IMPLEMENTED,` | `loadJson()` is called before STDJSON ships | +| `,U-STDSEED-INVALID-JSON,` | `loadJson()` text does not parse as JSON | +| `,U-STDSEED-INVALID-MANIFEST,` | `loadJson()` parsed JSON is not an array of `{"file":..}` objects | ## See also @@ -131,8 +132,14 @@ manual rollback. The original `v0.1.3` release shipped TSV + a `loadJson` stub raising `U-STDSEED-NOT-IMPLEMENTED`. The real `loadJson` implementation (driven by `$$parse^STDJSON`) landed at `v0.2.0` once STDJSON was available. -`FILE^DIE` is a runtime-only dep — needed at use-site for the default -filer, not at compile-time. + +Through `v0.2.x` STDSEED bundled a `fileViaDie` default filer that +called `FILE^DIE` when no filer was passed. That lone FileMan reference +was removed (2026-06-14) to make STDSEED strictly engine-neutral for the +**m/v waterline G2** (no-VistA-symbols in the `m` layer): the `filer` +argument is now required (`U-STDSEED-NO-FILER` if absent) and the +FileMan-backed filer (`fileViaDie^VSLSEED`) lives in the `v` layer +(v-stdlib), injected by the caller. The six `loadJson` raises-path tests (`U-STDSEED-LOAD-FAIL`, `U-STDSEED-NOT-IMPLEMENTED`, `U-STDSEED-INVALID-JSON`, etc.) were @@ -142,5 +149,6 @@ re-enabled together at `e637425` once `raises^STDASSERT` gained unwind. The actual hanging test was `tLoadFilerErrorPropagatesEcode` (test #12, mis-identified as #14 because of a TAP plan miscount); root cause was `$ETRAP` firing deep in `walk^STDSEED` while the OPENed TSV -fixture was the current device. Post-fix STDSEEDTST runs 35/35 green -on engine. +fixture was the current device. Post-fix STDSEEDTST runs 37/37 green +on both engines (the two `U-STDSEED-NO-FILER` raises-tests were added +with the G2 filer-required change; dual-engine verified YDB + IRIS). diff --git a/docs/tracking/module-tracker.md b/docs/tracking/module-tracker.md index 669c665..1d29c76 100644 --- a/docs/tracking/module-tracker.md +++ b/docs/tracking/module-tracker.md @@ -96,7 +96,7 @@ current state. | [x] | P1 | L7 | 9 | [`STDARGS`](../modules/stdargs.md) | `v0.1.0` | 4d | none (completed) | none | argparse (long/short/group/positional/`--`) | n/a | | [x] | P1b | L8 | 10 | [`STDFIX`](../modules/stdfix.md) | `v0.2.0` | 3d | none (completed) | none | Per-test transactional isolation | ✅ C3 | | [x] | P1b | L9 | 11 | [`STDMOCK`](../modules/stdmock.md) | `v0.2.0` | 3d | none (completed) | none | Test-time call interception | ✅ C4 | -| [x] | P1b | L10 | 12 | [`STDSEED`](../modules/stdseed.md) | `v0.2.0` | 3d | none (completed) | STDJSON; runtime-only `FILE^DIE` | TSV/JSON fixture loader + pluggable filer | ✅ C5 | +| [x] | P1b | L10 | 12 | [`STDSEED`](../modules/stdseed.md) | `v0.2.0` | 3d | none (completed) | STDJSON | TSV/JSON fixture loader; **required** caller-injected filer (engine-neutral — FileMan default removed to v-stdlib, m/v waterline G2, 2026-06-14) | ✅ C5 | | [x] | P2 | L11 | 13 | [`STDJSON`](../modules/stdjson.md) | `v0.2.0` | 7d | none (completed) | none | RFC 8259 JSON parser + serialiser | n/a | | [x] | P2 | L12 | 14 | [`STDREGEX`](../modules/stdregex.md) | `v0.2.0` | 10d | none (options) | none | Thompson-NFA regex (no back-refs / lookaround) | n/a | | [x] | P2 | L13 | 15 | [`STDCOLL`](../modules/stdcoll.md) | `v0.2.0` | 5d | none (completed) | none | Set/Map/Stack/Queue/Deque/Heap/OrderedDict | n/a | diff --git a/dist/repo.meta.json b/repo.meta.json similarity index 100% rename from dist/repo.meta.json rename to repo.meta.json diff --git a/src/STDSEED.m b/src/STDSEED.m index 4553417..e795148 100644 --- a/src/STDSEED.m +++ b/src/STDSEED.m @@ -2,7 +2,6 @@ ; ; Public API: ; load^STDSEED(path,filer) ; load TSV manifest via `filer` - ; (default: fileViaDie -> FILE^DIE) ; $$loaded^STDSEED(path) ; 1 iff path is currently tracked ; clear^STDSEED(path) ; drop bookkeeping for path ; $$validate^STDSEED(path) ; 1 if manifest parses; raises on syntax @@ -13,18 +12,20 @@ ; \t=\t=... ; Lines beginning with '#' are comments. Whitespace-only lines skip. ; - ; Filer hook: the optional `filer` argument is a "tag^routine" + ; Filer hook: the `filer` argument is REQUIRED — a "tag^routine" ; reference invoked once per row as ; do @filer@(file,.fda,.iens) - ; with fda(file,"+1,",field)=value and `iens` an output IEN. The - ; default — when filer is empty — calls FILE^DIE; that path - ; assumes FileMan is loaded in the runtime environment. + ; with fda(file,"+1,",field)=value and `iens` an output IEN. STDSEED is + ; engine-neutral (m layer, the m/v waterline) and ships no FileMan + ; default; a missing filer raises U-STDSEED-NO-FILER. A VistA caller + ; injects a FileMan-backed filer from the v layer (v-stdlib). ; ; State per loaded path lives under ^STDLIB($job,"stdseed",path,...). ; clear() drops it. STDSEED does NOT open its own transaction; ; callers wanting rollback semantics wrap in STDFIX (v0.1.1+). ; ; Errors set $ECODE to one of: + ; ,U-STDSEED-NO-FILER, ; ,U-STDSEED-FILE-NOT-FOUND, ; ,U-STDSEED-MISSING-FILE, ; ,U-STDSEED-MISSING-FIELD, @@ -36,21 +37,23 @@ ; ; ---------- public API ---------- ; -load(path,filer) ; Load manifest at `path` via `filer` (default FILE^DIE). +load(path,filer) ; Load manifest at `path` via the required `filer`. ; doc: @param path string filesystem path to a TSV manifest - ; doc: @param filer string M call-site "label^routine" (default fileViaDie^STDSEED) + ; doc: @param filer string REQUIRED M call-site "label^routine" filer + ; doc: @raises U-STDSEED-NO-FILER filer argument was empty ; doc: @raises U-STDSEED-FILE-NOT-FOUND could not open `path` ; doc: @raises U-STDSEED-MISSING-FILE a row's first TSV column is empty ; doc: @raises U-STDSEED-MISSING-FIELD a non-empty TSV piece does not contain "=" ; doc: @raises U-STDSEED-FILER-ERROR filer raised; propagated as a STDSEED code - ; doc: @example do load^STDSEED("/etc/seed.tsv") + ; doc: @example do load^STDSEED("/etc/seed.tsv","myFiler^MYAPP") ; doc: @since v0.1.3 ; doc: @stable stable ; doc: @see $$validate^STDSEED, do loadJson^STDSEED, do clear^STDSEED - ; doc: Each parsed row is dispatched once. + ; doc: Each parsed row is dispatched once. `filer` is required — + ; doc: STDSEED is engine-neutral and ships no FileMan default. new f set f=$get(filer) - if f="" set f="fileViaDie^STDSEED" + if f="" set $ecode=",U-STDSEED-NO-FILER," quit do walk(path,f,1) quit ; @@ -88,20 +91,21 @@ do walk(path,"",0) quit 1 ; -loadJson(jsonText,filer) ; Load JSON-array manifest via `filer`. +loadJson(jsonText,filer) ; Load JSON-array manifest via the required `filer`. ; doc: @param jsonText string JSON array; each element {"file":..,"fields":{..}} - ; doc: @param filer string M call-site "label^routine" (default fileViaDie^STDSEED) + ; doc: @param filer string REQUIRED M call-site "label^routine" filer + ; doc: @raises U-STDSEED-NO-FILER filer argument was empty ; doc: @raises U-STDSEED-INVALID-JSON jsonText does not parse as JSON ; doc: @raises U-STDSEED-INVALID-MANIFEST parsed JSON is not an array of objects with "file" ; doc: @raises U-STDSEED-MISSING-FILE element lacks a "file" string member ; doc: @raises U-STDSEED-FILER-ERROR filer raised; propagated as a STDSEED code - ; doc: @example do loadJson^STDSEED("[{""file"":""PATIENT"",""fields"":{"".01"":""Smith""}}]") + ; doc: @example do loadJson^STDSEED("[{""file"":""PATIENT"",""fields"":{"".01"":""Smith""}}]","myFiler^MYAPP") ; doc: @since v0.2.0 ; doc: @stable stable ; doc: @see do load^STDSEED, $$parse^STDJSON new tree,ok,f set f=$get(filer) - if f="" set f="fileViaDie^STDSEED" + if f="" set $ecode=",U-STDSEED-NO-FILER," quit set ok=$$parse^STDJSON(jsonText,.tree) if 'ok set $ecode=",U-STDSEED-INVALID-JSON," quit if $extract(tree)'="a" set $ecode=",U-STDSEED-INVALID-MANIFEST," quit @@ -200,27 +204,6 @@ if $ecode'="" set $ecode=",U-STDSEED-FILER-ERROR," quit quit ; -fileViaDie(file,fda,iens) ; Default filer — call FILE^DIE. - ; doc: @internal - ; doc: Default filer. Assumes FileMan available. After FILE^DIE, - ; doc: ^TMP("DIERR",$JOB) is checked; on error sets $ECODE which - ; doc: dispatch() relays as U-STDSEED-FILER-ERROR. Real-environment - ; doc: integration is the v0.1.4 follow-on; this label compiles and - ; doc: runs against any FileMan host but is not unit-tested. - new ien,sub,fld - kill ^TMP("STDSEED",$job,"FDA"),^TMP("STDSEED",$job,"IEN"),^TMP("DIERR",$job) - set sub="" - for set sub=$order(fda(file,sub)) quit:sub="" do - . set fld="" - . for set fld=$order(fda(file,sub,fld)) quit:fld="" do - . . set ^TMP("STDSEED",$job,"FDA",file,sub,fld)=fda(file,sub,fld) - ; m-lint: disable-next-line=M-MOD-036 - do FILE^DIE("","^TMP(""STDSEED"","_$job_",""FDA"")","^TMP(""STDSEED"","_$job_",""IEN"")") - if $data(^TMP("DIERR",$job)) set $ecode=",U-STDSEED-FILER-DIE-ERROR," quit - set iens=$get(^TMP("STDSEED",$job,"IEN",1)) - kill ^TMP("STDSEED",$job) - quit - ; ; ---------- internal: helpers ---------- ; trim(s) ; Strip leading and trailing ASCII whitespace (space, tab, CR, LF). diff --git a/tests/STDSEEDTST.m b/tests/STDSEEDTST.m index 0f3ff38..9273707 100644 --- a/tests/STDSEEDTST.m +++ b/tests/STDSEEDTST.m @@ -1,7 +1,7 @@ STDSEEDTST ; Test suite for STDSEED (v0.1.3). ; m-lint: disable-file=M-MOD-020 - ; Tests use a stub filer (capture^STDSEEDTST) so the suite does not - ; depend on FileMan being installed in the runtime environment. + ; Tests inject a stub filer (capture^STDSEEDTST); STDSEED is + ; engine-neutral and ships no default filer (the m/v waterline). new pass,fail do start^STDASSERT(.pass,.fail) ; @@ -54,6 +54,8 @@ ; ; ---- error paths ---- do tLoadOfMissingPathRaises(.pass,.fail) + do tLoadWithoutFilerRaises(.pass,.fail) + do tLoadJsonWithoutFilerRaises(.pass,.fail) ; do report^STDASSERT(pass,fail) quit @@ -81,7 +83,7 @@ set iens=ien quit ; -failingFiler(file,fda,iens) ; Stub filer that simulates a FILE^DIE failure. +failingFiler(file,fda,iens) ; Stub filer that simulates a filer failure. ; doc: Sets $ECODE to the contractual error code; STDSEED relays. set $ecode=",U-STDSEED-FILER-ERROR," quit @@ -358,15 +360,15 @@ quit ; tLoadJsonInvalidJsonRaises(pass,fail) ;@TEST "loadJson() raises U-STDSEED-INVALID-JSON on malformed JSON" - do raises^STDASSERT(.pass,.fail,"do loadJson^STDSEED(""{not-json"")","U-STDSEED-INVALID-JSON","malformed JSON rejected") + do raises^STDASSERT(.pass,.fail,"do loadJson^STDSEED(""{not-json"",""capture^STDSEEDTST"")","U-STDSEED-INVALID-JSON","malformed JSON rejected") quit ; tLoadJsonNonArrayRootRaises(pass,fail) ;@TEST "loadJson() raises U-STDSEED-INVALID-MANIFEST on non-array root" - do raises^STDASSERT(.pass,.fail,"do loadJson^STDSEED(""{}"")","U-STDSEED-INVALID-MANIFEST","object root rejected") + do raises^STDASSERT(.pass,.fail,"do loadJson^STDSEED(""{}"",""capture^STDSEEDTST"")","U-STDSEED-INVALID-MANIFEST","object root rejected") quit ; tLoadJsonMissingFileKeyRaises(pass,fail) ;@TEST "loadJson() raises U-STDSEED-MISSING-FILE on entry without 'file' key" - do raises^STDASSERT(.pass,.fail,"do loadJson^STDSEED(""[{}]"")","U-STDSEED-MISSING-FILE","missing file key rejected") + do raises^STDASSERT(.pass,.fail,"do loadJson^STDSEED(""[{}]"",""capture^STDSEEDTST"")","U-STDSEED-MISSING-FILE","missing file key rejected") quit ; ; ---------- error paths ---------- @@ -377,6 +379,17 @@ do raises^STDASSERT(.pass,.fail,"do load^STDSEED("""_path_""",""capture^STDSEEDTST"")","U-STDSEED-FILE-NOT-FOUND","missing path rejected") quit ; +tLoadWithoutFilerRaises(pass,fail) ;@TEST "load() without a filer raises U-STDSEED-NO-FILER (no FileMan default below the waterline)" + ; The filer is required: STDSEED is engine-neutral (layer m) and ships no + ; FileMan default. The check fires before the file read, so no fixture is + ; needed — proving load() short-circuits on an absent filer. + do raises^STDASSERT(.pass,.fail,"do load^STDSEED(""/tmp/stdseed-nofiler.tsv"")","U-STDSEED-NO-FILER","load with empty filer rejected") + quit + ; +tLoadJsonWithoutFilerRaises(pass,fail) ;@TEST "loadJson() without a filer raises U-STDSEED-NO-FILER" + do raises^STDASSERT(.pass,.fail,"do loadJson^STDSEED(""[]"")","U-STDSEED-NO-FILER","loadJson with empty filer rejected") + quit + ; ; ---------- helpers used by tests ---------- ; writeFixture(path,l1,l2,l3,l4,l5,l6,l7,l8) ; Write up to 8 lines + LF to path.