Skip to content

Commit e670308

Browse files
authored
Fix corpus false positives in deck diagnostics (#53)
* Fix TITLE free-text body being parsed as a keyword The line following TITLE is free-form text, but the diagnostics engine scanned it as a keyword. When that text happened to be an upper-case token matching a real keyword name (CO2STORE, H2STORE) or an arbitrary label (ACTIONX_GCONPROD, PYACTION_GCONPROD_INSERT_KW), it was wrongly flagged as an indented or unrecognised keyword. Consume the single free-form text line after any raw-text keyword (TITLE) verbatim, before section/terminator/keyword analysis. * Infer one record for fixed keywords lacking a resolved count opm-common classifies some keywords as 'fixed' without a concrete record count (EOS derives its count from another keyword). The engine read that as 'expects zero records', so the value record on the next line (EOS / PR) was mistaken for a new keyword and PR was flagged as unrecognised. When a fixed keyword has no records_meta/size_count but does declare a per-record column count, assume it expects at least one record. * Don't require a terminator on bare SUMMARY array vectors Probe-expanded SUMMARY vectors (WSIR, WSPR, WMCTL, and the C/W component-rate vectors) are classified 'array' but, unlike the manual-derived mnemonics, lack the optional_body flag. Written bare and stacked under a single shared '/', they were wrongly flagged as missing their own terminating '/'. Treat any array-kind keyword whose sections include SUMMARY as optional-body when it consumed no records, keying off the SUMMARY section so real cell arrays (PRESSURE, PORO) still require their '/'. * Understand RPT* report keywords' free-form mnemonic bodies RPTRST/RPTSCHED/RPTSOL/… take a list of output mnemonics terminated by '/'. Many mnemonics are spelled like real keywords in column 1 (PRESSURE, SGAS, SOIL, XMF, YMF, ZMF), so the scanner closed the report block early and flagged each as a wrong-section / unterminated keyword. While a report keyword's block is open, treat every column-1 token as body content; a section header still ends the block. RPTSCHED is dropped from the default exclusion list since the body is now parsed correctly without suppressing checks on the keyword itself. * Add curated supplement for OPM keywords missing from the index Some keywords OPM Flow accepts are in neither the reference manual nor opm-common, so they never reach the generated keyword index and were flagged as unrecognised on the known-good corpus — and where a missing keyword swallowed its own records, the well names under it cascaded into further false positives. Add a small hand-maintained supplement (CO2STORE/H2STORE/thermal/ compositional keywords FGDN, FCGMM/I, FCWM, WCMPR/IR, WELLSHUT, STORE, AIM, CVTYPE, AMF, PREFT, ZCRITVIS, SPECHA-H) merged into the index by both the extension and the corpus harness. WELLSHUT is list-shaped so it absorbs its well-name records; the SUMMARY vectors carry section/shape; the rest are recognised-only. * Normalise shapeless SUMMARY vectors and stop them swallowing mnemonics opm-common's probe expansion (and the L-modifier variants CGMIRL, CGMPRL, …) emit recognised SUMMARY entries with no record shape, so their optional '/'-terminated name list (CGMIRL / INJ1 / /) was parsed with the well name mistaken for a new keyword. Normalise every SUMMARY-section vector that lacks a size_kind to the 'array' shape (the bare case is already exempt from the terminator check). Guard the array name-list absorption so a column-1 token that is itself a recognised SUMMARY vector by shape (UDQ name, region-set, L-modifier, deck_name_regex family) starts its own vector rather than being swallowed — otherwise a bare enable-keyword (PERFORMA) absorbs the UDQ mnemonics that follow it and is then flagged for a missing terminator. Both loaders now share prepareKeywordIndex (supplement + normalisation).
1 parent 574280c commit e670308

7 files changed

Lines changed: 514 additions & 31 deletions

File tree

vscode-extension/src/analysis.test.ts

Lines changed: 191 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,49 @@ const index: Record<string, AnalysisEntry> = {
6868
sections: ['SCHEDULE'],
6969
size_kind: 'array',
7070
},
71+
TITLE: {
72+
name: 'TITLE',
73+
sections: ['RUNSPEC'],
74+
size_kind: 'none',
75+
},
76+
CO2STORE: {
77+
name: 'CO2STORE',
78+
sections: ['RUNSPEC'],
79+
size_kind: 'none',
80+
},
81+
// `fixed` keyword whose record count opm-common leaves unresolved (no
82+
// size_count); only `expected_columns` is known.
83+
EOS: {
84+
name: 'EOS',
85+
expected_columns: 1,
86+
sections: ['RUNSPEC', 'PROPS'],
87+
size_kind: 'fixed',
88+
},
89+
// SUMMARY-section array vectors (probe-expanded), no optional_body flag.
90+
WSIR: {
91+
name: 'WSIR',
92+
sections: ['SUMMARY'],
93+
size_kind: 'array',
94+
},
95+
WSPR: {
96+
name: 'WSPR',
97+
sections: ['SUMMARY'],
98+
size_kind: 'array',
99+
},
100+
// Report keyword with a free-form mnemonic body terminated by '/'.
101+
RPTRST: {
102+
name: 'RPTRST',
103+
sections: ['SOLUTION', 'SCHEDULE'],
104+
size_kind: 'fixed',
105+
size_count: 1,
106+
variadic_record: true,
107+
},
108+
// SOLUTION cell array whose name collides with an RPTRST mnemonic.
109+
PRESSURE: {
110+
name: 'PRESSURE',
111+
sections: ['SOLUTION'],
112+
size_kind: 'array',
113+
},
71114
};
72115

73116
// ---------------------------------------------------------------------------
@@ -692,10 +735,10 @@ describe('computeDiagnostics — unknown keywords', () => {
692735
});
693736

694737
it('does not flag keywords on the exclusion list', () => {
695-
// RPTSCHED is excluded — must not be flagged as unknown even though it's
696-
// absent from the supplied test index.
697-
const lines = ['SCHEDULE', 'RPTSCHED', "'WELLS=2' /", '/'];
698-
expect(computeDiagnostics(lines, index)).toEqual([]);
738+
// A keyword on the exclusion set must not be flagged as unknown even when
739+
// it's absent from the supplied index.
740+
const lines = ['SCHEDULE', 'FOORPT', "'WELLS=2' /", '/'];
741+
expect(computeDiagnostics(lines, index, new Set(['FOORPT']))).toEqual([]);
699742
});
700743

701744
it('does not run record-body checks after an unknown keyword', () => {
@@ -915,19 +958,24 @@ describe('computeDiagnostics — excluded keywords', () => {
915958
},
916959
};
917960

918-
it('does not flag RPTSCHED in a section where it would otherwise be invalid', () => {
961+
// An explicitly-excluded keyword opts out of every check. (RPTSCHED is no
962+
// longer excluded by default — its free-form body is now understood
963+
// directly — so these exercise the mechanism via an explicit custom set.)
964+
const excludeRptsched = new Set(['RPTSCHED']);
965+
966+
it('does not flag an excluded keyword in an otherwise-invalid section', () => {
919967
const lines = ['SCHEDULE', 'RPTSCHED', "'WELLS=2' 'SUMMARY=2' 'CPU=2' /", '/'];
920-
expect(computeDiagnostics(lines, indexWithRptsched)).toEqual([]);
968+
expect(computeDiagnostics(lines, indexWithRptsched, excludeRptsched)).toEqual([]);
921969
});
922970

923-
it('does not flag arity overflow on RPTSCHED records', () => {
971+
it('does not flag arity overflow on an excluded keyword', () => {
924972
const lines = ['SCHEDULE', 'RPTSCHED', "'A' 'B' 'C' 'D' /", '/'];
925-
expect(computeDiagnostics(lines, indexWithRptsched)).toEqual([]);
973+
expect(computeDiagnostics(lines, indexWithRptsched, excludeRptsched)).toEqual([]);
926974
});
927975

928-
it('does not flag a missing list terminator on RPTSCHED', () => {
976+
it('does not flag a missing list terminator on an excluded keyword', () => {
929977
const lines = ['SCHEDULE', 'RPTSCHED', "'WELLS=2' /", 'WELSPECS', '/'];
930-
expect(computeDiagnostics(lines, indexWithRptsched)).toEqual([]);
978+
expect(computeDiagnostics(lines, indexWithRptsched, excludeRptsched)).toEqual([]);
931979
});
932980

933981
it('honours a custom exclusion set passed to computeDiagnostics', () => {
@@ -1905,3 +1953,136 @@ describe('computeDiagnostics — ACTIONX block', () => {
19051953
expect(computeDiagnostics(lines, index)).toEqual([]);
19061954
});
19071955
});
1956+
1957+
// ---------------------------------------------------------------------------
1958+
// Raw-text keyword bodies (TITLE)
1959+
// ---------------------------------------------------------------------------
1960+
1961+
describe('computeDiagnostics — raw-text keyword bodies', () => {
1962+
it('does not parse an indented TITLE text line as a keyword', () => {
1963+
const lines = [
1964+
'RUNSPEC',
1965+
'TITLE',
1966+
' CO2STORE',
1967+
'DIMENS',
1968+
'20 1 20 /',
1969+
];
1970+
expect(computeDiagnostics(lines, index)).toEqual([]);
1971+
});
1972+
1973+
it('does not flag a TITLE text token that collides with a keyword name', () => {
1974+
const lines = [
1975+
'RUNSPEC',
1976+
'TITLE',
1977+
'ACTIONX_GCONPROD',
1978+
];
1979+
expect(computeDiagnostics(lines, index)).toEqual([]);
1980+
});
1981+
1982+
it('skips a blank line before consuming the TITLE text', () => {
1983+
const lines = [
1984+
'RUNSPEC',
1985+
'TITLE',
1986+
'',
1987+
'CO2STORE STUDY',
1988+
'DIMENS',
1989+
'1 1 1 /',
1990+
];
1991+
expect(computeDiagnostics(lines, index)).toEqual([]);
1992+
});
1993+
});
1994+
1995+
// ---------------------------------------------------------------------------
1996+
// Fixed keywords with an unresolved record count (EOS)
1997+
// ---------------------------------------------------------------------------
1998+
1999+
describe('computeDiagnostics — fixed keyword without size_count', () => {
2000+
it('absorbs the value record of a fixed keyword with no resolved count', () => {
2001+
const lines = [
2002+
'RUNSPEC',
2003+
'EOS',
2004+
'PR /',
2005+
'DIMENS',
2006+
'1 1 1 /',
2007+
];
2008+
expect(computeDiagnostics(lines, index)).toEqual([]);
2009+
});
2010+
2011+
it('still starts a new keyword when a known keyword follows a bare EOS', () => {
2012+
const lines = [
2013+
'RUNSPEC',
2014+
'EOS',
2015+
'DIMENS',
2016+
'1 1 1 /',
2017+
];
2018+
expect(computeDiagnostics(lines, index)).toEqual([]);
2019+
});
2020+
});
2021+
2022+
// ---------------------------------------------------------------------------
2023+
// Bare SUMMARY array vectors (WSIR/WSPR stacked, shared terminator)
2024+
// ---------------------------------------------------------------------------
2025+
2026+
describe('computeDiagnostics — bare SUMMARY array vectors', () => {
2027+
it('does not require a terminator on bare, stacked SUMMARY vectors', () => {
2028+
const lines = [
2029+
'SUMMARY',
2030+
'WSIR',
2031+
'WSPR',
2032+
'/',
2033+
];
2034+
expect(computeDiagnostics(lines, index)).toEqual([]);
2035+
});
2036+
2037+
it('accepts a lone bare SUMMARY vector with no body', () => {
2038+
const lines = [
2039+
'SUMMARY',
2040+
'WSIR',
2041+
];
2042+
expect(computeDiagnostics(lines, index)).toEqual([]);
2043+
});
2044+
2045+
it('still flags a SUMMARY vector that lists wells but forgets the close', () => {
2046+
const lines = [
2047+
'SUMMARY',
2048+
'WSIR',
2049+
"'PROD1' 'PROD2'",
2050+
];
2051+
const diags = computeDiagnostics(lines, index);
2052+
expect(diags).toHaveLength(1);
2053+
expect(diags[0].message).toMatch(/WSIR: missing terminating/);
2054+
});
2055+
});
2056+
2057+
// ---------------------------------------------------------------------------
2058+
// Free-form report mnemonic bodies (RPTRST, …)
2059+
// ---------------------------------------------------------------------------
2060+
2061+
describe('computeDiagnostics — report keyword mnemonic bodies', () => {
2062+
it('does not parse RPTRST mnemonics that collide with keyword names', () => {
2063+
const lines = [
2064+
'SOLUTION',
2065+
'RPTRST',
2066+
'BASIC = 2',
2067+
'PRESSURE',
2068+
'SGAS',
2069+
'SOIL',
2070+
'XMF',
2071+
'YMF',
2072+
'ZMF',
2073+
'/',
2074+
];
2075+
expect(computeDiagnostics(lines, index)).toEqual([]);
2076+
});
2077+
2078+
it('lets a section header end an unterminated report block', () => {
2079+
const lines = [
2080+
'SOLUTION',
2081+
'RPTRST',
2082+
'PRESSURE',
2083+
'SGAS',
2084+
'SCHEDULE',
2085+
];
2086+
expect(computeDiagnostics(lines, index)).toEqual([]);
2087+
});
2088+
});

0 commit comments

Comments
 (0)