diff --git a/dist/icr-registry.json b/dist/icr-registry.json index ec13d68..19e6a3c 100644 --- a/dist/icr-registry.json +++ b/dist/icr-registry.json @@ -21,6 +21,48 @@ "status": "Supported" } ], + "VSLFS": [ + { + "call": "UPDATE^DIE", + "custodian": "DI", + "icr": "DBS", + "source": { + "anchor": "updatedie-updater", + "doc_key": "DI/fm22_2dg" + }, + "status": "Supported" + }, + { + "call": "$$GET1^DIQ", + "custodian": "DI", + "icr": "DBS", + "source": { + "anchor": "get1diq-data-retriever-single-field", + "doc_key": "DI/fm22_2dg" + }, + "status": "Supported" + }, + { + "call": "$$GET1^DIQ", + "custodian": "DI", + "icr": "DBS", + "source": { + "anchor": "get1diq-data-retriever-single-field", + "doc_key": "DI/fm22_2dg" + }, + "status": "Supported" + }, + { + "call": "FILE^DIE", + "custodian": "DI", + "icr": "DBS", + "source": { + "anchor": "filedie-filer", + "doc_key": "DI/fm22_2dg" + }, + "status": "Supported" + } + ], "VSLIO": [ { "call": "CALL^%ZISTCP", diff --git a/dist/msl-seam-pin.json b/dist/msl-seam-pin.json index 30fa378..c139700 100644 --- a/dist/msl-seam-pin.json +++ b/dist/msl-seam-pin.json @@ -1,5 +1,5 @@ { - "msl_ref": "v0.8.0", + "msl_ref": "v0.9.0", "seams": { "STDENV": { "contract_version": 1, @@ -31,6 +31,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/namespace-registry.json b/dist/namespace-registry.json index bf658ec..562b0c6 100644 --- a/dist/namespace-registry.json +++ b/dist/namespace-registry.json @@ -11,6 +11,7 @@ "globals": [], "routines": [ "VSLCFG", + "VSLFS", "VSLIO" ] } diff --git a/docs/memory/MEMORY.md b/docs/memory/MEMORY.md index 2a096da..2748abe 100644 --- a/docs/memory/MEMORY.md +++ b/docs/memory/MEMORY.md @@ -2,6 +2,7 @@ One line per memory file. Content lives in the files, not here. +- [m3-vslfs](m3-vslfs.md) — VSL/MSL **M3 Lane B DONE** (2026-06-16): **VSLFS** binds the `STDKV` storage seam (MSL **v0.9.0**) to VistA's FileMan DBS — `$$set`=`UPDATE^DIE` (returns resolved IENS; `"+1,"` adds), `$$get`/`$$exists`=`$$GET1^DIQ`, `$$kill`=`FILE^DIE` with FDA **`.01="@"`** (**no `DELETE^DIE` exists**; `^DIK`/direct KILL forbidden). Re-pinned `msl_ref` v0.8.0→v0.9.0. **Dual-engine GREEN 7/7** (vehu + foia-t12) over **#8989.51** (free-text `.01`, no other required fields → safe ZZ throwaway record; no DD install). DIERR→**`,U-VSL-FS-DIERR,`** `$ECODE` + `$$lastError`; MSG_ROOT="ERR" keeps errors private. **No `$ZVERSION` arm** (FileMan DBS portable). ICR **notional** (`@icr DBS` marker — gen-icr.py taught NOTIONAL_MARKERS; see [[notional-dbia-not-a-blocker]]). 3 boundaries green; suite **22/22** no regression. Branch `m3-vslfs` **stacked on `m2-vslio`** (unmerged). Next: M4 (VSLSEC+VSLLOG). - [t1.2-vslcfg](t1.2-vslcfg.md) — VSL T1.2 (2026-06-16): **VSLCFG**, the first VSL* module — binds the STDENV config seam to XPAR at the **SYS entity** (`$$get`=`$$GET^XPAR("SYS",…)`, `$$set`=`EN^XPAR("SYS",…)`), validated live on vehu. **All 3 determinism boundaries GREEN** (① re-pin `msl_ref`→v0.7.0 carrying real STDENV; ② check-icr ICR #2263; ③ check-citations vs gold corpus). Citation reconciled: **XU/krn_8_0_dg_toolkit_ug / ICR #2263**, not the plan's XT guess. **Keystone unblock:** driver→live XPAR via `M_YDB_GBLDIR`/`M_YDB_ROUTINES`. **Remaining blocker:** `m test --docker` honors gbldir but NOT M_YDB_ROUTINES → VSLCFGTST aborts 0/0 (XPAR routines unresolved); fix = layer the resident routine base in the m test/m-ydb path (m-cli/m-ydb session) or test-in-place `--resident`. - [meta-root + owed VSLSEED filer](meta-root-and-owed.md) — layer declared in **root `repo.meta.json`** (migrated off `dist/` 2026-06-15, Phase B item 1); the owed `fileViaDie^VSLSEED` FileMan filer (re-homed from m-stdlib STDSEED per the G2 waterline decision) lands here when a v-layer seeding consumer needs it. - [t0b4-msl-seam-pin](t0b4-msl-seam-pin.md) — VSL T0b.4 (v-stdlib leg, 2026-06-15): the **cross-repo MSL seam-contract pin** — `dist/msl-seam-pin.json` (pins MSL `v0.6.0` + frozen `seams` copy) + `tools/msl_seam_pin.py` drift gate (`make check-msl-pin`). Reads the sibling MSL @ tag via `git show`; **SKIP-green when unreachable** (so it SKIPs in CI today, asserts at dev-time); fetch-at-tag path deferred to T1.1. Don't conflate with v-stdlib's own (VSL*) `dist/seam-snapshot.json`. diff --git a/docs/memory/m3-vslfs.md b/docs/memory/m3-vslfs.md new file mode 100644 index 0000000..80f8aa4 --- /dev/null +++ b/docs/memory/m3-vslfs.md @@ -0,0 +1,87 @@ +--- +name: m3-vslfs +description: VSL/MSL M3 Lane B DONE — VSLFS binds the STDKV storage seam (MSL v0.9.0) to VistA's FileMan DBS (UPDATE^DIE / $$GET1^DIQ / FILE^DIE). Re-pinned msl_ref v0.8.0→v0.9.0. Dual-engine GREEN 7/7 (vehu YDB + foia-t12 IRIS) over #8989.51: create/get byte-identical, exists, kill (FDA .01="@"), DIERR→,U-VSL-FS-DIERR, $ECODE. 3 boundaries green; ICR notional (DBS marker). +metadata: + type: project +--- + +# VSL T-M3 Lane B — VSLFS (FileMan DBS storage adapter), 2026-06-16 + +The VistA side of the M3 storage seam (S1): `VSLFS` binds the portable MSL +`STDKV` seam (MSL **v0.9.0**) to VistA's FileMan Database Server (DBS) API. +Branch `m3-vslfs` **stacked on `m2-vslio`** (NOT off `main` — m2-vslio is +unmerged and carries the Makefile `--routines $(SRC)` test fix + the +icr-registry/pin tooling VSLFS needs; branching off main would regress VSLIO). +Merge order: m2-vslio → m3-vslfs. Third `VSL*` module (after VSLCFG, VSLIO). + +## Re-pin (boundary ①) +`make pin` after hand-setting `dist/msl-seam-pin.json` `msl_ref` v0.8.0→**v0.9.0**: +syncs the `seams` block from `git show v0.9.0:dist/seam-snapshot.json` → now +carries **STDENV + STDNET + STDKV** (STDKV = 4 verbs). `check-msl-pin` green. + +## The adapter — FileMan DBS binding ONLY (4 verbs, same signature as STDKV) +`$$set^VSLFS(file,iens,field,value)`→resolved IENS · `$$get(file,iens,field, +default)` · `$$exists(file,iens)`→1/0 · `$$kill(file,iens)`→1 · `$$lastError()`. +- **set** = `UPDATE^DIE("","FDA","IEN","ERR")` (handles both `"+1,"` add and + in-place file); returns the resolved IENS (for an add, the new IEN from + `IEN(n)` via `resolveIens`). Pragmatic adapter return (like VSLIO's device + handle), not STDKV's bool — FileMan create must surface the new IEN. +- **get/exists** = `$$GET1^DIQ(file,iens,field,"","","ERR")`. A DIERR on a read + is NOT an error — `$$get` returns the default, `$$exists` returns 0 (the STDKV + "absent → default" semantics). exists probes `.01`. +- **kill** = delete via an FDA **`.01="@"`** through `FILE^DIE("","FDA","ERR")`. + **KEY corpus finding: there is NO `DELETE^DIE`** — the Supported DBS delete is + filing `.01="@"`; `^DIK`/direct global KILL are forbidden (Classic/non-DBS). + +## Error contract — loud (kickoff decision 4) +A DIERR on a **write** maps to a clean **`,U-VSL-FS-DIERR,`** `$ECODE` (via +`raiseDierr`), with the composed DIERR TEXT in `^TMP($job,"vslfs","err")` for +`$$lastError`. Every DBS call passes an explicit **MSG_ROOT `"ERR"`** so errors +land in the adapter's own array, never the shared `^TMP("DIERR",$J)`. `kill` is +idempotent (records a DIERR, still returns 1). + +## Test file — an EXISTING low-risk file (no DD install; that's the deferred v-pkg track) +**#8989.51 PARAMETER DEFINITION**: `.01` (NAME) is free-text with **NO other +required fields** (verified live via a DD probe — `^DD(8989.51,*)`), so a +throwaway **ZZ-namespaced** record (`"ZZVSLFS "_$job_`) is created and +deleted cleanly through the DBS API. The NAME is uppercase, so the round-trip +value is **transform-invariant** → byte-identical set→get over real FileMan. +Present on both engines. + +## Acceptance — dual-engine GREEN 7/7 (the exit criterion) +3 tests / 7 assertions on **BOTH** `vehu`(YDB) and `foia-t12`(IRIS): create→get +byte-identical, exists→kill→exists-false→get-default, and DIERR-is-loud +(`$$set` into bogus file 99999999 raises `U-VSL-FS-...`, `$$lastError` carries +the FileMan text). **No `$ZVERSION` arm** — FileMan DBS is VistA-portable +(kickoff decision 5 confirmed). Full v-stdlib suite on vehu **22/22** (VSLCFG 3 + +VSLFS 7 + VSLIO 10 + smoke 2) — no regression. Recipe: `m test --engine ydb +--docker vehu --chset m --routines src --routines /src tests/VSLFSTST.m` +(IRIS: `--engine iris --docker foia-t12 --namespace VISTA`). Driver stack only. + +## ICR is NOTIONAL — never a blocker (gate change shipped here) +The FileMan DBS API has **no ICR number in the gold corpus** (custodian DI, doc +`DI/fm22_2dg`) — and per the user directive the DBIA registry is a notional, +human-curated FORUM list, not enforced programmatically. So `tools/gen-icr.py` +now accepts a **notional marker** `@icr DBS` (NOTIONAL_MARKERS) in place of a +number; the gate's real invariants stay (`@status Supported` + no-direct-global). +Each VSLFS call: `; doc: @icr DBS @call @status Supported @custodian DI +@source DI/fm22_2dg#`. See shared memory [[notional-dbia-not-a-blocker]] ++ plan §5.4. Do NOT re-raise the missing-number as a gap. + +## Gates (all green) +`make check-fast`: fmt/lint (**0 findings** — restructured `stashDierr`'s `$order` +loops to the `for quit do` house idiom to clear M-MOD-009 4-commands/line) + +`m arch check .` (layer v) + check-seams (0 — VSLFS is a consumer) + **check-icr +(8: VSLCFG #2263×2 + VSLIO #2118×2 + VSLFS DBS×4)** + **check-citations (8 vs +gold corpus — DI/fm22_2dg anchors verified)** + check-namespaces (3 VSL routines) ++ **check-msl-pin (v0.9.0)** + check-engine-access. No KIDS/VSLBLD (that's M5). + +## Owed / next +- **M2 tail** (parallel, unblocked): STDNET IRIS leg + tier-3 TLS. +- **DD-install enabler** (deferred v-pkg track): teach `v pkg` the FileMan + FILE-DD component, then re-test VSLFS with a throwaway *dedicated* file + instead of #8989.51. The "DD install" the M3 milestone bundles. +- **Next: M4** (VSLSEC + VSLLOG — security + audit seams, §12.2). VSLLOG reuses + this FileMan-DBS binding (S3) for the audit-file sink. +Companion to [[m2-vslio]] (the Lane-B adapter rhythm) + the m-stdlib leaf +`m3-stdkv-storage-seam`. diff --git a/src/VSLFS.m b/src/VSLFS.m new file mode 100644 index 0000000..28a892f --- /dev/null +++ b/src/VSLFS.m @@ -0,0 +1,124 @@ +VSLFS ; v-stdlib — VistA FileMan storage adapter (FileMan DBS record store). + ; m-lint: disable-file=M-MOD-024 + ; M-MOD-024 false positives: the analyser reads the FileMan DBS I/O arrays + ; (FDA / IEN / ERR, written by the called DBS routine by-reference) as + ; locals-before-def; they are the documented GETS/UPDATE/FILE convention. + ; Same suppression as VSLIO/STDNET. + ; + ; Binds the MSL storage seam (STDKV, S1) to VistA's FileMan Database Server + ; (DBS) API: a record store addressed by (file, iens, field). It exposes the + ; same four-verb signature as STDKV — $$set/$$get/$$exists/$$kill — backed by + ; FileMan DBS calls, never direct global access (architecture §3.2). The + ; adapter contains ONLY the VistA binding; any non-FileMan logic stays in the + ; MSL seam, called up (m/v waterline §9 no-duplication). + ; + ; Public API (the handle is a FileMan IENS; values are field values): + ; $$set^VSLFS(file,iens,field,value) — file a field (UPDATE^DIE); add a + ; record with iens "+1," -> resolved IENS + ; $$get^VSLFS(file,iens,field,default)— read a field ($$GET1^DIQ), else default + ; $$exists^VSLFS(file,iens) — 1 iff the record exists + ; $$kill^VSLFS(file,iens) — delete the record (FILE^DIE, .01="@") + ; $$lastError^VSLFS() — last FileMan DIERR detail, else "" + ; + ; *** ERROR CONTRACT — loud, never a silent wrong value *** + ; A FileMan DIERR on a write maps to a clean ,U-VSL-FS-DIERR, $ECODE, with the + ; DIERR text composed into ^TMP($job,"vslfs","err") for $$lastError. Reads of + ; an absent record/field are NOT errors — $$get returns the default and + ; $$exists returns 0 (the STDKV "absent -> default" semantics). Every DBS call + ; passes an explicit MSG_ROOT ("ERR") so errors land in this adapter's own + ; array, never the shared ^TMP("DIERR",$J). + ; + ; ICR note: the FileMan DBS API is the public DBS programmer API (FileMan + ; Developer's Guide, custodian DI). The DBIA/ICR *number* is notional — a + ; manually-curated FORUM list, not enforced programmatically — so each call is + ; tagged `@icr DBS` (the notional marker), with a real @status/@custodian/ + ; @source. See docs/memory notional-dbia-not-a-blocker + plan §5.4. + ; + quit + ; + ; ---------- the storage seam, bound to FileMan DBS (4 verbs) ---------- + ; +set(file,iens,field,value) ; File `value` into (file,iens,field); return the resolved IENS, else raise. + ; doc: @param file numeric FileMan file number + ; doc: @param iens string IENS; "+1," (etc.) adds a new record + ; doc: @param field string field number within the file + ; doc: @param value string external value to file + ; doc: @returns string the resolved IENS on success (the new IENS for an add) + ; doc: @raises U-VSL-FS-DIERR a FileMan DIERR (detail in $$lastError) + ; doc: @icr DBS @call UPDATE^DIE @status Supported @custodian DI @source DI/fm22_2dg#updatedie-updater + new FDA,IEN,ERR + set FDA(file,iens,field)=value + do UPDATE^DIE("","FDA","IEN","ERR") + if $data(ERR("DIERR")) do raiseDierr("set",.ERR) quit "" + quit $$resolveIens(iens,.IEN) + ; +get(file,iens,field,default) ; Read (file,iens,field) via $$GET1^DIQ; return value, else `default`. + ; doc: @param file numeric FileMan file number + ; doc: @param iens string IENS of the record + ; doc: @param field string field number + ; doc: @param default string value returned when the field/record is unset + ; doc: @returns string the external field value, or `default` + ; doc: @icr DBS @call $$GET1^DIQ @status Supported @custodian DI @source DI/fm22_2dg#get1diq-data-retriever-single-field + new val,ERR + set val=$$GET1^DIQ(file,iens,field,"","","ERR") + if $data(ERR("DIERR")) quit default + quit $select(val="":default,1:val) + ; +exists(file,iens) ; Return 1 iff record (file,iens) exists (its .01 reads without a DIERR). + ; doc: @param file numeric FileMan file number + ; doc: @param iens string IENS of the record + ; doc: @returns bool 1 iff the record exists; 0 otherwise + ; doc: @icr DBS @call $$GET1^DIQ @status Supported @custodian DI @source DI/fm22_2dg#get1diq-data-retriever-single-field + new val,ERR + set val=$$GET1^DIQ(file,iens,".01","","","ERR") + if $data(ERR("DIERR")) quit 0 + quit $select(val="":0,1:1) + ; +kill(file,iens) ; Delete record (file,iens) via an FDA .01="@" through FILE^DIE; return 1. + ; doc: @param file numeric FileMan file number + ; doc: @param iens string IENS of the record to delete + ; doc: @returns bool 1 (idempotent — a DIERR is recorded, not raised) + ; doc: @icr DBS @call FILE^DIE @status Supported @custodian DI @source DI/fm22_2dg#filedie-filer + new FDA,ERR + set FDA(file,iens,".01")="@" + do FILE^DIE("","FDA","ERR") + if $data(ERR("DIERR")) do stashDierr("kill",.ERR) + quit 1 + ; +lastError() ; The last VSLFS error message (the composed FileMan DIERR detail). + ; doc: @returns string ^TMP($job,"vslfs","err"), or "" if none + quit $get(^TMP($job,"vslfs","err")) + ; + ; ---------- internals ---------- + ; +raiseDierr(who,ERR) ; Stash the DIERR detail, then raise the clean ,U-VSL-FS-DIERR,. + do stashDierr(who,.ERR) + set $ecode=",U-VSL-FS-DIERR," + quit + ; +stashDierr(who,ERR) ; Compose the FileMan DIERR text into ^TMP($job,"vslfs","err"). + new m,nl,seq + set nl=$char(10) + set m=who_": FileMan DIERR ("_$get(ERR("DIERR"))_")" + set seq=$order(ERR("DIERR","")) + for quit:seq="" do + . do:seq=+seq addText(seq,.ERR,.m,nl) + . set seq=$order(ERR("DIERR",seq)) + set ^TMP($job,"vslfs","err")=m + quit + ; +addText(seq,ERR,m,nl) ; Append every TEXT line of DIERR `seq` to `m` (by ref). + new ln + set ln=$order(ERR("DIERR",seq,"TEXT","")) + for quit:ln="" do + . set m=m_nl_$get(ERR("DIERR",seq,"TEXT",ln)) + . set ln=$order(ERR("DIERR",seq,"TEXT",ln)) + quit + ; +resolveIens(iens,IEN) ; Resolve a "+n," add-node IENS to its real IENS; else echo iens. + ; UPDATE^DIE returns the new internal entry number for a "+n," placeholder in + ; IEN(n); a non-placeholder IENS files in place and is returned unchanged. + new n + if $extract(iens,1)'="+" quit iens + set n=+$piece($extract(iens,2,$length(iens)),",") + quit $get(IEN(n))_"," diff --git a/tests/VSLFSTST.m b/tests/VSLFSTST.m new file mode 100644 index 0000000..c725ed1 --- /dev/null +++ b/tests/VSLFSTST.m @@ -0,0 +1,67 @@ +VSLFSTST ; v-stdlib — VSLFS (FileMan DBS storage adapter) test suite. + ; Exercises VSLFS against a live VistA's FileMan DBS API, over the driver + ; stack only (m/v waterline — the ONLY path): + ; m test --engine ydb --docker vehu --chset m \ + ; --routines src --routines /src tests/VSLFSTST.m + ; m test --engine iris --docker foia-t12 --namespace VISTA \ + ; --routines src --routines /src tests/VSLFSTST.m + ; + ; The "test FileMan file" is an EXISTING low-risk file — #8989.51 PARAMETER + ; DEFINITION — whose .01 (NAME) is free-text with NO other required fields, so + ; a throwaway, ZZ-namespaced record can be created and deleted cleanly through + ; the DBS API (no DD install needed; the DD-install enabler is a deferred + ; v-pkg track). Each test creates a uniquely-named record and removes it; the + ; .01 NAME is uppercase free text, so the round-trip value is chosen + ; transform-invariant (byte-identical set->get over real FileMan). + new pass,fail + do start^STDASSERT(.pass,.fail) + ; + do tCreateGetRoundtrip(.pass,.fail) + do tExistsThenKill(.pass,.fail) + do tDierrIsLoud(.pass,.fail) + ; + do report^STDASSERT(pass,fail) + quit + ; +tCreateGetRoundtrip(pass,fail) ;@TEST "$$set creates a record and $$get reads its field back byte-identical" + new file,name,iens + do setup(.file) + set name="ZZVSLFS "_$job_"RT" + set iens=$$set^VSLFS(file,"+1,",".01",name) + do true^STDASSERT(.pass,.fail,iens'="","record created (got a resolved IENS)") + quit:iens="" + do eq^STDASSERT(.pass,.fail,$$get^VSLFS(file,iens,".01","MISS"),name,"field reads back byte-identical") + do teardown(file,iens) + quit + ; +tExistsThenKill(pass,fail) ;@TEST "$$exists is true after create; $$kill removes the record so $$exists is false and $$get returns default" + new file,name,iens,x + do setup(.file) + set name="ZZVSLFS "_$job_"EK" + set iens=$$set^VSLFS(file,"+1,",".01",name) + quit:iens="" + do eq^STDASSERT(.pass,.fail,$$exists^VSLFS(file,iens),1,"record exists after create") + set x=$$kill^VSLFS(file,iens) + do eq^STDASSERT(.pass,.fail,$$exists^VSLFS(file,iens),0,"record gone after kill") + do eq^STDASSERT(.pass,.fail,$$get^VSLFS(file,iens,".01","gone"),"gone","killed field reads as default") + quit + ; +tDierrIsLoud(pass,fail) ;@TEST "a FileMan DIERR maps to a clean ,U-VSL-FS-..., $ECODE with the detail in $$lastError" + new file + do setup(.file) + do raises^STDASSERT(.pass,.fail,"set x=$$set^VSLFS(99999999,""+1,"","".01"",""ZZ"")","U-VSL-FS","$$set into a bogus file raises U-VSL-FS-...") + do true^STDASSERT(.pass,.fail,$$lastError^VSLFS()'="","lastError carries the FileMan DIERR detail") + quit + ; + ; ---------- fixtures ---------- + ; +setup(file) ; FileMan programmer context + the safe test file (#8989.51). + set DUZ=1,DUZ(0)="@",U="^",DT=$$DT^XLFDT + set file=8989.51 + quit + ; +teardown(file,iens) ; Remove the throwaway record if it still exists. + new x + quit:'$$exists^VSLFS(file,iens) + set x=$$kill^VSLFS(file,iens) + quit diff --git a/tools/gen-icr.py b/tools/gen-icr.py index f6a3682..946c8a4 100644 --- a/tools/gen-icr.py +++ b/tools/gen-icr.py @@ -56,6 +56,17 @@ # (Private, Supplemental, Retired, …) is a violation when a call relies on it. OK_STATUSES = frozenset({"Supported", "Controlled Subscription"}) +# Notional ICR markers (in place of a number). The VistA DBIA/ICR *registry* is +# a manually human-curated FORUM list — not in code, not in a FileMan DD, not +# enforced programmatically — so the *number* is notional and must never be a +# hard gate requirement (coordination plan §5.4; user directive 2026-06-16). A +# declaration may carry one of these markers instead of a number; the gate's +# real invariants stay (@status Supported + no direct global access), and the +# missing number raises NO warning. The canonical case is the FileMan DBS API +# (GETS^DIQ / $$GET1^DIQ / UPDATE^DIE / FILE^DIE / $$FIND1^DIC), for which no ICR +# number exists in the gold corpus by design. +NOTIONAL_MARKERS = frozenset({"DBS", "notional"}) + # A reference to an external routine or global: ^NAME or label^NAME. REF_RE = re.compile(r"\^(%?[A-Z][A-Z0-9]*)") # A direct set/kill against a global (point 3): SET ^NAME(...) / KILL ^NAME(...). @@ -89,17 +100,30 @@ def is_l4_name(name: str) -> bool: def parse_icr_tag(body: str) -> dict: """Parse an `@icr …` doc-tag body into a registry entry. - Body shape: ` @call @status @custodian @source `. - Fields after the leading number are `@key value` segments; a value runs to - the next `@key` (so multi-word statuses like "Controlled Subscription" work). + Body shape: ` @call @status @custodian @source `, + where `` is either a number (a real DBIA) or a notional marker + (`DBS`/`notional` — see NOTIONAL_MARKERS; the number is notional and never a + gate requirement). Fields after the leading token are `@key value` segments; + a value runs to the next `@key` (so multi-word statuses like "Controlled + Subscription" work). """ body = body.strip() - # Leading number. - m = re.match(r"(\d+)\s*(.*)$", body, re.DOTALL) - if not m: - raise ValueError(f"@icr tag missing leading ICR number: {body!r}") - entry: dict = {"icr": int(m.group(1))} - rest = m.group(2) + # Leading ICR token: a number, or a notional marker. + m = re.match(r"(?P\S+)\s*(?P.*)$", body, re.DOTALL) + if not m or m.group("icr").startswith("@"): + raise ValueError(f"@icr tag missing leading ICR number/marker: {body!r}") + first = m.group("icr") + if first.isdigit(): + icr_val: object = int(first) + elif first in NOTIONAL_MARKERS: + icr_val = first + else: + raise ValueError( + f"@icr leading token {first!r} is neither a number nor a notional " + f"marker {sorted(NOTIONAL_MARKERS)}: {body!r}" + ) + entry: dict = {"icr": icr_val} + rest = m.group("rest") # Split into @key value segments. for seg in re.split(r"(?=@[a-z])", rest): seg = seg.strip() @@ -249,6 +273,21 @@ def expect(cond, msg): e2 = parse_icr_tag("10063 @call $$EN^XPAR @status Controlled Subscription @custodian XT") expect(e2["status"] == "Controlled Subscription", f"multiword status wrong: {e2}") + # notional ICR marker (FileMan DBS — no number exists; never a blocker) + e3 = parse_icr_tag("DBS @call $$GET1^DIQ @status Supported @custodian DI " + "@source DI/fm22_2dg#get1diq-data-retriever-single-field") + expect(e3["icr"] == "DBS", f"notional icr marker wrong: {e3}") + expect(e3["call"] == "$$GET1^DIQ" and e3["status"] == "Supported", f"notional fields wrong: {e3}") + # a notional-Supported call is conformant (green) — the number is not required + expect(find_icr_violations({"VSLFS": [e3]}, [("VSLFS", "DIQ", 9, "call")]) == [], + "notional Supported DBS call should be green") + # a bogus leading token (typo) is rejected, not silently accepted + try: + parse_icr_tag("DBZ @call X^Y @status Supported") + expect(False, "bogus icr marker should raise") + except ValueError: + pass + # is_l4_name expect(is_l4_name("DIC") and is_l4_name("%ZISTCP") and is_l4_name("XPAR"), "L4 names misclassified") expect(not is_l4_name("STDENV") and not is_l4_name("VSLCFG"), "own namespaces flagged as L4")