Add browser asset quality gate
This commit is contained in:
parent
2841042b9c
commit
5ccb336e84
14 changed files with 2898 additions and 16 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
/.venv/
|
/.venv/
|
||||||
|
/node_modules/
|
||||||
/.ruff_cache/
|
/.ruff_cache/
|
||||||
/.pytest_cache/
|
/.pytest_cache/
|
||||||
/dist/
|
/dist/
|
||||||
|
|
|
||||||
6
.htmlvalidate.json
Normal file
6
.htmlvalidate.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"extends": ["html-validate:recommended"],
|
||||||
|
"rules": {
|
||||||
|
"valid-id": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,5 +12,5 @@
|
||||||
- Use deterministic ordering, hashes, JSON results, and structured errors.
|
- Use deterministic ordering, hashes, JSON results, and structured errors.
|
||||||
- Fail closed on stale caches, invalid configuration, ambiguous IDs, and unauthorized families.
|
- Fail closed on stale caches, invalid configuration, ambiguous IDs, and unauthorized families.
|
||||||
- Keep dependencies small and pinned by compatible major version.
|
- Keep dependencies small and pinned by compatible major version.
|
||||||
- Run strict `pyright`, formatting, Ruff, compilation, focused tests, and the complete warning-strict
|
- Run strict `pyright`, `npm run lint:web`, formatting, Ruff, compilation, focused tests, and the
|
||||||
test suite before closing a gate.
|
complete warning-strict test suite before closing a gate.
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,11 @@ virtual environment and treats a clean strict run as a required development gate
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync
|
uv sync
|
||||||
|
npm ci
|
||||||
pyright
|
pyright
|
||||||
uv run ruff check src tests
|
npm run lint:web
|
||||||
uv run ruff format --check src tests
|
uv run ruff check src tests tools
|
||||||
|
uv run ruff format --check src tests tools
|
||||||
uv run python -m unittest discover -s tests -v
|
uv run python -m unittest discover -s tests -v
|
||||||
uv run docforge --project-root tests/fixtures/alpha validate
|
uv run docforge --project-root tests/fixtures/alpha validate
|
||||||
uv run docforge --project-root tests/fixtures/alpha render-status manual
|
uv run docforge --project-root tests/fixtures/alpha render-status manual
|
||||||
|
|
@ -59,6 +61,9 @@ uv run docforge --project-root tests/fixtures/alpha render manual
|
||||||
uv run docforge-mcp --project-root tests/fixtures/alpha --proposal-writer alpha-editor
|
uv run docforge-mcp --project-root tests/fixtures/alpha --proposal-writer alpha-editor
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The web gate validates the exact graph-browser HTML, CSS, and JavaScript served by DocForge. It also
|
||||||
|
renders the fixture manual into a temporary project and validates that final HTML document.
|
||||||
|
|
||||||
The command prints deterministic JSON. Derived indexes live under each project's configured cache
|
The command prints deterministic JSON. Derived indexes live under each project's configured cache
|
||||||
directory and are never canonical input.
|
directory and are never canonical input.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,24 @@
|
||||||
# Completed slices
|
# Completed slices
|
||||||
|
|
||||||
|
## DFG-16 browser asset quality gate
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Added pinned ESLint, Stylelint, CSS-tree, and HTML Validate development tooling.
|
||||||
|
- Added one `npm run lint:web` gate that extracts the exact embedded viewer assets without writing
|
||||||
|
generated repository files.
|
||||||
|
- Validated a freshly rendered fixture manual in addition to the graph viewer.
|
||||||
|
- Corrected viewer landmark names, explicit input type, ARIA group semantics, inline legend styles,
|
||||||
|
and HTML doctype casing.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
- HTML Validate passes the served graph document and a freshly rendered manual.
|
||||||
|
- Stylelint and CSS-tree pass the embedded stylesheet with syntax and property-value validation.
|
||||||
|
- ESLint passes the embedded browser script with recommended browser rules and no inline disables.
|
||||||
|
- Strict Pyright, Ruff, formatting, compilation, all 52 warning-strict tests, dependency locks, and
|
||||||
|
diff checks pass.
|
||||||
|
|
||||||
## DFG-15 strict static typing gate
|
## DFG-15 strict static typing gate
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
23
eslint.config.mjs
Normal file
23
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
files: ["**/*.js"],
|
||||||
|
...js.configs.recommended,
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2024,
|
||||||
|
sourceType: "script",
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
linterOptions: {
|
||||||
|
reportUnusedDisableDirectives: "error",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
eqeqeq: "error",
|
||||||
|
"no-implicit-coercion": "error",
|
||||||
|
"no-var": "error",
|
||||||
|
"prefer-const": "error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
2659
package-lock.json
generated
Normal file
2659
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
18
package.json
Normal file
18
package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "docforge-web-quality",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "npm@10.9.7",
|
||||||
|
"scripts": {
|
||||||
|
"lint:web": "uv run python tools/check_web_assets.py"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "10.0.1",
|
||||||
|
"eslint": "10.8.0",
|
||||||
|
"globals": "17.7.0",
|
||||||
|
"html-validate": "11.5.6",
|
||||||
|
"stylelint": "17.14.1",
|
||||||
|
"stylelint-config-standard": "40.0.0",
|
||||||
|
"stylelint-csstree-validator": "4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,6 @@ select = ["E", "F", "I", "UP", "B", "SIM"]
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
pythonVersion = "3.12"
|
pythonVersion = "3.12"
|
||||||
typeCheckingMode = "strict"
|
typeCheckingMode = "strict"
|
||||||
include = ["src"]
|
include = ["src", "tools"]
|
||||||
venvPath = "."
|
venvPath = "."
|
||||||
venv = ".venv"
|
venv = ".venv"
|
||||||
|
|
|
||||||
|
|
@ -858,7 +858,7 @@ def _facet_rows(connection: sqlite3.Connection, table: str, column: str) -> list
|
||||||
return [{"value": row[0], "count": row[1]} for row in rows]
|
return [{"value": row[0], "count": row[1]} for row in rows]
|
||||||
|
|
||||||
|
|
||||||
_GRAPH_BROWSER_HTML = r"""<!doctype html>
|
_GRAPH_BROWSER_HTML = r"""<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
@ -963,6 +963,9 @@ _GRAPH_BROWSER_HTML = r"""<!doctype html>
|
||||||
display: flex; align-items: center; gap: 5px; color: var(--muted); font-size: 10px;
|
display: flex; align-items: center; gap: 5px; color: var(--muted); font-size: 10px;
|
||||||
}
|
}
|
||||||
.legend i { width: 8px; height: 8px; border-radius: 50%; }
|
.legend i { width: 8px; height: 8px; border-radius: 50%; }
|
||||||
|
.legend-primary i { background: var(--primary-stroke); }
|
||||||
|
.legend-child i { background: var(--child-stroke); }
|
||||||
|
.legend-edge i { background: var(--edge-stroke); }
|
||||||
.result {
|
.result {
|
||||||
width: 100%; text-align: left; border: 1px solid var(--line); border-radius: 9px;
|
width: 100%; text-align: left; border: 1px solid var(--line); border-radius: 9px;
|
||||||
padding: 9px; background: var(--panel-2); color: var(--text);
|
padding: 9px; background: var(--panel-2); color: var(--text);
|
||||||
|
|
@ -1097,11 +1100,12 @@ _GRAPH_BROWSER_HTML = r"""<!doctype html>
|
||||||
<div class="status" id="status">Loading validated index snapshot…</div>
|
<div class="status" id="status">Loading validated index snapshot…</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<aside class="left">
|
<aside class="left" aria-label="Graph navigation">
|
||||||
<form id="search-form">
|
<form id="search-form">
|
||||||
<label for="search">Find nodes</label>
|
<label for="search">Find nodes</label>
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<input id="search" name="q" autocomplete="off" placeholder="title, symbol, path…">
|
<input id="search" name="q" type="search" autocomplete="off"
|
||||||
|
placeholder="title, symbol, path…">
|
||||||
<button class="button" type="submit">Find</button>
|
<button class="button" type="submit">Find</button>
|
||||||
</div>
|
</div>
|
||||||
<label for="family">Family</label>
|
<label for="family">Family</label>
|
||||||
|
|
@ -1109,10 +1113,10 @@ _GRAPH_BROWSER_HTML = r"""<!doctype html>
|
||||||
</form>
|
</form>
|
||||||
<div class="results" id="results"></div>
|
<div class="results" id="results"></div>
|
||||||
<div class="neighborhood" id="neighborhood" hidden>
|
<div class="neighborhood" id="neighborhood" hidden>
|
||||||
<div class="legend" aria-label="Node role colors">
|
<div class="legend" role="group" aria-label="Node role colors">
|
||||||
<span><i style="background: var(--primary-stroke)"></i>Primary</span>
|
<span class="legend-primary"><i></i>Primary</span>
|
||||||
<span><i style="background: var(--child-stroke)"></i>Children</span>
|
<span class="legend-child"><i></i>Children</span>
|
||||||
<span><i style="background: var(--edge-stroke)"></i>Edge</span>
|
<span class="legend-edge"><i></i>Edge</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="neighborhood-sections"></div>
|
<div id="neighborhood-sections"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1121,7 +1125,7 @@ _GRAPH_BROWSER_HTML = r"""<!doctype html>
|
||||||
aria-label="Resize navigation panel" aria-orientation="vertical"
|
aria-label="Resize navigation panel" aria-orientation="vertical"
|
||||||
aria-valuemin="220" aria-valuemax="900" aria-valuenow="310"></div>
|
aria-valuemin="220" aria-valuemax="900" aria-valuenow="310"></div>
|
||||||
<main class="canvas">
|
<main class="canvas">
|
||||||
<div class="viewport-controls" aria-label="Graph viewport controls">
|
<div class="viewport-controls" role="group" aria-label="Graph viewport controls">
|
||||||
<button class="viewport-control" id="zoom-in" type="button"
|
<button class="viewport-control" id="zoom-in" type="button"
|
||||||
title="Zoom in" aria-label="Zoom in">+</button>
|
title="Zoom in" aria-label="Zoom in">+</button>
|
||||||
<button class="viewport-control" id="zoom-out" type="button"
|
<button class="viewport-control" id="zoom-out" type="button"
|
||||||
|
|
@ -1144,7 +1148,7 @@ _GRAPH_BROWSER_HTML = r"""<!doctype html>
|
||||||
<div class="panel-resizer" id="right-resizer" role="separator" tabindex="0"
|
<div class="panel-resizer" id="right-resizer" role="separator" tabindex="0"
|
||||||
aria-label="Resize details panel" aria-orientation="vertical"
|
aria-label="Resize details panel" aria-orientation="vertical"
|
||||||
aria-valuemin="240" aria-valuemax="900" aria-valuenow="350"></div>
|
aria-valuemin="240" aria-valuemax="900" aria-valuenow="350"></div>
|
||||||
<aside class="right">
|
<aside class="right" aria-label="Node details">
|
||||||
<div id="details">
|
<div id="details">
|
||||||
<p class="summary">Choose a search result to load its neighborhood.</p>
|
<p class="summary">Choose a search result to load its neighborhood.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
16
stylelint.config.mjs
Normal file
16
stylelint.config.mjs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
export default {
|
||||||
|
extends: ["stylelint-config-standard"],
|
||||||
|
plugins: ["stylelint-csstree-validator"],
|
||||||
|
rules: {
|
||||||
|
"csstree/validator": true,
|
||||||
|
"alpha-value-notation": null,
|
||||||
|
"at-rule-empty-line-before": null,
|
||||||
|
"color-function-alias-notation": null,
|
||||||
|
"color-function-notation": null,
|
||||||
|
"custom-property-empty-line-before": null,
|
||||||
|
"declaration-block-single-line-max-declarations": null,
|
||||||
|
"declaration-empty-line-before": null,
|
||||||
|
"media-feature-range-notation": null,
|
||||||
|
"rule-empty-line-before": null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
|
||||||
131
tools/check_web_assets.py
Normal file
131
tools/check_web_assets.py
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
"""Validate the exact embedded graph-browser HTML, CSS, and JavaScript."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from docforge.index import ProjectIndex
|
||||||
|
from docforge.project import Project
|
||||||
|
from docforge.rendering import RenderService
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SOURCE = ROOT / "src" / "docforge" / "visualization.py"
|
||||||
|
|
||||||
|
|
||||||
|
def _graph_browser_html() -> str:
|
||||||
|
tree = ast.parse(SOURCE.read_text(encoding="utf-8"), filename=str(SOURCE))
|
||||||
|
for statement in tree.body:
|
||||||
|
if not isinstance(statement, ast.Assign):
|
||||||
|
continue
|
||||||
|
if any(
|
||||||
|
isinstance(target, ast.Name) and target.id == "_GRAPH_BROWSER_HTML"
|
||||||
|
for target in statement.targets
|
||||||
|
):
|
||||||
|
value = ast.literal_eval(statement.value)
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value
|
||||||
|
raise RuntimeError("Could not find the literal graph-browser HTML asset")
|
||||||
|
|
||||||
|
|
||||||
|
def _embedded(html: str, tag: str) -> str:
|
||||||
|
opening = f"<{tag}>"
|
||||||
|
closing = f"</{tag}>"
|
||||||
|
if html.count(opening) != 1 or html.count(closing) != 1:
|
||||||
|
raise RuntimeError(f"Expected exactly one embedded {tag} asset")
|
||||||
|
return html.split(opening, 1)[1].split(closing, 1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _rendered_manual_html() -> str:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
fixture = ROOT / "tests" / "fixtures" / "alpha"
|
||||||
|
project_root = Path(directory) / "alpha"
|
||||||
|
shutil.copytree(fixture, project_root)
|
||||||
|
project = Project.open(project_root)
|
||||||
|
ProjectIndex(project).build()
|
||||||
|
result = RenderService(project).render("manual")
|
||||||
|
output = result.get("output")
|
||||||
|
if not isinstance(output, dict):
|
||||||
|
raise RuntimeError("Manual render did not return output identity")
|
||||||
|
relative_path = cast(dict[str, object], output).get("path")
|
||||||
|
if not isinstance(relative_path, str):
|
||||||
|
raise RuntimeError("Manual render did not return an output path")
|
||||||
|
return (project_root / relative_path).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _tool(name: str) -> str:
|
||||||
|
suffix = ".cmd" if os.name == "nt" else ""
|
||||||
|
path = ROOT / "node_modules" / ".bin" / f"{name}{suffix}"
|
||||||
|
if not path.is_file():
|
||||||
|
raise RuntimeError(f"Missing web validator {name}; run npm ci")
|
||||||
|
return str(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(label: str, command: list[str], content: str) -> bool:
|
||||||
|
print(f"Checking {label}...")
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
cwd=ROOT,
|
||||||
|
input=content,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
html = _graph_browser_html()
|
||||||
|
checks = (
|
||||||
|
(
|
||||||
|
"HTML",
|
||||||
|
[
|
||||||
|
_tool("html-validate"),
|
||||||
|
"--stdin",
|
||||||
|
"--stdin-filename=graph-browser.html",
|
||||||
|
"--max-warnings=0",
|
||||||
|
],
|
||||||
|
html,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"rendered manual HTML",
|
||||||
|
[
|
||||||
|
_tool("html-validate"),
|
||||||
|
"--stdin",
|
||||||
|
"--stdin-filename=rendered-manual.html",
|
||||||
|
"--max-warnings=0",
|
||||||
|
],
|
||||||
|
_rendered_manual_html(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"CSS",
|
||||||
|
[
|
||||||
|
_tool("stylelint"),
|
||||||
|
"--stdin",
|
||||||
|
"--stdin-filename=graph-browser.css",
|
||||||
|
"--max-warnings=0",
|
||||||
|
],
|
||||||
|
_embedded(html, "style"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"JavaScript",
|
||||||
|
[
|
||||||
|
_tool("eslint"),
|
||||||
|
"--stdin",
|
||||||
|
"--stdin-filename=graph-browser.js",
|
||||||
|
"--max-warnings=0",
|
||||||
|
],
|
||||||
|
_embedded(html, "script"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
results = [_run(label, command, content) for label, command, content in checks]
|
||||||
|
return 0 if all(results) else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Loading…
Add table
Add a link
Reference in a new issue