From f1fabaf0ca229c8da636d8b084ce5760a3a77bba Mon Sep 17 00:00:00 2001 From: Andraxion Date: Wed, 29 Jul 2026 12:38:25 -0400 Subject: [PATCH] Complete independent projection runtime --- Makefile | 20 +- eslint.config.mjs | 22 + package-lock.json | 88 ++ package.json | 6 +- playwright.accessibility.config.mjs | 24 + schemas/client-configuration.schema.json | 38 +- schemas/projection-policy.schema.json | 19 + schemas/result.schema.json | 3 + src/docforge/application.py | 21 +- src/docforge/assets/graph.html | 2 +- src/docforge/cli.py | 106 ++- src/docforge/client_config.py | 158 +++- src/docforge/doctor.py | 17 + src/docforge/graph_rendering.py | 30 +- src/docforge/manual_projection.py | 29 +- src/docforge/mcp_server.py | 109 ++- src/docforge/project.py | 1 - src/docforge/projection_fragments.py | 489 ++++++++++++ src/docforge/projection_policy.py | 234 ++++++ src/docforge/projection_worker.py | 436 ++++++++++ src/docforge/render_contract.py | 149 +++- src/docforge/rendering.py | 86 +- src/docforge/telemetry.py | 3 + src/docforge/viewer_manager.py | 21 +- src/docforge_renderers/graph.py | 2 +- src/docforge_renderers/manual.py | 152 +++- tests/accessibility.spec.mjs | 187 +++++ tests/test_client_integration.py | 265 ++++++ tests/test_onboarding.py | 23 + tests/test_projection_contract.py | 167 ++++ tests/test_projection_fragments.py | 205 +++++ tests/test_projection_policy.py | 266 +++++++ tests/test_projection_policy_integration.py | 239 ++++++ tests/test_projection_worker.py | 387 +++++++++ tests/test_public_contract.py | 19 + tests/test_rendering.py | 30 +- tools/accessibility_fixture.py | 99 +++ tools/milestone3_benchmark.py | 842 ++++++++++++++++++++ 38 files changed, 4907 insertions(+), 87 deletions(-) create mode 100644 playwright.accessibility.config.mjs create mode 100644 schemas/projection-policy.schema.json create mode 100644 src/docforge/projection_fragments.py create mode 100644 src/docforge/projection_policy.py create mode 100644 src/docforge/projection_worker.py create mode 100644 tests/accessibility.spec.mjs create mode 100644 tests/test_projection_fragments.py create mode 100644 tests/test_projection_policy.py create mode 100644 tests/test_projection_policy_integration.py create mode 100644 tests/test_projection_worker.py create mode 100644 tools/accessibility_fixture.py create mode 100644 tools/milestone3_benchmark.py diff --git a/Makefile b/Makefile index a3334df..64d52c8 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,10 @@ NPM := npm PYTHONPYCACHEPREFIX := /tmp/docforge-quality-pycache PYTEST_BASETEMP := /tmp/docforge-quality-pytest -.PHONY: benchmark benchmark-m1 benchmark-m1-smoke benchmark-m2 benchmark-m2-smoke benchmark-smoke build compile contract dependencies format-check gate lint lock test type +.PHONY: accessibility benchmark benchmark-m1 benchmark-m1-smoke benchmark-m2 benchmark-m2-smoke benchmark-m3 benchmark-m3-full benchmark-m3-smoke benchmark-smoke build compile contract dependencies format-check gate lint lock test type + +accessibility: + $(NPM) run test:accessibility format-check: $(PYTHON) -m ruff format --check src tests tools @@ -25,6 +28,10 @@ contract: -p no:cacheprovider --basetemp=$(PYTEST_BASETEMP) \ tests/test_public_contract.py \ tests/test_policy.py \ + tests/test_projection_policy.py \ + tests/test_projection_policy_integration.py \ + tests/test_projection_worker.py \ + tests/test_projection_fragments.py \ tests/test_retrieval.py \ tests/test_generation_diff.py \ tests/test_client_integration.py \ @@ -73,4 +80,13 @@ benchmark-m2-smoke: benchmark-m2: $(PYTHON) tools/milestone2_benchmark.py --nodes 1000 --samples 10 -gate: format-check lint type compile contract test lock dependencies build benchmark-smoke benchmark-m1-smoke benchmark-m2-smoke +benchmark-m3-smoke: + $(PYTHON) tools/milestone3_benchmark.py --mode smoke \ + --output /tmp/docforge-milestone3-smoke.json > /dev/null + +benchmark-m3: + $(PYTHON) tools/milestone3_benchmark.py --mode full + +benchmark-m3-full: benchmark-m3 + +gate: format-check lint type compile contract test accessibility lock dependencies build benchmark-smoke benchmark-m1-smoke benchmark-m2-smoke benchmark-m3-smoke diff --git a/eslint.config.mjs b/eslint.config.mjs index 0de1999..5fc5cd0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -21,4 +21,26 @@ export default [ "prefer-const": "error", }, }, + { + files: ["playwright.accessibility.config.mjs", "tests/accessibility.spec.mjs"], + ...js.configs.recommended, + languageOptions: { + ecmaVersion: 2024, + sourceType: "module", + globals: { + ...globals.browser, + ...globals.node, + }, + }, + linterOptions: { + reportUnusedDisableDirectives: "error", + }, + rules: { + ...js.configs.recommended.rules, + eqeqeq: "error", + "no-implicit-coercion": "error", + "no-var": "error", + "prefer-const": "error", + }, + }, ]; diff --git a/package-lock.json b/package-lock.json index d61baea..62e1957 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "docforge-web-quality", "version": "0.0.0", "devDependencies": { + "@axe-core/playwright": "4.12.1", "@eslint/js": "10.0.1", + "@playwright/test": "1.62.0", "eslint": "10.8.0", "globals": "17.7.0", "html-validate": "11.5.6", @@ -18,6 +20,19 @@ "stylelint-csstree-validator": "4.0.0" } }, + "node_modules/@axe-core/playwright": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.12.1.tgz", + "integrity": "sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.12.1" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -515,6 +530,22 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0.tgz", + "integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", @@ -635,6 +666,16 @@ "node": ">=8" } }, + "node_modules/axe-core": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1943,6 +1984,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", + "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", + "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", diff --git a/package.json b/package.json index edad8d0..2e654a8 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,14 @@ "private": true, "packageManager": "npm@10.9.7", "scripts": { - "lint:web": "uv run python tools/check_web_assets.py" + "install:accessibility-browser": "playwright install chromium", + "lint:web": "uv run python tools/check_web_assets.py && eslint --max-warnings=0 playwright.accessibility.config.mjs tests/accessibility.spec.mjs", + "test:accessibility": "npm run install:accessibility-browser && playwright test --config=playwright.accessibility.config.mjs" }, "devDependencies": { + "@axe-core/playwright": "4.12.1", "@eslint/js": "10.0.1", + "@playwright/test": "1.62.0", "eslint": "10.8.0", "globals": "17.7.0", "html-validate": "11.5.6", diff --git a/playwright.accessibility.config.mjs b/playwright.accessibility.config.mjs new file mode 100644 index 0000000..7a7c340 --- /dev/null +++ b/playwright.accessibility.config.mjs @@ -0,0 +1,24 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests", + testMatch: "accessibility.spec.mjs", + fullyParallel: false, + workers: 1, + retries: 0, + reporter: "line", + outputDir: "/tmp/docforge-playwright-accessibility", + timeout: 30_000, + expect: { + timeout: 5_000, + }, + use: { + browserName: "chromium", + bypassCSP: true, + headless: true, + viewport: { + width: 1440, + height: 1000, + }, + }, +}); diff --git a/schemas/client-configuration.schema.json b/schemas/client-configuration.schema.json index b4cf208..1cf7c6a 100644 --- a/schemas/client-configuration.schema.json +++ b/schemas/client-configuration.schema.json @@ -127,6 +127,17 @@ }, "additionalProperties": false }, + "projection_policy": { + "type": "object", + "required": ["schema_version", "manual", "portable_graph", "live_viewer"], + "properties": { + "schema_version": { "const": 2 }, + "manual": { "enum": ["auto", "explicit", "disabled"] }, + "portable_graph": { "enum": ["explicit", "disabled"] }, + "live_viewer": { "enum": ["on-demand", "disabled"] } + }, + "additionalProperties": false + }, "diagnostics": { "type": "object", "required": [ @@ -211,6 +222,9 @@ "project", "binding", "effective_policy", + "projection_policy", + "projection_policy_hash", + "projection_availability", "artifact", "configuration_hash", "warnings" @@ -231,7 +245,8 @@ "project_id", "project_root", "project_root_fingerprint", - "adapter" + "adapter", + "descriptor_hash" ], "properties": { "project_id": { "type": "string", "minLength": 1 }, @@ -240,7 +255,8 @@ "type": "string", "pattern": "^[0-9a-f]{16}$" }, - "adapter": { "type": "string", "minLength": 1 } + "adapter": { "type": "string", "minLength": 1 }, + "descriptor_hash": { "$ref": "#/$defs/sha256" } }, "additionalProperties": false }, @@ -317,6 +333,24 @@ "additionalProperties": false }, "effective_policy": { "$ref": "#/$defs/effective_policy" }, + "projection_policy": { "$ref": "#/$defs/projection_policy" }, + "projection_policy_hash": { "$ref": "#/$defs/sha256" }, + "projection_availability": { + "type": "object", + "required": [ + "manual_configured", + "portable_graph_configured", + "application_enabled", + "live_viewer_available" + ], + "properties": { + "manual_configured": { "type": "boolean" }, + "portable_graph_configured": { "type": "boolean" }, + "application_enabled": { "type": "boolean" }, + "live_viewer_available": { "const": true } + }, + "additionalProperties": false + }, "artifact": { "type": "object", "required": [ diff --git a/schemas/projection-policy.schema.json b/schemas/projection-policy.schema.json new file mode 100644 index 0000000..9186721 --- /dev/null +++ b/schemas/projection-policy.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://docforge.local/schema/projection-policy-v2.json", + "title": "DocForge independent projection policy", + "type": "object", + "required": [ + "schema_version", + "manual", + "portable_graph", + "live_viewer" + ], + "properties": { + "schema_version": { "const": 2 }, + "manual": { "enum": ["auto", "explicit", "disabled"] }, + "portable_graph": { "enum": ["explicit", "disabled"] }, + "live_viewer": { "enum": ["on-demand", "disabled"] } + }, + "additionalProperties": false +} diff --git a/schemas/result.schema.json b/schemas/result.schema.json index ae0ea04..31cc087 100644 --- a/schemas/result.schema.json +++ b/schemas/result.schema.json @@ -20,6 +20,7 @@ "test", "benchmark.m1", "benchmark.m2", + "benchmark.m3", "mcp.invoke", "mcp.bootstrap", "mcp.sync", @@ -37,6 +38,8 @@ "mcp.generation_diff", "mcp.validate_project", "mcp.render_status", + "mcp.graph_plan", + "mcp.graph_render_status", "mcp.visualize", "mcp.visualization_status", "mcp.stop_visualization", diff --git a/src/docforge/application.py b/src/docforge/application.py index bb52c0a..3efb2b1 100644 --- a/src/docforge/application.py +++ b/src/docforge/application.py @@ -15,6 +15,7 @@ from .changesets import ChangesetStore from .errors import DocForgeError from .index import ProjectIndex from .models import Node, ProjectService, ProjectSnapshot +from .projection_policy import ManualProjectionMode, validate_manual_projection_mode from .rendering import RenderService @@ -353,13 +354,19 @@ class CanonicalApplicationService: applier_id: str | None, applier: CanonicalApplier | None, index: ProjectIndex | None = None, + manual_policy: ManualProjectionMode = "auto", ) -> None: self.project = project self.applier_id = applier_id self.applier = applier self.changesets = ChangesetStore(project, applier_id) self.index = index or ProjectIndex(project) - self.rendering = RenderService(project, self.changesets) + self.manual_policy = validate_manual_projection_mode(manual_policy) + self.rendering = RenderService( + project, + self.changesets, + manual_policy=self.manual_policy, + ) @property def enabled(self) -> bool: @@ -402,7 +409,9 @@ class CanonicalApplicationService: ) renders: list[dict[str, object]] = [] config = self.project.descriptor.render - if config is not None: + render_action = "not_configured" + if config is not None and self.manual_policy == "auto": + render_action = "rendered" for view in config.views: try: rendered = self.rendering.render(view.view_id) @@ -435,6 +444,10 @@ class CanonicalApplicationService: "error": error.as_dict(), } ) + elif config is not None: + render_action = ( + "skipped_explicit" if self.manual_policy == "explicit" else "skipped_disabled" + ) return { **applied, "derived_refresh": { @@ -442,6 +455,10 @@ class CanonicalApplicationService: "index": index_result, "check": index_check, "renders": renders, + "render_policy": { + "mode": self.manual_policy, + "action": render_action, + }, "errors": refresh_errors, }, } diff --git a/src/docforge/assets/graph.html b/src/docforge/assets/graph.html index be58d3d..2f3ed8c 100644 --- a/src/docforge/assets/graph.html +++ b/src/docforge/assets/graph.html @@ -95,7 +95,7 @@ aria-label="Visible relationship color and symbol key"> + role="group" aria-label="Interactive node neighborhood">
Search for a node to inspect its neighborhood.