Skip to content

Commit 2aab7a8

Browse files
committed
Resolve deck_name aliases and suppress opm-common family containers
opm-common models families of summary vectors and array variants under a single schema name (WELL_PROBE, AQUIFER_PROBE_ANALYTIC, MULT_XYZ, …) whose deck_names are the concrete keywords users actually type (WOPR, AAQP, MULTX). - Suppress the 31 family/container schema names from the index: they are never typed in a deck, but were leaking in as fake keywords with a placeholder summary, polluting completions and passing as valid. Real keywords that carry deck_names (KRNUM, IMBNUM, DIFF, NEXTSTEP) keep their reference-manual entry untouched. - Tag every expanded mnemonic with alias_of naming its family keyword (WOPR -> WELL_PROBE, KRNUMX -> KRNUM), including pre-existing entries from the manual SUMMARY table and the directional-variant expansion. Skip the self-reference where a keyword lists its own name in deck_names. - Propagate alias_of into the compact index and surface it in keyword hover. - Document the alias hover and container suppression in the README. Adds python tests and a vscode-free data-integrity test over the shipped index.
1 parent d610100 commit 2aab7a8

6 files changed

Lines changed: 188 additions & 4 deletions

File tree

scripts/build_keyword_index.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,17 @@ def synthesize_opm_only_entries(index: dict, opm_common_index: dict) -> int:
559559
for name, opm in opm_common_index.items():
560560
if name in index:
561561
continue
562+
if opm.get("deck_names"):
563+
# Alias/family container: an opm-common schema name (WELL_PROBE,
564+
# AQUIFER_PROBE_ANALYTIC, ENDPOINT_SPECIFIERS, MULT_XYZ, …) whose
565+
# real deck keywords are listed in ``deck_names`` and are emitted
566+
# separately by ``expand_probe_deck_names``. The container name
567+
# itself is never typed in a deck, so don't synthesize it as a
568+
# keyword (it would otherwise pollute completions and be wrongly
569+
# accepted as valid). Real keywords that happen to carry deck_names
570+
# (KRNUM, IMBNUM, DIFF, NEXTSTEP) already have a reference-manual
571+
# entry, so they were skipped by the ``name in index`` guard above.
572+
continue
562573
sections = list(opm["sections"])
563574
records = opm.get("records")
564575
items = opm["items"]
@@ -622,8 +633,15 @@ def expand_probe_deck_names(index: dict, opm_common_index: dict) -> int:
622633
(WELL_PROBE, FIELD_PROBE, BLOCK_PROBE, …) into individual recognised
623634
entries. Each is a minimal entry (name, sections, one-line summary) with no
624635
size shape, so it is recognised by the diagnostics engine without triggering
625-
arity or terminator checks. Existing entries (e.g. WOPR from the manual) are
626-
left untouched. Returns the number of entries added.
636+
arity or terminator checks.
637+
638+
Every expanded mnemonic is tagged with ``alias_of`` naming the family
639+
keyword it belongs to (WOPR -> WELL_PROBE, KRNUMX -> KRNUM), so hover/docs
640+
can surface the relationship. A mnemonic that already exists (e.g. WOPR from
641+
the manual SUMMARY table, or KRNUMX from the directional-variant expansion)
642+
keeps its richer fields and only gains the ``alias_of`` tag. Returns the
643+
number of entries added (pre-existing entries that were merely tagged are
644+
not counted).
627645
"""
628646
added = 0
629647
for probe_name, opm in opm_common_index.items():
@@ -633,7 +651,20 @@ def expand_probe_deck_names(index: dict, opm_common_index: dict) -> int:
633651
sections = list(opm.get("sections", []))
634652
summary = _probe_summary(opm.get("comment", ""), probe_name)
635653
for dn in deck_names:
636-
if not dn or dn in index:
654+
if not dn or dn == probe_name:
655+
# Some real keywords (IMBNUM, NEXTSTEP) list their own name in
656+
# deck_names alongside their variants; a keyword is not an alias
657+
# of itself, so skip the self-reference.
658+
continue
659+
existing = index.get(dn)
660+
if existing is not None:
661+
# Already present from the manual or directional expansion;
662+
# record the family it aliases without clobbering its fields.
663+
# Index values may be a list (multi-section manual entry) or a
664+
# plain dict; tag the primary entry. First family wins, which
665+
# is deterministic in opm-common's stable load order.
666+
primary = existing[0] if isinstance(existing, list) else existing
667+
primary.setdefault("alias_of", probe_name)
637668
continue
638669
index[dn] = {
639670
"name": dn,
@@ -646,6 +677,7 @@ def expand_probe_deck_names(index: dict, opm_common_index: dict) -> int:
646677
"examples": [],
647678
"full_text": "",
648679
"source_file": "",
680+
"alias_of": probe_name,
649681
}
650682
added += 1
651683
print(f"Expanded {added} summary-vector deck names")
@@ -1645,6 +1677,9 @@ def write_compact_json(index: dict, output_path: Path):
16451677
prohibits = primary.get("prohibits")
16461678
if prohibits:
16471679
out_entry["prohibits"] = prohibits
1680+
alias_of = primary.get("alias_of")
1681+
if alias_of:
1682+
out_entry["alias_of"] = alias_of
16481683
compact[name] = out_entry
16491684
with open(output_path, "w", encoding="utf-8") as f:
16501685
json.dump(compact, f, separators=(",", ":"), ensure_ascii=False)

scripts/test_build_keyword_index.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,28 @@ def test_keywords_only_in_opm_common_get_synthesized(self):
10421042
assert e["parameters"][1]["default"] == "1"
10431043
assert "OPM Flow keyword" in e["summary"]
10441044

1045+
def test_alias_family_containers_are_not_synthesized(self):
1046+
# opm-common schema names that carry deck_names (PROBE families,
1047+
# ENDPOINT_SPECIFIERS, MULT_XYZ, …) are not deck keywords themselves;
1048+
# their deck_names are expanded separately. They must not be added as
1049+
# standalone keywords (else they pollute completions / pass as valid).
1050+
index: dict = {}
1051+
opm = {
1052+
"WELL_PROBE": {
1053+
"sections": ["SUMMARY"],
1054+
"items": [],
1055+
"deck_names": ["WOPR", "WWIP"],
1056+
},
1057+
"PYACTION": { # a normal OPM-only keyword, no deck_names
1058+
"sections": ["SCHEDULE"],
1059+
"items": [{"name": "FILE", "value_type": "STRING"}],
1060+
},
1061+
}
1062+
added = synthesize_opm_only_entries(index, opm)
1063+
assert added == 1
1064+
assert "WELL_PROBE" not in index
1065+
assert "PYACTION" in index
1066+
10451067
def test_already_present_keywords_are_left_alone(self):
10461068
index = {"EXISTING": {"name": "EXISTING", "summary": "kept"}}
10471069
opm = {"EXISTING": {"sections": ["RUNSPEC"], "items": []}}
@@ -1480,6 +1502,8 @@ def test_expands_deck_names_into_minimal_entries(self):
14801502
assert index["WWIP"]["name"] == "WWIP"
14811503
assert index["WWIP"]["sections_opm"] == ["SUMMARY"]
14821504
assert index["WWIP"]["summary"] == "Well summary vectors."
1505+
# Each expanded mnemonic is tagged with the family it derives from.
1506+
assert index["WWIP"]["alias_of"] == "WELL_PROBE"
14831507
# No size shape -> no terminator/arity checks downstream.
14841508
assert "size_kind" not in index["WWIP"]
14851509

@@ -1489,6 +1513,35 @@ def test_does_not_overwrite_existing_entries(self):
14891513
added = expand_probe_deck_names(index, opm)
14901514
assert added == 1
14911515
assert index["WOPR"]["summary"] == "from manual"
1516+
# A pre-existing mnemonic keeps its richer fields but still gains the
1517+
# alias tag so hover can show the family relationship.
1518+
assert index["WOPR"]["alias_of"] == "WELL_PROBE"
1519+
1520+
def test_tags_existing_list_valued_entry(self):
1521+
# Multi-section manual entries are stored as a list of dicts; the
1522+
# primary (first) entry must receive the alias tag without error.
1523+
index = {"WOPR": [{"name": "WOPR", "summary": "from manual"}]}
1524+
opm = {"WELL_PROBE": {"sections": ["SUMMARY"], "deck_names": ["WOPR"]}}
1525+
added = expand_probe_deck_names(index, opm)
1526+
assert added == 0
1527+
assert index["WOPR"][0]["alias_of"] == "WELL_PROBE"
1528+
1529+
def test_self_referential_deck_name_is_not_tagged(self):
1530+
# IMBNUM/NEXTSTEP list their own name in deck_names; a keyword must not
1531+
# be marked as an alias of itself.
1532+
index = {"IMBNUM": {"name": "IMBNUM", "summary": "real keyword"}}
1533+
opm = {"IMBNUM": {"sections": ["REGIONS"],
1534+
"deck_names": ["IMBNUM", "IMBNUMX", "IMBNUMY"]}}
1535+
expand_probe_deck_names(index, opm)
1536+
assert "alias_of" not in index["IMBNUM"]
1537+
assert index["IMBNUMX"]["alias_of"] == "IMBNUM"
1538+
1539+
def test_existing_alias_tag_is_not_overwritten(self):
1540+
# First family wins when a mnemonic is claimed by more than one family.
1541+
index = {"WOPR": {"name": "WOPR", "alias_of": "ALREADY"}}
1542+
opm = {"WELL_PROBE": {"sections": ["SUMMARY"], "deck_names": ["WOPR"]}}
1543+
expand_probe_deck_names(index, opm)
1544+
assert index["WOPR"]["alias_of"] == "ALREADY"
14921545

14931546
def test_collect_deck_name_regexes_dedupes(self):
14941547
opm = {

vscode-extension/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ Hovering over a **value in a data record** shows the description for that specif
6969
parameter column. For example, hovering over the group name in a `WELSPECS` record
7070
shows the `GRPNAME` parameter description, units, and default.
7171

72+
For a summary vector or array variant that derives from an `opm-common` keyword
73+
family — e.g. `WOPR` (from `WELL_PROBE`) or `KRNUMX` (from `KRNUM`) — the hover
74+
adds a *Deck-name alias of `<family>`* line, so you can see which keyword family
75+
the concrete name belongs to.
76+
7277
Keywords on the diagnostics exclusion list (see `opm-flow.diagnostics.excludedKeywords`
7378
under [Settings](#settings)) carry an extra notice in the hover indicating that
7479
arity, terminator, and section checks are skipped — useful when squiggles are
@@ -383,6 +388,12 @@ The language is registered as `opm-flow`.
383388
supply the section header.
384389
- An indented unknown token under an active keyword is treated as record body
385390
rather than flagged as an unknown keyword.
391+
- **Deck-name alias resolution** — concrete summary vectors and directional
392+
array variants now record which `opm-common` keyword family they derive from
393+
(e.g. `WOPR``WELL_PROBE`, `KRNUMX``KRNUM`) and surface it in hover. The
394+
31 family/container schema names themselves (`WELL_PROBE`,
395+
`AQUIFER_PROBE_ANALYTIC`, `MULT_XYZ`, …) are no longer offered as completions
396+
or accepted as valid keywords, since they are never typed in a deck.
386397

387398
### 0.8.0
388399

vscode-extension/data/keyword_index_compact.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

vscode-extension/src/alias.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// ---------------------------------------------------------------------------
2+
// Deck-name alias coverage on the shipped keyword index.
3+
//
4+
// opm-common models families of summary vectors / array variants under a
5+
// single schema name (WELL_PROBE, AQUIFER_PROBE_ANALYTIC, MULT_XYZ, …) whose
6+
// `deck_names` are the concrete keywords a user actually types (WOPR, AAQP,
7+
// MULTX). The build expands those into real index entries tagged with
8+
// `alias_of`, and suppresses the schema container names (which are never typed
9+
// in a deck) so they don't pollute completions or pass as valid keywords.
10+
//
11+
// These assertions guard the shipped `keyword_index_compact.json` against
12+
// regressions in that build step.
13+
// ---------------------------------------------------------------------------
14+
15+
import * as fs from 'fs';
16+
import * as path from 'path';
17+
18+
interface CompactEntry {
19+
name: string;
20+
alias_of?: string;
21+
[k: string]: unknown;
22+
}
23+
24+
function loadIndex(): Record<string, CompactEntry> {
25+
const p = path.join(__dirname, '..', 'data', 'keyword_index_compact.json');
26+
return JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, CompactEntry>;
27+
}
28+
29+
describe('deck-name alias coverage', () => {
30+
const index = loadIndex();
31+
32+
it.each([
33+
'WELL_PROBE',
34+
'FIELD_PROBE',
35+
'AQUIFER_PROBE_ANALYTIC',
36+
'ENDPOINT_SPECIFIERS',
37+
'MULT_XYZ',
38+
])('suppresses the opm-common family/container name %s', (container) => {
39+
expect(index[container]).toBeUndefined();
40+
});
41+
42+
it.each([
43+
['WOPR', 'WELL_PROBE'],
44+
['AAQP', 'AQUIFER_PROBE_ANALYTIC'],
45+
['MULTX', 'MULT_XYZ'],
46+
['KRNUMX', 'KRNUM'],
47+
])('tags %s as an alias of %s', (mnemonic, family) => {
48+
expect(index[mnemonic]).toBeDefined();
49+
expect(index[mnemonic].alias_of).toBe(family);
50+
});
51+
52+
it('keeps real keywords that carry deck_names, untagged', () => {
53+
for (const kw of ['KRNUM', 'IMBNUM', 'DIFF', 'NEXTSTEP']) {
54+
expect(index[kw]).toBeDefined();
55+
expect(index[kw].alias_of).toBeUndefined();
56+
}
57+
});
58+
59+
it('never marks a keyword as an alias of itself', () => {
60+
for (const [name, entry] of Object.entries(index)) {
61+
expect(entry.alias_of).not.toBe(name);
62+
}
63+
});
64+
65+
it('every alias target is itself a known keyword family (no dangling tags)', () => {
66+
// The alias target is either a real keyword still in the index (KRNUM) or a
67+
// suppressed family container — but it must never be the empty string.
68+
for (const entry of Object.values(index)) {
69+
if (entry.alias_of !== undefined) {
70+
expect(typeof entry.alias_of).toBe('string');
71+
expect(entry.alias_of.length).toBeGreaterThan(0);
72+
}
73+
}
74+
});
75+
});

vscode-extension/src/extension.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ interface KeywordEntry {
6969
* when the literal token isn't in the index.
7070
*/
7171
templated?: boolean;
72+
/**
73+
* Deck-name alias: the opm-common family/keyword this mnemonic belongs to
74+
* (WOPR -> WELL_PROBE, KRNUMX -> KRNUM). Surfaced in hover so the user sees
75+
* which keyword family a concrete summary vector or directional variant
76+
* derives from.
77+
*/
78+
alias_of?: string;
7279
}
7380

7481
type KeywordIndex = Record<string, KeywordEntry>;
@@ -594,6 +601,9 @@ function buildKeywordHover(
594601
const sectionLabel = entry.sections.length ? ` — ${entry.sections.join(', ')}` : '';
595602
md.appendMarkdown(`## \`${entry.name}\`${sectionLabel}\n\n`);
596603
if (entry.summary) md.appendMarkdown(`${entry.summary}\n\n`);
604+
if (entry.alias_of) {
605+
md.appendMarkdown(`*Deck-name alias of \`${entry.alias_of}\`.*\n\n`);
606+
}
597607
appendParameterTable(md, entry.parameters, getDocColumns());
598608
if (entry.example) md.appendMarkdown(`**Example**\n\`\`\`\n${entry.example}\n\`\`\`\n`);
599609
return md;

0 commit comments

Comments
 (0)