PsychScanner Parser Modules — Tutorial¶
This tutorial walks you through how parser classes are organized in the psychscanner package after the recent reorganization.
Layout at a glance¶
| Module | Contents | When to use |
|---|---|---|
psychscanner.parsers |
Top-level namespace. Re-exports every bundled parser + list_parsers() / get_parser() registry helpers. |
New code — import from here. |
psychscanner.datasets.prompts.parser_tasks |
Task/paradigm-specific parsers (vividness + reality-monitoring). | When you want to inspect or extend a specific paradigm's parsers. |
psychscanner.datasets.prompts.parser_general |
Paradigm-agnostic parsers (Likert, generic response+rating, word classification, readiness, TaskResponse). |
When you need a generic shape. |
psychscanner.datasets.prompts.parser |
Backward-compat shim. Every name previously importable here still works. | Existing code — keep working unchanged. |
psychscanner.datasets.prompts.parser_extra |
Backward-compat shim. Every name previously importable here still works. | Existing code — keep working unchanged. |
Important: all four import paths return the same class objects — there is no duplication. The shims just import from the themed modules.
1. Setup¶
import psychscanner as psy
from psychscanner.parsers import list_parsers, get_parser, PARSER_REGISTRY
print('psychscanner version:', psy.__version__)
print('Total parsers in registry:', len(PARSER_REGISTRY))
/opt/anaconda3/envs/psyscan/lib/python3.11/site-packages/langgraph/checkpoint/base/__init__.py:17: LangChainPendingDeprecationWarning: The default value of `allowed_objects` will change in a future version. Pass an explicit value (e.g., allowed_objects='messages' or allowed_objects='core') to suppress this warning. from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
psychscanner version: 0.2.0 Total parsers in registry: 41
2. Discover what's available¶
list_parsers() returns every bundled parser class name, sorted.
names = list_parsers()
print(f'{len(names)} bundled parsers:')
for n in names:
print(f' - {n}')
41 bundled parsers: - AllResponseRMEI - AllResponseRMEIN - AllResponseRMIE - AllResponseRMIEN - Confidence16 - DefaultLiteralAgree - DefaultLiteralVivid010 - DefaultLiteralVivid15 - DefaultLiteralVivid15Pol - DefaultParser - DefaultRMEncodingPhase - DefaultRatingParser - DefaultResponseRating - DefaultResponseRatingConvo - DefaultRmChoiceConf16 - DefaultWordCaseNonWord - DefaultWordCaseNonWordConf16 - JudgmentEI - JudgmentEIN - JudgmentIE - JudgmentIEN - Ready - RelatednessRating - ResponseRmScSt - ResponseRmStEI - ResponseRmStEIN - ResponseRmStIE - ResponseRmStIEN - Response_part_1_rm - Response_part_2_rm - Response_part_2_rmrevo - SimpleResponseRating - Source - TaskReadyConfidence - TaskResponse - Task_1_ResponseRate - Task_2_ResponseRate - Task_3_ResponseRate - TwoResponses - Word2 - WordNonWord
3. Look at the themed split¶
Inspect each themed module to see which parsers belong to which group.
import inspect
from pydantic import BaseModel
from psychscanner.datasets.prompts import parser_tasks, parser_general
def native_parsers(mod):
"""Classes actually defined in `mod` (not just re-imported)."""
return sorted(
name for name, obj in inspect.getmembers(mod, inspect.isclass)
if issubclass(obj, BaseModel) and obj is not BaseModel
and obj.__module__ == mod.__name__
)
print('parser_tasks (paradigm-specific):')
for n in native_parsers(parser_tasks): print(f' - {n}')
print()
print('parser_general (paradigm-agnostic):')
for n in native_parsers(parser_general): print(f' - {n}')
parser_tasks (paradigm-specific): - AllResponseRMEI - AllResponseRMEIN - AllResponseRMIE - AllResponseRMIEN - Confidence16 - DefaultLiteralVivid010 - DefaultLiteralVivid15 - DefaultLiteralVivid15Pol - DefaultRMEncodingPhase - DefaultRmChoiceConf16 - JudgmentEI - JudgmentEIN - JudgmentIE - JudgmentIEN - RelatednessRating - ResponseRmScSt - ResponseRmStEI - ResponseRmStEIN - ResponseRmStIE - ResponseRmStIEN - Response_part_1_rm - Response_part_2_rm - Response_part_2_rmrevo - Source - Task_1_ResponseRate - Task_2_ResponseRate - Task_3_ResponseRate - Word2 parser_general (paradigm-agnostic): - DefaultLiteralAgree - DefaultParser - DefaultRatingParser - DefaultResponseRating - DefaultResponseRatingConvo - DefaultWordCaseNonWord - DefaultWordCaseNonWordConf16 - Ready - SimpleResponseRating - TaskReadyConfidence - TaskResponse - TwoResponses - WordNonWord
4. Three ways to import the same parser¶
Pick whichever feels most readable in your code — they all return the same class object.
# Way A — top-level namespace (recommended for new code)
from psychscanner.parsers import DefaultLiteralVivid15 as A
# Way B — themed module (when grouping with related parsers)
from psychscanner.datasets.prompts.parser_tasks import DefaultLiteralVivid15 as B
# Way C — backward-compat shim (existing code keeps working)
from psychscanner.datasets.prompts.parser import DefaultLiteralVivid15 as C
print('All three are the same class object:', A is B is C)
print('Class:', A.__name__)
print('Defined in:', A.__module__)
All three are the same class object: True Class: DefaultLiteralVivid15 Defined in: psychscanner.datasets.prompts.parser_tasks
5. Look up by name (string lookup)¶
When the parser name comes from a config file or a task JSON, use get_parser().
# String-based lookup
parser_cls = get_parser('DefaultLiteralAgree')
print('Resolved:', parser_cls.__name__)
print('Defined in:', parser_cls.__module__)
# Misspelled? Get a clear error with the available options listed.
try:
get_parser('NoSuchParser')
except KeyError as e:
msg = str(e)
print('Error message starts with:')
print(' ', msg[:120], '...')
Resolved: DefaultLiteralAgree Defined in: psychscanner.datasets.prompts.parser_general Error message starts with: "Parser 'NoSuchParser' not found. Available parsers: AllResponseRMEI, AllResponseRMEIN, AllResponseRMIE, AllResponseRMIE ...
6. Inspect a parser's schema¶
Every parser is a Pydantic BaseModel, so you can introspect its JSON schema, fields, and field descriptions.
import json
from psychscanner.parsers import DefaultLiteralVivid15
print('Field names: ', list(DefaultLiteralVivid15.model_fields.keys()))
print('Required fields:', list(DefaultLiteralVivid15.model_fields.keys()))
print()
print('JSON schema:')
print(json.dumps(DefaultLiteralVivid15.model_json_schema(), indent=2)[:600], '...')
Field names:
['Vividness']
Required fields: ['Vividness']
JSON schema:
{
"description": "Give response on a Vividness Rating Scale of range 1 to 5 for the given item.",
"properties": {
"Vividness": {
"description": "Vividness Rating Scale. \n '5' = Perfectly clear and as vivid as normal vision; \n '4' = Clear and reasonably vivid; \n '3' = Moderately clear and vivid; \n '2' = Vague and dim; \n '1' = No image at all, you only \u201cknow\u201d that you are thinking of the object. \n Always give a single integer rating value ranging from 1 to 5 on the ...
7. Use a parser in an ExpCard¶
Pass any parser class directly via the parser= kwarg.
import shutil
from pathlib import Path
from psychscanner import ExpCard, ExpCardInit, ScannerModel
from psychscanner.parsers import DefaultLiteralVivid15
PROJECT_DIR = Path.cwd() / '_parser_modules_tutorial_runs'
if PROJECT_DIR.exists():
shutil.rmtree(PROJECT_DIR)
PROJECT_DIR.mkdir()
task_file = Path.cwd() / 'tasks' / 'example_survey.json'
card = ExpCardInit()
card.proj_dir = PROJECT_DIR
card.projectname = 'direct_class'
card.model = 'mock-chat-model' # mock for a runnable demo
card.family = 'mock-llm'
card.task_file = task_file
card.parser = '0' # mock-llm only supports no-parser mode
card.cogtype = 'no'
card.nsim = 1
card.tunnel_status = '0'
exp = ExpCard(card)
print('Card resolved parser:', exp.parser)
print('Output dir: ', exp.data_root_dir)
----<PROJECT AND DATA ROOT DIRECTORY>----
Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_modules_tutorial_runs
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_modules_tutorial_runs/direct_class/example_survey/mock-llm_mock-chat-model_SingleTurn
----<>----
Card resolved parser: None Output dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_modules_tutorial_runs/direct_class/example_survey/mock-llm_mock-chat-model_SingleTurn
8. Resolve a parser by name from a task JSON (parser="1")¶
When parser='1', psychscanner reads the parser class name from the task JSON's top-level "parser" field and resolves it through the registry. The example survey ships with "parser": "DefaultLiteralAgree".
import json
task_json = json.loads(task_file.read_text())
print('Task JSON declares parser:', task_json['parser'])
card2 = ExpCardInit()
card2.proj_dir = PROJECT_DIR
card2.projectname = 'json_lookup'
card2.task_file = task_file
card2.parser = '1' # ← resolve from task JSON
card2.cogtype = 'no'
card2.nsim = 1
card2.tunnel_status = '0'
exp2 = ExpCard(card2)
print('Card resolved parser:', exp2.parser.__name__)
print('Defined in: ', exp2.parser.__module__)
Task JSON declares parser: DefaultLiteralAgree ----<PROJECT AND DATA ROOT DIRECTORY>----
Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_modules_tutorial_runs
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_modules_tutorial_runs/json_lookup/example_survey/mock-llm_mock-chat-model_SingleTurn
----<>----
Card resolved parser: DefaultLiteralAgree Defined in: psychscanner.datasets.prompts.parser_general
9. What if the JSON references an unknown parser?¶
You get a clear error message listing every available parser — no more cryptic NameError from an eval() call.
import tempfile
bad_task = {
'tasktype': 'survey', 'taskname': 'bad', 'instructions': {'definition': ['x']},
'contexts': ['A'], 'contexts_id': ['A'], 'context_present': True,
'items': {'A_1': [{'trcode': 'A_1', 'stimulus': 'x'}]},
'parser': 'TotallyMadeUpClass',
'chain_type': 'item',
}
tmp_path = Path(tempfile.mkdtemp()) / 'bad.json'
tmp_path.write_text(json.dumps(bad_task))
card_bad = ExpCardInit()
card_bad.task_file = tmp_path
card_bad.parser = '1'
card_bad.cogtype = 'no'
card_bad.nsim = 1
card_bad.tunnel_status = '0'
card_bad.proj_dir = PROJECT_DIR / 'bad_parser_attempt'
try:
ExpCard(card_bad)
except ValueError as e:
print('Caught ValueError:')
print(' ', str(e)[:160], '...')
Caught ValueError: "Parser 'TotallyMadeUpClass' not found. Available parsers: AllResponseRMEI, AllResponseRMEIN, AllResponseRMIE, AllResponseRMIEN, Confidence16, DefaultLiteralAgr ...
10. Define your own parser¶
Any pydantic.BaseModel subclass works. Pass it directly via parser=.
from pydantic import BaseModel, Field
from typing import Literal
class YesNoConfidence(BaseModel):
"""Answer yes/no and rate your confidence on a 1–5 scale."""
answer: Literal['yes', 'no'] = Field(..., description='Your yes/no answer')
confidence: Literal[1, 2, 3, 4, 5] = Field(..., description='1=guess, 5=certain')
card_custom = ExpCardInit()
card_custom.task_file = task_file
card_custom.parser = YesNoConfidence # custom Pydantic class
card_custom.cogtype = 'no'
card_custom.nsim = 1
card_custom.tunnel_status = '0'
card_custom.proj_dir = PROJECT_DIR / 'custom_parser'
card_custom.model = 'mock-chat-model'
card_custom.family = 'mock-llm'
# We can't actually run YesNoConfidence with mock-llm (mock doesn't do structured
# output), but ExpCard still validates and stores the parser.
exp_c = ExpCard(card_custom)
print('Resolved parser :', exp_c.parser.__name__)
print('Schema fields :', list(exp_c.parser.model_fields.keys()))
----<PROJECT AND DATA ROOT DIRECTORY>----
Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_modules_tutorial_runs/custom_parser
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_modules_tutorial_runs/custom_parser/DEFAULTPROJ/example_survey/mock-llm_mock-chat-model_SingleTurn
----<>----
Resolved parser : YesNoConfidence Schema fields : ['answer', 'confidence']
11. Mental model — when to use which import path¶
# Best for new code: top-level namespace, short and discoverable
from psychscanner.parsers import DefaultLiteralVivid15
# When you want to pick from a paradigm group:
from psychscanner.datasets.prompts.parser_tasks import DefaultLiteralVivid15
from psychscanner.datasets.prompts.parser_general import DefaultLiteralAgree
# Pre-existing imports continue to work unchanged:
from psychscanner.datasets.prompts.parser import DefaultLiteralVivid15
from psychscanner.datasets.prompts.parser_extra import DefaultLiteralAgree
Pick whichever path makes the import line read most naturally — the package treats them as equivalent.
12. End-to-end live run with parser="1"¶
Construction-time validation only proves the parser was resolved. The cell below actually runs the full pipeline — Ollama with a small instruct model, structured-output enforced by DefaultLiteralAgree (looked up by name from the task JSON via the registry), and the parsed rating field returned for every trial.
Requirements
- Ollama running locally (
ollama serve). - A small instruct model pulled, e.g.
ollama pull smollm2:360m-instruct-fp16orollama pull llama3.2:3b-instruct-fp16.
If you don't have Ollama, change family="ollama" and model="..." to whichever provider you do have configured (the rest of the cell stays identical).
import shutil
from pathlib import Path
from psychscanner import ExpCard, ExpCardInit, ScannerModel
E2E_DIR = Path.cwd() / '_parser_modules_tutorial_e2e'
if E2E_DIR.exists():
shutil.rmtree(E2E_DIR)
card_run = ExpCardInit()
card_run.proj_dir = E2E_DIR
card_run.projectname = 'e2e_run'
card_run.model = 'smollm2:360m-instruct-fp16' # any local Ollama model works
card_run.family = 'ollama'
card_run.parameters = {'temperature': 0}
card_run.task_file = task_file
card_run.parser = '1' # resolve via task JSON + registry
card_run.cogtype = 'no'
card_run.nsim = 1
card_run.tunnel_status = '0'
exp_run = ExpCard(card_run)
print(f'parser="1" resolved to: {exp_run.parser.__name__} (in {exp_run.parser.__module__.split(".")[-1]})')
print()
scanner = ScannerModel(expcard=exp_run)
results = scanner.run()
print(f'\nGot {len(results)} system-prompt iteration(s) x {len(results[0])} trials each.\n')
print('--- All trials ---')
for trial in results[0]:
raw = trial['pred_resp'].content if hasattr(trial['pred_resp'], 'content') else str(trial['pred_resp'])
stim = trial['stimulus'][:50] + ('...' if len(trial['stimulus']) > 50 else '')
print(f' {trial["trcode"]:6} -> {raw:20} ({stim})')
----<PROJECT AND DATA ROOT DIRECTORY>----
Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_modules_tutorial_e2e
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_modules_tutorial_e2e/e2e_run/example_survey/ollama_smollm2:360m-instruct-fp16_SingleTurn
----<>----
parser="1" resolved to: DefaultLiteralAgree (in parser_general) --<api key>-- warning: OLLAMA_API_KEY not set; proceeding without explicit api_key for family 'ollama'
--<chat model>-- model='smollm2:360m-instruct-fp16' temperature=0.0
2026-05-07 11:06:58.726 | CRITICAL | psychscanner.session_tunnel.session_tunnel:create_tunnel:152 - BEGIN
TOTAL RUNS: 1 RESUME IDX: None
----<>---- task running
0it [00:00, ?it/s]
1it [00:07, 7.98s/it]
2it [00:14, 7.32s/it]
3it [01:09, 28.86s/it]
4it [01:59, 37.40s/it]
5it [02:46, 40.62s/it]
6it [03:36, 43.87s/it]
6it [03:36, 36.06s/it]
2026-05-07 11:10:35.284 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 0
2026-05-07 11:10:35.326 | CRITICAL | psychscanner.session_tunnel.session_tunnel:end_checkpoint:163 - END
Got 1 system-prompt iteration(s) x 6 trials each.
--- All trials ---
O_1 -> {'rating': 5} (I enjoy trying new things and exploring new ideas.)
O_2 -> {'rating': 5} (I am comfortable with ambiguity and uncertainty.)
O_3 -> {'rating': 1} (I prefer routine and predictability.)
C_1 -> {'rating': 4} (I am organized and pay attention to details.)
C_2 -> {'rating': 5} (I follow through on my commitments.)
C_3 -> {'rating': 1} (I tend to procrastinate on important tasks.)
13. Parser x Memory: comparing SingleTurn vs Convo¶
The same parser produces different downstream behavior depending on the agent's memory mode.
memory="SingleTurn"-- every trial is an independent chat. The model doesn't see any prior trial.memory="Convo"withchain_type="task"-- LangGraph keeps a per-threadMemorySaver; all trials in the same task share one thread, so the model sees its own earlier answers as conversation history.memory="Convo"withchain_type="item"-- nothread_idis set per call, soConvois effectively not in use. Demonstrates a pitfall: turning onConvowithout settingchain_typeaccordingly does nothing.
Below we run the same example_survey.json (parser DefaultLiteralAgree) under two memory configurations and compare the parsed ratings side-by-side.
Caveat: with the current implementation, parsed responses are stringified and wrapped in an AIMessage before being put back into conversation history. So Convo mode shows the model JSON-like strings of its prior answers, not natural language. This is a known item on the list of parser-streamlining work; the cell below demonstrates the existing behavior.
import ast
import shutil
from pathlib import Path
from psychscanner import ExpCard, ExpCardInit, ScannerModel
MEM_DIR = Path.cwd() / '_parser_modules_tutorial_mem'
if MEM_DIR.exists():
shutil.rmtree(MEM_DIR)
def run_memory_config(memory, chain_type):
label = f'{memory}/{chain_type}'
card = ExpCardInit()
card.proj_dir = MEM_DIR / label.replace('/', '_')
card.projectname = label.replace('/', '_')
card.model = 'smollm2:360m-instruct-fp16'
card.family = 'ollama'
card.parameters = {'temperature': 0}
card.task_file = task_file
card.parser = '1'
card.cogtype = 'no'
card.nsim = 1
card.tunnel_status = '0'
card.memory = memory
card.chain_type = chain_type
exp = ExpCard(card)
scanner = ScannerModel(expcard=exp)
return scanner.run()[0] # first (only) system-prompt iteration
configs = [
('SingleTurn', 'item'), # baseline -- no memory at all
('Convo', 'task'), # full task-level conversation memory
]
all_results = {f'{m}/{c}': run_memory_config(m, c) for m, c in configs}
# Side-by-side comparison
labels = list(all_results.keys())
trcodes = [t['trcode'] for t in all_results[labels[0]]]
print()
print(f'{"trcode":7} {"stimulus":52} {labels[0]:>18} {labels[1]:>14}')
print('-' * 100)
for tc in trcodes:
row = []
for lbl in labels:
trial = next(t for t in all_results[lbl] if t['trcode'] == tc)
try:
parsed = ast.literal_eval(trial['pred_resp'].content)
row.append(str(parsed))
except Exception:
row.append(repr(trial['pred_resp'].content)[:18])
stim = next(t['stimulus'] for t in all_results[labels[0]] if t['trcode'] == tc)
stim_short = stim[:50] + ('..' if len(stim) > 50 else '')
print(f'{tc:7} {stim_short:52} {row[0]:>18} {row[1]:>14}')
----<PROJECT AND DATA ROOT DIRECTORY>----
Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_modules_tutorial_mem/SingleTurn_item
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_modules_tutorial_mem/SingleTurn_item/SingleTurn_item/example_survey/ollama_smollm2:360m-instruct-fp16_SingleTurn
----<>----
--<api key>-- warning: OLLAMA_API_KEY not set; proceeding without explicit api_key for family 'ollama'
--<chat model>-- model='smollm2:360m-instruct-fp16' temperature=0.0
2026-05-07 11:10:35.737 | CRITICAL | psychscanner.session_tunnel.session_tunnel:create_tunnel:152 - BEGIN
TOTAL RUNS: 1 RESUME IDX: None
----<>---- task running
0it [00:00, ?it/s]
1it [00:22, 22.91s/it]
2it [00:54, 28.27s/it]
3it [01:37, 34.85s/it]
4it [02:33, 43.22s/it]
5it [02:57, 36.18s/it]
6it [03:32, 35.93s/it]
6it [03:32, 35.47s/it]
2026-05-07 11:14:08.675 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 0
2026-05-07 11:14:08.706 | CRITICAL | psychscanner.session_tunnel.session_tunnel:end_checkpoint:163 - END
----<PROJECT AND DATA ROOT DIRECTORY>----
Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_modules_tutorial_mem/Convo_task
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_modules_tutorial_mem/Convo_task/Convo_task/example_survey/ollama_smollm2:360m-instruct-fp16_Convo
----<>----
--<api key>-- warning: OLLAMA_API_KEY not set; proceeding without explicit api_key for family 'ollama'
--<chat model>-- model='smollm2:360m-instruct-fp16' temperature=0.0
2026-05-07 11:14:09.004 | CRITICAL | psychscanner.session_tunnel.session_tunnel:create_tunnel:152 - BEGIN
TOTAL RUNS: 1 RESUME IDX: None
----<>---- task running
0it [00:00, ?it/s]
1it [00:31, 31.89s/it]
2it [01:28, 46.21s/it]
3it [02:02, 40.88s/it]
4it [02:34, 37.27s/it]
5it [03:16, 39.11s/it]
6it [03:59, 40.24s/it]
6it [03:59, 39.87s/it]
2026-05-07 11:18:08.319 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 0
2026-05-07 11:18:08.341 | CRITICAL | psychscanner.session_tunnel.session_tunnel:end_checkpoint:163 - END
trcode stimulus SingleTurn/item Convo/task
----------------------------------------------------------------------------------------------------
O_1 I enjoy trying new things and exploring new ideas. {'rating': 5} {'rating': 5}
O_2 I am comfortable with ambiguity and uncertainty. {'rating': 5} {'rating': 4}
O_3 I prefer routine and predictability. {'rating': 1} {'rating': 3}
C_1 I am organized and pay attention to details. {'rating': 4} {'rating': 4}
C_2 I follow through on my commitments. {'rating': 5} {'rating': 3}
C_3 I tend to procrastinate on important tasks. {'rating': 1} {'rating': 4}
Summary¶
parser_tasks= vividness + reality-monitoring (paradigm-specific).parser_general= Likert, response/rating, word-classification, readiness,TaskResponseUnion (paradigm-agnostic).parserandparser_extra= backward-compat shims; nothing breaks.psychscanner.parsers= single registry +list_parsers()+get_parser(name).parser="1"reads the class name from the task JSON's"parser"key, looks it up in the registry, and uses it. Misspellings produce a clear error with all available names listed.
All previously-shipped parser classes are unchanged — only file layout was streamlined.