Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b3e8c56
chore: add shared project and test scaffolding
jpagh Apr 30, 2026
627e803
feat: make formatter Jinja-aware
jpagh Apr 30, 2026
7ff2864
feat: add structured YAML validation reporting
jpagh Apr 30, 2026
704d540
feat: add dayaml command wrapper
jpagh Apr 30, 2026
9c24c22
fix: allow code only dynamic fields without a label
jpagh Apr 30, 2026
fa8600d
chore: added/updated tests to get full coverage
jpagh Apr 30, 2026
05391ff
feat: accessibility checks are now coded warnings, W5xx, not errors, …
jpagh Apr 30, 2026
01538fd
fix: clarify exception handling for YAML parsing in format_yaml_string
jpagh Apr 30, 2026
b7d6167
fix: improve summary and template for fields dict message definition
jpagh Apr 30, 2026
ceb0afa
fix: add UTF-8 encoding to file reading for better compatibility
jpagh Apr 30, 2026
fcecde6
feat: make url checking async
jpagh Apr 30, 2026
fdcb961
fix: convert wcag accessibility warnings to be errors
jpagh May 1, 2026
e30c844
feat: add support for ignoring specific diagnostic codes in processing
jpagh May 2, 2026
726933e
feat: add `tool.dayaml` pyproject config options
jpagh May 2, 2026
7c061b0
fix: single entry in fields without proper listing, W405
jpagh May 2, 2026
276a320
fix: convert all warnings to errors unless url related
jpagh May 2, 2026
17944b9
feat: add tests for message severity and YAML project config handling
jpagh May 2, 2026
87f50a8
feat: enhance progress reporting with dot and line outputs in formatt…
jpagh May 2, 2026
2c9d559
feat: add --format-on-success option to check command for automatic f…
jpagh May 2, 2026
e8ee871
fix: treat `dayaml check` and `dayamlchecker` as aliases, same with `…
jpagh May 2, 2026
bb9cb17
feat: add validation for object field choices to prevent nested code …
jpagh May 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ htmlcov/
.coverage.*
.cache
nosetests.xml
coverage.xml
coverage.*
*.cover
*.py,cover
.hypothesis/
Expand Down Expand Up @@ -159,6 +159,9 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# VSCode
.vscode/

# uv
uv.lock

Expand Down
122 changes: 119 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,133 @@ An LSP for Docassemble YAML Interviews

```bash
pip install .
python3 -m dayamlchecker `find . -name "*.yml" -path "*/questions/*" snot -path "*/.venv/*" -not -path "*/build/*"` # i.e. a space separated list of files
dayaml check # defaults to the current project; usually scans ./docassemble
dayaml format # defaults to the current project; usually scans ./docassemble
dayaml check path/to/yaml-or-dir
dayaml check --show-experimental path/to/yaml-or-dir
dayaml check --ignore-codes E410,E301 path/to/yaml-or-dir
dayaml check --format-on-success --no-url-check path/to/yaml-or-dir
dayaml format path/to/interview.yml

# Backwards-compatible entry points
python3 -m dayamlchecker # defaults to the current project; usually scans ./docassemble
dayamlchecker # defaults to the current project; usually scans ./docassemble
python3 -m dayamlchecker `find . -name "*.yml" -path "*/questions/*" -not -path "*/.venv/*" -not -path "*/build/*"` # i.e. a space separated list of files
dayamlchecker `find . -name "*.yml" -path "*/questions/*" -not -path "*/.venv/*" -not -path "*/build/*"`
dayamlchecker-fmt path/to/interview.yml
```

## Message Codes

Validation output now includes stable message codes in the style of tools like pylint:

```text
[E301] At interview.yml:12: Keys that shouldn't exist! ['not_a_real_key']
```

Use `dayaml check --show-experimental ...` to include the legacy `REAL ERROR:`
prefix for non-experimental errors.

Use `dayaml check --ignore-codes E410,E301 ...` to suppress specific
diagnostic codes when you need to waive known findings.

`dayaml` also reads optional project settings from `pyproject.toml`:

```toml
[tool.dayaml]
ignore_codes = ["E503", "E410"]
yaml_path = "docassemble"
args = ["--no-url-check"]
```

When you pass a project root that contains `pyproject.toml`, `dayaml` scans the
configured `yaml_path` relative to that file. If `yaml_path` is omitted, it
defaults to `docassemble`.

When you omit file arguments, `dayaml check`, `dayaml format`, `dayamlchecker`,
and `dayamlchecker-fmt` all start from the current working directory. If that
directory contains a `pyproject.toml` (or is inside a project that does), they
then resolve `tool.dayaml.yaml_path` from the nearest project config. In the
common case where `yaml_path` is not set, the practical result is that running
these commands from a project root will scan `./docassemble`.

`tool.dayaml.args` lets you set checker CLI defaults in the project config.
Those args are applied before the actual command-line args, so an explicit CLI
flag still wins. For example, `args = ["--no-url-check"]` disables URL checks
by default, while `dayaml check --url-check ...` turns them back on for one run.

`dayaml check --format-on-success ...` validates each file first and then runs
the formatter on files that have no error-severity findings after ignore-code
filtering. This uses the already-read file content in memory, so it avoids a
separate checker-then-formatter pass over the same file. Formatting happens
before the later URL-check phase, so a run can still exit nonzero for URL
errors after formatting changes have already been written. Use
`--no-url-check` with this mode if you want the combined YAML-check-and-format
behavior without the later repository URL scan.

### Real Errors

| Code | Meaning |
| --- | --- |
| `E101` | Duplicate YAML key |
| `E102` | YAML parsing error |
| `E103` | Value should be a YAML string |
| `E111` | Invalid Mako syntax |
| `E112` | Mako compile error |
| `E121` | Python code block must be a YAML string |
| `E122` | Python syntax error |
| `E201` | Jinja2 syntax error |
| `E202` | Jinja2 template error |
| `E203` | JavaScript modifier must be a string |
| `E204` | Invalid JavaScript syntax |
| `E205` | JavaScript modifier must contain at least one `val()` call |
| `E206` | `val()` references a field not defined on this screen |
| `E207` | `val()` argument must be a quoted string literal |
| `E301` | Unknown YAML keys |
| `E302` | Malformed `show if` shorthand |
| `E303` | `show if: code` must be a YAML string |
| `E304` | `show if: code` has a Python syntax error |
| `E305` | `show if` dict must include `variable` or `code` |
| `E306` | No recognized block type found |
| `E307` | Block matches too many exclusive interview types |
| `E308` | Interview-order block is missing a matching show/hide guard |
| `E309` | Show/hide visibility logic is nested too deeply |
| `E401` | Python variable reference must be a YAML string |
| `E402` | Python variable reference cannot contain whitespace |
| `E403` | `objects` block must be a list or dict |
| `E404` | `fields: code` must be a YAML string |
| `E405` | Bare `fields` dict has no recognized field or `code` key |
| `E406` | `fields` must be a list or dict |
| `E407` | Field modifier `variable` must be a string |
| `E408` | Field modifier `variable` references a field not defined on this screen |
| `E409` | Field modifier `code` contains another validation error |
| `E410` | `show if: code` references a variable defined on the same screen |
| `E411` | Field modifier dict must include `variable` or `code` |
| `E412` | Field modifier shorthand references a field not defined on this screen |
| `E501` | Combobox widget is not accessible |
| `E502` | Field label is missing on a multi-field screen |
| `E503` | DOCX attachment is missing `tagged pdf` |
| `E504` | Bootstrap theme CSS has low contrast |
| `E505` | Image is missing alt text |
| `E506` | Markdown heading levels skip |
| `E507` | HTML heading levels skip |
| `E508` | Link has no accessible text |
| `E509` | Link text is too generic |

### Conventions

| Code | Meaning |
| --- | --- |
| `C101` | `validation code` should call `validation_error()` |

## WCAG checks

The checker includes WCAG-style checks for clear static accessibility failures in interview source. These checks run by default; use `--no-wcag` to disable them.

```bash
python3 -m dayamlchecker path/to/interview.yml # WCAG checks on (default)
python3 -m dayamlchecker --no-wcag path/to/interview.yml # WCAG checks off
python3 -m dayamlchecker --accessibility-error-on-widget combobox path/to/interview.yml # opt into combobox failures
python3 -m dayamlchecker --accessibility-error-on-widget combobox path/to/interview.yml # opt into combobox accessibility errors
```

Some accessibility checks are behind runtime options while the rules are still being evaluated. Right now `combobox` failures are default-off and can be enabled with `--accessibility-error-on-widget combobox`.
Expand All @@ -41,7 +157,7 @@ Optional runtime-gated accessibility checks:

- `combobox` usage, including `datatype: combobox` when `--accessibility-error-on-widget combobox` is enabled

Accessibility informational notes are also emitted for likely PDF accessibility issues:
Accessibility errors are also emitted for likely PDF accessibility issues:

- DOCX attachments missing `tagged pdf: True` (set this in `features` or on the attachment)

Expand Down
42 changes: 34 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"esprima>=4.0.1",
"jinja2>=3.1.6",
"mako>=1.3.10",
"black>=24.0.0",
"ruamel.yaml>=0.18.0",
Expand All @@ -21,21 +22,46 @@ authors = [
{ name="Bryce Willey", email="[email protected]" },
]

[tool.setuptools]
package-dir = { "" = "src" }
[dependency-groups]
dev = [
"mypy>=1.19.1",
"pytest>=9.0.2",
"pytest-cov>=7.1.0",
"pytest-xdist>=3.8.0",
"types-requests>=2.32.0",
]

[tool.setuptools.package-data]
dayamlchecker = ["py.typed"]
[project.scripts]
dayaml = "dayamlchecker.cli:main"
dayamlchecker = "dayamlchecker.yaml_structure:main"
dayamlchecker-fmt = "dayamlchecker.code_formatter:main"

[build-system]
requires = ["setuptools >= 77.0.3"]
build-backend = "setuptools.build_meta"

[project.scripts]
dayamlchecker = "dayamlchecker.yaml_structure:main"
dayamlchecker-fmt = "dayamlchecker.code_formatter:main"

[tool.mypy]
mypy_path = "src"
exclude = ["^build/"]
explicit_package_bases = true

[tool.setuptools]
package-dir = { "" = "src" }

[tool.setuptools.package-data]
dayamlchecker = ["py.typed"]

[tool.coverage.run]
branch = true
omit = [
"tests/*",
]
relative_files = true
source = [
"src",
]

[tool.coverage.report]
omit = [
"tests/*",
]
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
esprima>=4.0.1
jinja2>=3.0
mako>=1.3.10
black>=24.0.0
ruamel.yaml>=0.18.0
Expand Down
5 changes: 2 additions & 3 deletions src/dayamlchecker/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from .yaml_structure import main

import sys

sys.exit(main())
if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())
Loading