Replace fixed DSL fields with parameterized metric expressions

This commit is contained in:
Peter Stockings
2026-10-04 10:24:12 +11:00
parent 418e03583a
commit d660b86bb0
18 changed files with 402 additions and 137 deletions
+6 -3
View File
@@ -31,7 +31,9 @@
['days', 'Daily workload and rest days, using local calendar windows'],
['health', 'Private daily recovery observations from Garmin'],
['reduce by run min 3 min', 'Match recording windows first; one observation per qualifying session'],
['long run distance >= 10 km', 'Choose the qualifying long-run threshold for daily continuity'],
['summarize latest(days_since_run(distance >= 10 km))', 'Days since a run matching your condition, including prior history'],
['summarize min(best_time(5 km))', 'Fastest recorded effort at a distance'],
['summarize latest(rolling_sum(distance, 28 days))', 'Trailing distance using a configurable window'],
['route similar to 1', 'Approximate full-course match to one of your own recorded routes'],
['where elapsed >= 10 min', 'Exclude the first ten elapsed recording minutes'],
['and grade between -2 and 2 %', 'Require approximately flat smoothed terrain'],
@@ -90,7 +92,7 @@
if (stream.match(/[a-zA-Z_][a-zA-Z0-9_]*/)) {
const word = stream.current();
if (names.has(word) || availableMetrics().some(metric=>metric.name===word)) return 'variable-2';
if (language.functions.includes(word)) return 'builtin';
if (language.functions.includes(word) || word in language.metric_functions) return 'builtin';
if (/^(runs|during|last|ending|where|and|or|between|summarize|by|compare|previous|period|none|has|missing|title|contains|sort|asc|desc|to|let|having|rank|take|limit|trend|rolling|cumulative|change|percent_change|plot|vs|chart|bins|all|time|baseline|flag|sd|min|sustained|buckets|above|below|outside|display|overlay|separate|color|size|reference|as)$/.test(word)) return 'keyword';
if (/^(day|days|week|weeks|month|months|km|m|bpm|spm|W|ms|h|min|s|date)$/.test(word)) return 'atom';
return null;
@@ -118,6 +120,7 @@
} else if (/^\s*(summarize|having|rank by|let \w+ =)\s/.test(line) || /^\s*plot\s+(?:\w+ vs )?\w*$/.test(line) || /^\s*sort by\s+\w*$/.test(line) || /^\s*(where|and|or)\s+(?:(has|missing)\s+)?\w*$/.test(line)) {
const used=/^\s*summarize\s/.test(line)?new Set(before.replace(/^\s*summarize\s+/,'').split(',').map(name=>name.trim())):new Set();
list = availableMetrics().filter(metric=>metric.name.startsWith(token)&&!used.has(metric.name)).map(metric=>({text:metric.name + (/\(\w*$/.test(line) && cm.getLine(cursor.line)[cursor.ch] !== ')' ? ')' : ''), detail:`${metric.unit} · run metric`}));
Object.entries(language.metric_functions).filter(([name])=>name.startsWith(token)).forEach(([,spec])=>list.push({text:spec.example,detail:spec.description}));
if (/^\s*(summarize|having|rank by)\s/.test(line) && !/\(\w*$/.test(line)) language.functions.filter(name=>name.startsWith(token)).forEach(name=>list.push({text:name==='count'?'count()':name+'(',detail:'Statistical function · available values only'}));
if (/sort by/.test(line) && 'date'.startsWith(token)) list.push({text:'date',detail:'Local run date'});
if (/^\s*(where|and|or)\s+\w*$/.test(line)) ['has','missing','title'].forEach(word=>{if(word.startsWith(token))list.push({text:word,detail:'Filter recorded data'});});
@@ -130,7 +133,7 @@
list = ['asc','desc'].filter(text=>text.startsWith(token)).map(text=>({text,detail:'Evidence order'}));
} else if (/^\s*(where|and|or)\s+\w+\s+(?:>=|<=|>|<|=|!=|between)\s*$/.test(line)) {
const field=line.trim().split(/\s+/)[1];
const values={distance:'8 km',pace:'5:30 /km',duration:'45 min',heart_rate:'145 bpm',cadence:'175 spm',power:'250 W',elevation:'100 m',ground_contact:'250 ms',stride_length:'1 m',gap_since_previous_run:'7 days',speed:'12 km/h',rolling_7d_distance:'30 km',run_day_streak:'3 days'};
const values={distance:'8 km',pace:'5:30 /km',duration:'45 min',heart_rate:'145 bpm',cadence:'175 spm',power:'250 W',elevation:'100 m',ground_contact:'250 ms',stride_length:'1 m',gap_since_previous_run:'7 days',speed:'12 km/h',run_day_streak:'3 days'};
const between=/between\s*$/.test(line);
const ranges={distance:'8 and 20 km',pace:'5:00 and 6:00 /km',duration:'30 and 60 min',heart_rate:'140 and 150 bpm',cadence:'170 and 185 spm',power:'200 and 300 W',elevation:'50 and 150 m',ground_contact:'230 and 270 ms',stride_length:'0.8 and 1.2 m'};
const metric=availableMetrics().find(metric=>metric.name===field); const example=values[field] || (metric?.units.length ? `5 ${metric.units[0]}` : null); list=example?[{text:between?(ranges[field] || `5 and 10 ${metric.units[0]}`):example,detail:'Example value · adjust to your question'}]:[];