Compare commits
2 commits
eb9355b003
...
4c5773c865
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c5773c865 | |||
| fb0df5e4a1 |
23 changed files with 6956 additions and 19 deletions
|
|
@ -6,11 +6,14 @@ Goal: Let one project-bound server return compact, task-shaped, explainable cont
|
||||||
In scope: Capability modes; capability-aware bootstrap; versioned retrieval plans and context capsules; task-shaped context; generation diffs; evidence-gap diagnostics; generated client configuration; doctor checks.
|
In scope: Capability modes; capability-aware bootstrap; versioned retrieval plans and context capsules; task-shaped context; generation diffs; evidence-gap diagnostics; generated client configuration; doctor checks.
|
||||||
Out of scope: Independent render-plan packages; adapter SDK expansion; self-hosting; storage replacement; embeddings; WorldForge or ScrapeStation changes; production MCP repointing; tags and releases.
|
Out of scope: Independent render-plan packages; adapter SDK expansion; self-hosting; storage replacement; embeddings; WorldForge or ScrapeStation changes; production MCP repointing; tags and releases.
|
||||||
Done when: Policy and capabilities are explicit; bootstrap recommends only available actions; task context is compact, deterministic, provenance-bearing, and bounded; generation and evidence gaps are explainable; generated configuration and doctor checks are safe and tested; the complete repository gate and Milestone 2 benchmark pass.
|
Done when: Policy and capabilities are explicit; bootstrap recommends only available actions; task context is compact, deterministic, provenance-bearing, and bounded; generation and evidence gaps are explainable; generated configuration and doctor checks are safe and tested; the complete repository gate and Milestone 2 benchmark pass.
|
||||||
Status: Active. Effective policy and versioned task retrieval are committed. The latest-generation
|
Status: Complete. Effective policy, versioned task retrieval, latest-generation diff receipts,
|
||||||
diff receipt is implemented with focused contract, failure, CLI, MCP, legacy-adapter, no-AST, and
|
logarithmic bounded page packing, deterministic client configuration, and the read-only integration
|
||||||
zero-work tests. The full repository gate passes with 176 tests and 113 subtests. Independent
|
doctor are implemented and contract-tested. The complete repository gate passes with 205 tests and
|
||||||
publication, contract, and performance audits approve the hardened tree for commit. Generated
|
120 subtests. Three independent adversarial audits found no remaining implementation blocker. The
|
||||||
client configuration and doctor checks follow.
|
clean 1,000-node baseline is recorded against candidate commit
|
||||||
|
`fb0df5e4a1c591c2a84788fd4814d98550f11863`, including task/generation reconstruction,
|
||||||
|
response-size behavior, zero-hidden-work counters, and isolated memory. No tag or release was
|
||||||
|
created, no production integration was repointed, and self-hosting remains out of scope.
|
||||||
```
|
```
|
||||||
|
|
||||||
Milestones 3–5 remain directional context and are not active.
|
Milestones 3–5 remain directional context and are not active.
|
||||||
|
|
|
||||||
|
|
@ -523,3 +523,90 @@ fell from 272.06 ms p95 to 56.617 ms p95, while full traversal fell from roughly
|
||||||
203.55 ms p95. Regression tests require one receipt validation and at most 15 response encodes for
|
203.55 ms p95. Regression tests require one receipt validation and at most 15 response encodes for
|
||||||
1,000 page candidates. Final independent publication, contract, and performance audits approve
|
1,000 page candidates. Final independent publication, contract, and performance audits approve
|
||||||
the slice for commit.
|
the slice for commit.
|
||||||
|
|
||||||
|
### Deterministic client configuration and read-only doctor
|
||||||
|
|
||||||
|
Client integration remains an explicit machine-local boundary rather than canonical project
|
||||||
|
content. `docforge configure {codex,claude,openclaw} --project PATH` previews a deterministic
|
||||||
|
version-1 fragment by default. An optional output path publishes only a standalone fragment into
|
||||||
|
an existing real directory. Publication is create-only, private-mode, no-follow, bounded, and
|
||||||
|
conflict-aware. Existing differing client configuration is never merged, replaced, or silently
|
||||||
|
overwritten.
|
||||||
|
|
||||||
|
Generated commands use the exact current virtual-environment Python executable with isolated
|
||||||
|
module startup. The binding records explicit read, proposal, or application mode, no-AST policy,
|
||||||
|
render policy, empty environment, and bounded timeouts. Proposal and application generation fail
|
||||||
|
closed unless the descriptor declares the required writer and matching applier identity. A generic
|
||||||
|
CLI cannot reconstruct project-owned adapter composition, so custom adapters return an explicit
|
||||||
|
unavailable result instead of generating a misleading command.
|
||||||
|
|
||||||
|
Codex and OpenClaw fragments include their verified timeout fields. Claude JSON fragment syntax is
|
||||||
|
supported, while its timeout representation remains an explicit warning. The configuration result
|
||||||
|
has a strict JSON schema and canonical plan hash. Diagnostics are additive and remain disabled by
|
||||||
|
default.
|
||||||
|
|
||||||
|
`docforge doctor --client CLIENT` performs bounded, non-mutating inspection only. It reads the
|
||||||
|
project descriptor and selected client file through stable, directory-bound, no-follow handles;
|
||||||
|
parses at most 1 MiB and 256 server entries; selects at most one exact project binding; validates
|
||||||
|
the closed server argument set; checks executable, capability, declared authority, no-AST,
|
||||||
|
timeouts, environment-key names, and tool-filter presence; and performs only a stat-level index
|
||||||
|
presence check. It never loads canonical sources, opens SQLite, starts MCP, executes the configured
|
||||||
|
command, synchronizes, builds, renders, starts a viewer, or writes client configuration.
|
||||||
|
|
||||||
|
Doctor reports healthy, degraded, or unhealthy with stable process exit codes 0, 1, and 2. Secret
|
||||||
|
environment values are parsed only to enforce bounded string limits and are never returned.
|
||||||
|
Unknown or unverified client tool filtering, Claude timeout representation, implicit legacy
|
||||||
|
capability mode, shadowed authority, and missing disposable indexes are warnings. Unsafe paths,
|
||||||
|
malformed matching entries, unexpected executables, wrong project roots, invalid authorities, and
|
||||||
|
missing configuration are failures.
|
||||||
|
|
||||||
|
The first benchmark smoke failed for the correct product reason: its disposable doctor fragment
|
||||||
|
used the shared path `/tmp/doctor-codex.toml`, where a previous run had left different content. The
|
||||||
|
harness now creates a project subdirectory inside one unique temporary root and places the client
|
||||||
|
fragment beside it. This preserves create-only conflict safety and makes every run disposable.
|
||||||
|
|
||||||
|
The final pre-commit 1,000-node audit sample passes every provisional Milestone 2 gate. Task
|
||||||
|
context reconstructs 1,000 candidates as 108 cited evidence records and 892 explicit bounded
|
||||||
|
omissions across 11 pages in 703.808 ms. Generation diff reconstructs 1,000 changed details across
|
||||||
|
10 pages in 427.450 ms. Maximum pages remain below the 200,000-byte MCP budget; generation diff
|
||||||
|
uses 199,566 bytes and proves that diagnostics are discarded before the primary result. Isolated
|
||||||
|
peak RSS is 86,168 KiB.
|
||||||
|
|
||||||
|
Configuration preview now includes a bounded real import probe of the exact isolated interpreter,
|
||||||
|
so its provisional single-sample latency is about 315 ms rather than the earlier sub-millisecond
|
||||||
|
derivation-only figure. Doctor remains below 1 ms on generated disposable configurations.
|
||||||
|
Every configuration and doctor hidden-work counter is zero.
|
||||||
|
|
||||||
|
The aggregate `make gate` includes the Milestone 2 smoke benchmark. The frozen candidate passes
|
||||||
|
205 tests and 120 schema subtests, strict warnings, Ruff, formatting, Pyright, web checks,
|
||||||
|
compilation, lock and dependency checks, package builds, and all three milestone smoke benchmarks.
|
||||||
|
Three independent final audits approve client publication and policy binding, doctor fail-closed
|
||||||
|
behavior, and benchmark/contract coverage. Clean-revision benchmark evidence is still required
|
||||||
|
before closeout.
|
||||||
|
|
||||||
|
### Milestone 2 closeout
|
||||||
|
|
||||||
|
Candidate commit `fb0df5e4a1c591c2a84788fd4814d98550f11863` passed the clean ten-sample
|
||||||
|
Milestone 2 benchmark. Task-context complete traversal measured 703.561 ms median and 721.847 ms
|
||||||
|
p95 across 11 bounded pages. It reconstructed the exact 1,000-candidate collection from 108 cited
|
||||||
|
evidence records, 891 original token-budget omissions, and one hash-attested response-limit
|
||||||
|
surrogate. Generation-diff complete traversal measured 418.607 ms median and 425.315 ms p95 across
|
||||||
|
10 pages.
|
||||||
|
|
||||||
|
Read and no-AST bootstrap remained below 10 ms p95. The maximum generation page used 199,566 bytes
|
||||||
|
of the 200,000-byte budget and correctly discarded diagnostics before primary evidence.
|
||||||
|
Configuration preview measured about 314 ms median and 365 ms p95 because it proves the real
|
||||||
|
isolated interpreter import on every invocation. Codex and OpenClaw doctor checks remained below
|
||||||
|
0.6 ms p95; Claude remained explicitly degraded because its timeout format is unverified.
|
||||||
|
Isolated-process peak RSS was 86,448 KiB against the 262,144 KiB gate.
|
||||||
|
|
||||||
|
All measured configuration and doctor counters were zero. Task-context pages performed one index
|
||||||
|
check and two cheap generation checks with no loads, parses, synchronization, builds, extraction,
|
||||||
|
rendering, or viewer work. Generation-diff pages performed two cheap generation checks and no
|
||||||
|
index check. The canonical machine-readable result is
|
||||||
|
`benchmarks/milestone2-2026-07-29.json`.
|
||||||
|
|
||||||
|
Milestone 2 is complete. Follow-up ideas stay explicitly later-scope: avoid recomputing the
|
||||||
|
task-shaped capsule for every continuation page, add authenticated continuation when the threat
|
||||||
|
model requires it, verify a native Claude timeout representation, and introduce adapter-owned
|
||||||
|
launcher metadata before generating configurations for custom adapters.
|
||||||
|
|
|
||||||
16
Makefile
16
Makefile
|
|
@ -5,7 +5,7 @@ NPM := npm
|
||||||
PYTHONPYCACHEPREFIX := /tmp/docforge-quality-pycache
|
PYTHONPYCACHEPREFIX := /tmp/docforge-quality-pycache
|
||||||
PYTEST_BASETEMP := /tmp/docforge-quality-pytest
|
PYTEST_BASETEMP := /tmp/docforge-quality-pytest
|
||||||
|
|
||||||
.PHONY: benchmark benchmark-m1 benchmark-m1-smoke benchmark-smoke build compile contract dependencies format-check gate lint lock test type
|
.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
|
||||||
|
|
||||||
format-check:
|
format-check:
|
||||||
$(PYTHON) -m ruff format --check src tests tools
|
$(PYTHON) -m ruff format --check src tests tools
|
||||||
|
|
@ -24,6 +24,11 @@ contract:
|
||||||
PYTHONPYCACHEPREFIX=$(PYTHONPYCACHEPREFIX) $(PYTHON) -m pytest -q \
|
PYTHONPYCACHEPREFIX=$(PYTHONPYCACHEPREFIX) $(PYTHON) -m pytest -q \
|
||||||
-p no:cacheprovider --basetemp=$(PYTEST_BASETEMP) \
|
-p no:cacheprovider --basetemp=$(PYTEST_BASETEMP) \
|
||||||
tests/test_public_contract.py \
|
tests/test_public_contract.py \
|
||||||
|
tests/test_policy.py \
|
||||||
|
tests/test_retrieval.py \
|
||||||
|
tests/test_generation_diff.py \
|
||||||
|
tests/test_client_integration.py \
|
||||||
|
tests/test_observability.py::TelemetryContractTests::test_schema_fixed_names_match_the_implementation \
|
||||||
tests/test_adapter_contract.py::AdapterContractTests::test_no_ast_index_policy_rejects_logic_publication \
|
tests/test_adapter_contract.py::AdapterContractTests::test_no_ast_index_policy_rejects_logic_publication \
|
||||||
tests/test_adapter_contract.py::AdapterContractTests::test_no_ast_index_accepts_legacy_and_non_logic_incremental_adapters \
|
tests/test_adapter_contract.py::AdapterContractTests::test_no_ast_index_accepts_legacy_and_non_logic_incremental_adapters \
|
||||||
tests/test_adapter_contract.py::AdapterContractTests::test_no_ast_rejects_preexisting_logic_index_and_viewer_snapshot \
|
tests/test_adapter_contract.py::AdapterContractTests::test_no_ast_rejects_preexisting_logic_index_and_viewer_snapshot \
|
||||||
|
|
@ -56,4 +61,11 @@ benchmark-m1-smoke:
|
||||||
benchmark-m1:
|
benchmark-m1:
|
||||||
$(PYTHON) tools/milestone1_benchmark.py --nodes 1000 --samples 10
|
$(PYTHON) tools/milestone1_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:
|
||||||
|
$(PYTHON) tools/milestone2_benchmark.py --nodes 25 --samples 1 \
|
||||||
|
--output /tmp/docforge-milestone2-smoke.json > /dev/null
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
||||||
20
README.md
20
README.md
|
|
@ -13,6 +13,10 @@ declared manuals, visualizes project structure, and manages reviewable documenta
|
||||||
continuation.
|
continuation.
|
||||||
- Records one bounded, versioned latest-generation graph transition without creating a history
|
- Records one bounded, versioned latest-generation graph transition without creating a history
|
||||||
database.
|
database.
|
||||||
|
- Generates deterministic project-bound Codex, Claude, and OpenClaw client fragments without
|
||||||
|
copying ambient secrets.
|
||||||
|
- Diagnoses one client binding through bounded read-only checks without starting MCP or rebuilding
|
||||||
|
project state.
|
||||||
- Automatically synchronizes disposable indexes before MCP work.
|
- Automatically synchronizes disposable indexes before MCP work.
|
||||||
- Creates, validates, diffs, and previews isolated changesets.
|
- Creates, validates, diffs, and previews isolated changesets.
|
||||||
- Registers complete proposals atomically without caller-managed hash chaining.
|
- Registers complete proposals atomically without caller-managed hash chaining.
|
||||||
|
|
@ -135,6 +139,16 @@ Start an MCP server for one project:
|
||||||
Add `--canonical-applier project-editor` only when that MCP integration should expose the
|
Add `--canonical-applier project-editor` only when that MCP integration should expose the
|
||||||
hash-bound `docforge_apply_changeset` tool.
|
hash-bound `docforge_apply_changeset` tool.
|
||||||
|
|
||||||
|
Preview a read-only Codex fragment and diagnose an installed binding:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/docforge configure codex --project "$PROJECT"
|
||||||
|
.venv/bin/docforge doctor --client codex --project "$PROJECT"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass `--output /absolute/path/docforge.toml` only when creating a standalone fragment. DocForge
|
||||||
|
never replaces or merges an existing different client file.
|
||||||
|
|
||||||
For an unconfigured codebase, begin with a read-only language and documentation assessment:
|
For an unconfigured codebase, begin with a read-only language and documentation assessment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -156,6 +170,10 @@ DocForge describes them as a source graph.
|
||||||
performance, memory, rendering and response sizes, bottlenecks, and missing coverage.
|
performance, memory, rendering and response sizes, bottlenecks, and missing coverage.
|
||||||
- [Milestone 0 closeout](docs/MILESTONE_0_CLOSEOUT.md) — lineage, migration, security scan,
|
- [Milestone 0 closeout](docs/MILESTONE_0_CLOSEOUT.md) — lineage, migration, security scan,
|
||||||
repository state, and fresh-clone proof.
|
repository state, and fresh-clone proof.
|
||||||
|
- [Milestone 2 baseline](docs/MILESTONE_2_BASELINE.md) — task context, generation diff, client
|
||||||
|
configuration, doctor, response-size, counter, and memory measurements.
|
||||||
|
- [Milestone 2 closeout](docs/MILESTONE_2_CLOSEOUT.md) — implemented contracts, adversarial
|
||||||
|
validation, exclusions, and exact candidate evidence.
|
||||||
- [MCP contract](docs/MCP_CONTRACT.md) — exact tool and process boundary.
|
- [MCP contract](docs/MCP_CONTRACT.md) — exact tool and process boundary.
|
||||||
- [Viewer manager](docs/VIEWER_MANAGER.md) — native service setup and lifecycle.
|
- [Viewer manager](docs/VIEWER_MANAGER.md) — native service setup and lifecycle.
|
||||||
- [Adapter decision](docs/APPLICATION_DECISION.md) — why custom adapters own canonical
|
- [Adapter decision](docs/APPLICATION_DECISION.md) — why custom adapters own canonical
|
||||||
|
|
@ -178,7 +196,7 @@ make gate
|
||||||
|
|
||||||
Focused entry points are available as `make contract`, `make test`, `make type`,
|
Focused entry points are available as `make contract`, `make test`, `make type`,
|
||||||
`make benchmark-smoke`, `make benchmark`, `make benchmark-m1-smoke`, and
|
`make benchmark-smoke`, `make benchmark`, `make benchmark-m1-smoke`, and
|
||||||
`make benchmark-m1`.
|
`make benchmark-m1`. Milestone 2 adds `make benchmark-m2-smoke` and `make benchmark-m2`.
|
||||||
|
|
||||||
The committed 1,000-node baseline and its measurement method are under `benchmarks/`.
|
The committed 1,000-node baseline and its measurement method are under `benchmarks/`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,13 @@ Run the maintained 1,000-node Milestone 1 benchmark:
|
||||||
make benchmark-m1
|
make benchmark-m1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Run the Milestone 2 agent-workflow smoke and full gates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make benchmark-m2-smoke
|
||||||
|
make benchmark-m2
|
||||||
|
```
|
||||||
|
|
||||||
The benchmark creates canonical sources, derived state, changesets, rendered output, and caches
|
The benchmark creates canonical sources, derived state, changesets, rendered output, and caches
|
||||||
only in a disposable temporary directory. It does not read another project, self-host DocForge, or
|
only in a disposable temporary directory. It does not read another project, self-host DocForge, or
|
||||||
mutate repository content.
|
mutate repository content.
|
||||||
|
|
@ -42,6 +49,13 @@ harness enforces operation-specific p95 ceilings and fixed zero-work counter inv
|
||||||
human-readable interpretation is in
|
human-readable interpretation is in
|
||||||
[`docs/MILESTONE_1_BASELINE.md`](../docs/MILESTONE_1_BASELINE.md).
|
[`docs/MILESTONE_1_BASELINE.md`](../docs/MILESTONE_1_BASELINE.md).
|
||||||
|
|
||||||
|
`milestone2-2026-07-29.json` is the clean-tree agent-retrieval and client-integration baseline
|
||||||
|
captured from commit `fb0df5e4a1c591c2a84788fd4814d98550f11863`. It gates every warmup and
|
||||||
|
sample, reconstructs complete task-context and generation-diff collections across bounded pages,
|
||||||
|
records whether diagnostics were dropped for response budget, checks all hidden-work counters,
|
||||||
|
and measures isolated-process peak RSS. Its interpretation is in
|
||||||
|
[`docs/MILESTONE_2_BASELINE.md`](../docs/MILESTONE_2_BASELINE.md).
|
||||||
|
|
||||||
The generic fixture exposes whole-source scaling. It does not replace the incremental adapter
|
The generic fixture exposes whole-source scaling. It does not replace the incremental adapter
|
||||||
equivalence tests and does not claim to measure a portable graph renderer, because Milestone 0 has
|
equivalence tests and does not claim to measure a portable graph renderer, because Milestone 0 has
|
||||||
no portable graph-planning or graph-rendering contract.
|
no portable graph-planning or graph-rendering contract.
|
||||||
|
|
|
||||||
457
benchmarks/milestone2-2026-07-29.json
Normal file
457
benchmarks/milestone2-2026-07-29.json
Normal file
|
|
@ -0,0 +1,457 @@
|
||||||
|
{
|
||||||
|
"benchmark": "docforge2_milestone2",
|
||||||
|
"environment": {
|
||||||
|
"implementation": "CPython",
|
||||||
|
"machine": "x86_64",
|
||||||
|
"platform": "Linux-7.1.3-200.nobara.fc44.x86_64-x86_64-with-glibc2.43",
|
||||||
|
"python": "3.14.6"
|
||||||
|
},
|
||||||
|
"fixture": {
|
||||||
|
"edge_count": 999,
|
||||||
|
"kind": "synthetic_generic_focus_fan_in",
|
||||||
|
"max_tool_output_chars": 200000,
|
||||||
|
"node_count": 1000,
|
||||||
|
"source_file_count": 1000
|
||||||
|
},
|
||||||
|
"isolated_process_peak_rss_kib": 86448,
|
||||||
|
"method": {
|
||||||
|
"clock": "time.perf_counter_ns",
|
||||||
|
"memory": "isolated child-process resource.getrusage(RUSAGE_SELF).ru_maxrss",
|
||||||
|
"memory_limit_kib": 262144,
|
||||||
|
"memory_probe_samples": 10,
|
||||||
|
"percentile": "nearest-rank",
|
||||||
|
"response_size": "UTF-8 bytes of compact sorted JSON",
|
||||||
|
"samples": 10,
|
||||||
|
"warmups": 1,
|
||||||
|
"zero_work_counters": [
|
||||||
|
"project_loads",
|
||||||
|
"source_files_parsed",
|
||||||
|
"source_bytes_parsed",
|
||||||
|
"adapter_projection_loads",
|
||||||
|
"adapter_source_extractions",
|
||||||
|
"index_synchronizations",
|
||||||
|
"index_builds",
|
||||||
|
"render_prepare_calls",
|
||||||
|
"render_output_bytes_built",
|
||||||
|
"render_output_bytes_hashed",
|
||||||
|
"viewer_manager_requests"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"operations": {
|
||||||
|
"bootstrap_no_ast": {
|
||||||
|
"counter_ranges": {
|
||||||
|
"adapter_projection_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"adapter_source_extractions": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_builds": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_checks": {"maximum": 1, "minimum": 1},
|
||||||
|
"index_synchronizations": {"maximum": 1, "minimum": 1},
|
||||||
|
"project_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_built": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_hashed": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_prepare_calls": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_bytes_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_files_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_generation_checks": {"maximum": 1, "minimum": 1},
|
||||||
|
"viewer_manager_requests": {"maximum": 0, "minimum": 0}
|
||||||
|
},
|
||||||
|
"max_ms": 9.379,
|
||||||
|
"maximum_response_bytes": 8796,
|
||||||
|
"median_ms": 9.153,
|
||||||
|
"min_ms": 9.026,
|
||||||
|
"p95_limit_ms": 100,
|
||||||
|
"p95_ms": 9.379,
|
||||||
|
"response_bytes": 8795,
|
||||||
|
"response_limit_bytes": 32768,
|
||||||
|
"samples": 10,
|
||||||
|
"validated_invocations": 11
|
||||||
|
},
|
||||||
|
"bootstrap_read": {
|
||||||
|
"counter_ranges": {
|
||||||
|
"adapter_projection_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"adapter_source_extractions": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_builds": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_checks": {"maximum": 1, "minimum": 1},
|
||||||
|
"index_synchronizations": {"maximum": 1, "minimum": 1},
|
||||||
|
"project_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_built": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_hashed": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_prepare_calls": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_bytes_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_files_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_generation_checks": {"maximum": 1, "minimum": 1},
|
||||||
|
"viewer_manager_requests": {"maximum": 0, "minimum": 0}
|
||||||
|
},
|
||||||
|
"max_ms": 9.884,
|
||||||
|
"maximum_response_bytes": 7495,
|
||||||
|
"median_ms": 9.406,
|
||||||
|
"min_ms": 9.053,
|
||||||
|
"p95_limit_ms": 100,
|
||||||
|
"p95_ms": 9.884,
|
||||||
|
"response_bytes": 7491,
|
||||||
|
"response_limit_bytes": 32768,
|
||||||
|
"samples": 10,
|
||||||
|
"validated_invocations": 11
|
||||||
|
},
|
||||||
|
"configuration_preview": {
|
||||||
|
"claude": {
|
||||||
|
"artifact_format": "claude-json-fragment-v1",
|
||||||
|
"configuration_hash": "4dbb4ed0f38264fdba350de8904cc898493d620194b5105b7889c54bd5913c9c",
|
||||||
|
"counter_ranges": {
|
||||||
|
"adapter_projection_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"adapter_source_extractions": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_builds": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_synchronizations": {"maximum": 0, "minimum": 0},
|
||||||
|
"project_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_built": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_hashed": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_prepare_calls": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_bytes_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_files_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_generation_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"viewer_manager_requests": {"maximum": 0, "minimum": 0}
|
||||||
|
},
|
||||||
|
"max_ms": 364.365,
|
||||||
|
"maximum_response_bytes": 2748,
|
||||||
|
"median_ms": 314.326,
|
||||||
|
"min_ms": 314.278,
|
||||||
|
"p95_limit_ms": 500,
|
||||||
|
"p95_ms": 364.365,
|
||||||
|
"response_bytes": 2748,
|
||||||
|
"response_limit_bytes": 32768,
|
||||||
|
"samples": 10,
|
||||||
|
"validated_invocations": 11
|
||||||
|
},
|
||||||
|
"codex": {
|
||||||
|
"artifact_format": "codex-toml-fragment-v1",
|
||||||
|
"configuration_hash": "3e1dd5191021da1778cd1c4f4658768537775e5e16252e42d4c80e328841145b",
|
||||||
|
"counter_ranges": {
|
||||||
|
"adapter_projection_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"adapter_source_extractions": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_builds": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_synchronizations": {"maximum": 0, "minimum": 0},
|
||||||
|
"project_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_built": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_hashed": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_prepare_calls": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_bytes_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_files_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_generation_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"viewer_manager_requests": {"maximum": 0, "minimum": 0}
|
||||||
|
},
|
||||||
|
"max_ms": 364.383,
|
||||||
|
"maximum_response_bytes": 2627,
|
||||||
|
"median_ms": 314.365,
|
||||||
|
"min_ms": 314.248,
|
||||||
|
"p95_limit_ms": 500,
|
||||||
|
"p95_ms": 364.383,
|
||||||
|
"response_bytes": 2627,
|
||||||
|
"response_limit_bytes": 32768,
|
||||||
|
"samples": 10,
|
||||||
|
"validated_invocations": 11
|
||||||
|
},
|
||||||
|
"openclaw": {
|
||||||
|
"artifact_format": "openclaw-json-fragment-v1",
|
||||||
|
"configuration_hash": "6f269e90a55088c5d517f91761c53a3b90036d62fd80b5b9d094668267257b99",
|
||||||
|
"counter_ranges": {
|
||||||
|
"adapter_projection_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"adapter_source_extractions": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_builds": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_synchronizations": {"maximum": 0, "minimum": 0},
|
||||||
|
"project_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_built": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_hashed": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_prepare_calls": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_bytes_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_files_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_generation_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"viewer_manager_requests": {"maximum": 0, "minimum": 0}
|
||||||
|
},
|
||||||
|
"max_ms": 364.532,
|
||||||
|
"maximum_response_bytes": 2869,
|
||||||
|
"median_ms": 314.401,
|
||||||
|
"min_ms": 314.251,
|
||||||
|
"p95_limit_ms": 500,
|
||||||
|
"p95_ms": 364.532,
|
||||||
|
"response_bytes": 2869,
|
||||||
|
"response_limit_bytes": 32768,
|
||||||
|
"samples": 10,
|
||||||
|
"validated_invocations": 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"doctor": {
|
||||||
|
"claude": {
|
||||||
|
"counter_ranges": {
|
||||||
|
"adapter_projection_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"adapter_source_extractions": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_builds": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_synchronizations": {"maximum": 0, "minimum": 0},
|
||||||
|
"project_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_built": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_hashed": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_prepare_calls": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_bytes_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_files_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_generation_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"viewer_manager_requests": {"maximum": 0, "minimum": 0}
|
||||||
|
},
|
||||||
|
"doctor_state": "degraded",
|
||||||
|
"max_ms": 0.446,
|
||||||
|
"maximum_response_bytes": 3669,
|
||||||
|
"median_ms": 0.364,
|
||||||
|
"min_ms": 0.352,
|
||||||
|
"p95_limit_ms": 100,
|
||||||
|
"p95_ms": 0.446,
|
||||||
|
"response_bytes": 3669,
|
||||||
|
"response_limit_bytes": 32768,
|
||||||
|
"samples": 10,
|
||||||
|
"summary": {"failed": 0, "passed": 11, "skipped": 1, "warning": 2},
|
||||||
|
"validated_invocations": 11
|
||||||
|
},
|
||||||
|
"codex": {
|
||||||
|
"counter_ranges": {
|
||||||
|
"adapter_projection_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"adapter_source_extractions": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_builds": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_synchronizations": {"maximum": 0, "minimum": 0},
|
||||||
|
"project_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_built": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_hashed": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_prepare_calls": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_bytes_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_files_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_generation_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"viewer_manager_requests": {"maximum": 0, "minimum": 0}
|
||||||
|
},
|
||||||
|
"doctor_state": "healthy",
|
||||||
|
"max_ms": 0.556,
|
||||||
|
"maximum_response_bytes": 3595,
|
||||||
|
"median_ms": 0.421,
|
||||||
|
"min_ms": 0.404,
|
||||||
|
"p95_limit_ms": 100,
|
||||||
|
"p95_ms": 0.556,
|
||||||
|
"response_bytes": 3595,
|
||||||
|
"response_limit_bytes": 32768,
|
||||||
|
"samples": 10,
|
||||||
|
"summary": {"failed": 0, "passed": 13, "skipped": 1, "warning": 0},
|
||||||
|
"validated_invocations": 11
|
||||||
|
},
|
||||||
|
"openclaw": {
|
||||||
|
"counter_ranges": {
|
||||||
|
"adapter_projection_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"adapter_source_extractions": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_builds": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_synchronizations": {"maximum": 0, "minimum": 0},
|
||||||
|
"project_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_built": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_hashed": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_prepare_calls": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_bytes_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_files_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_generation_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"viewer_manager_requests": {"maximum": 0, "minimum": 0}
|
||||||
|
},
|
||||||
|
"doctor_state": "healthy",
|
||||||
|
"max_ms": 0.484,
|
||||||
|
"maximum_response_bytes": 3602,
|
||||||
|
"median_ms": 0.384,
|
||||||
|
"min_ms": 0.353,
|
||||||
|
"p95_limit_ms": 100,
|
||||||
|
"p95_ms": 0.484,
|
||||||
|
"response_bytes": 3602,
|
||||||
|
"response_limit_bytes": 32768,
|
||||||
|
"samples": 10,
|
||||||
|
"summary": {"failed": 0, "passed": 13, "skipped": 1, "warning": 0},
|
||||||
|
"validated_invocations": 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"generation_diff_complete": {
|
||||||
|
"max_ms": 425.315,
|
||||||
|
"maximum_response_bytes": 984,
|
||||||
|
"median_ms": 418.607,
|
||||||
|
"min_ms": 410.974,
|
||||||
|
"p95_limit_ms": 500,
|
||||||
|
"p95_ms": 425.315,
|
||||||
|
"response_bytes": 983,
|
||||||
|
"response_limit_bytes": 32768,
|
||||||
|
"result_summary": {
|
||||||
|
"aggregate_page_bytes": 664715,
|
||||||
|
"counter_ranges": {
|
||||||
|
"adapter_projection_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"adapter_source_extractions": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_builds": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_synchronizations": {"maximum": 0, "minimum": 0},
|
||||||
|
"project_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_built": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_hashed": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_prepare_calls": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_bytes_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_files_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_generation_checks": {"maximum": 2, "minimum": 2},
|
||||||
|
"viewer_manager_requests": {"maximum": 0, "minimum": 0}
|
||||||
|
},
|
||||||
|
"elapsed_ms": 419.04,
|
||||||
|
"item_count": 1000,
|
||||||
|
"maximum_cursor_bytes": 448,
|
||||||
|
"maximum_page_bytes": 66516,
|
||||||
|
"ordered_item_hash": "1ac48cc72532809ef5d3e949756e536eec819f348eaf06338c9b39b14e63b2c7",
|
||||||
|
"page_count": 10,
|
||||||
|
"receipt_hash": "6913c962972d8255f56966e5cfab5ac8293e41bdfb5f39ef91a4f33a3b092f88",
|
||||||
|
"status": "ok"
|
||||||
|
},
|
||||||
|
"samples": 10,
|
||||||
|
"validated_invocations": 11
|
||||||
|
},
|
||||||
|
"generation_diff_diagnostic_page": {
|
||||||
|
"counter_ranges": {
|
||||||
|
"adapter_projection_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"adapter_source_extractions": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_builds": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_checks": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_synchronizations": {"maximum": 0, "minimum": 0},
|
||||||
|
"project_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_built": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_hashed": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_prepare_calls": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_bytes_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_files_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_generation_checks": {"maximum": 2, "minimum": 2},
|
||||||
|
"viewer_manager_requests": {"maximum": 0, "minimum": 0}
|
||||||
|
},
|
||||||
|
"max_ms": 41.65,
|
||||||
|
"maximum_response_bytes": 66516,
|
||||||
|
"median_ms": 40.961,
|
||||||
|
"min_ms": 40.239,
|
||||||
|
"p95_limit_ms": 100,
|
||||||
|
"p95_ms": 41.65,
|
||||||
|
"response_bytes": 66516,
|
||||||
|
"response_limit_bytes": 200000,
|
||||||
|
"samples": 10,
|
||||||
|
"validated_invocations": 11
|
||||||
|
},
|
||||||
|
"generation_diff_maximum_page": {
|
||||||
|
"diagnostics_dropped_for_budget": true,
|
||||||
|
"max_ms": 59.184,
|
||||||
|
"maximum_response_bytes": 199566,
|
||||||
|
"median_ms": 55.565,
|
||||||
|
"min_ms": 54.68,
|
||||||
|
"p95_limit_ms": 100,
|
||||||
|
"p95_ms": 59.184,
|
||||||
|
"response_bytes": 199566,
|
||||||
|
"response_limit_bytes": 200000,
|
||||||
|
"samples": 10,
|
||||||
|
"validated_invocations": 11
|
||||||
|
},
|
||||||
|
"task_context_complete": {
|
||||||
|
"max_ms": 721.847,
|
||||||
|
"maximum_response_bytes": 1325,
|
||||||
|
"median_ms": 703.561,
|
||||||
|
"min_ms": 688.975,
|
||||||
|
"p95_limit_ms": 2500,
|
||||||
|
"p95_ms": 721.847,
|
||||||
|
"response_bytes": 1324,
|
||||||
|
"response_limit_bytes": 32768,
|
||||||
|
"result_summary": {
|
||||||
|
"aggregate_page_bytes": 348845,
|
||||||
|
"capsule_hash": "20663ed685a255f7cb8a0e8d78262bf7bf26863d728f0159e0b29000f6a52b0a",
|
||||||
|
"collection_hash": "9dbc46eb5b1ac8c8340bb149205d3599fffaf67e3b363de2d035475da274857c",
|
||||||
|
"collection_hash_reconstructed": true,
|
||||||
|
"counter_ranges": {
|
||||||
|
"adapter_projection_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"adapter_source_extractions": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_builds": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_checks": {"maximum": 1, "minimum": 1},
|
||||||
|
"index_synchronizations": {"maximum": 0, "minimum": 0},
|
||||||
|
"project_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_built": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_hashed": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_prepare_calls": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_bytes_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_files_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_generation_checks": {"maximum": 2, "minimum": 2},
|
||||||
|
"viewer_manager_requests": {"maximum": 0, "minimum": 0}
|
||||||
|
},
|
||||||
|
"elapsed_ms": 719.55,
|
||||||
|
"evidence_count": 108,
|
||||||
|
"item_count": 1000,
|
||||||
|
"maximum_cursor_bytes": 1066,
|
||||||
|
"maximum_page_bytes": 151172,
|
||||||
|
"omission_count": 892,
|
||||||
|
"ordered_candidate_hash": "bdeb3a8f4018000f72a5ff1891aa800b6edf98070b9814f443c6bac4e52c38f3",
|
||||||
|
"ordered_evidence_hash": "ec07bac7f528f5a3afbc083ea5ad60541335c8789d42980fe7bae0476e0a5331",
|
||||||
|
"page_count": 11,
|
||||||
|
"plan_hash": "84dbe60267e5f8359adcf30d4d395f1ac5d5bea957beaa3888d2938120860a9c",
|
||||||
|
"status": "ok"
|
||||||
|
},
|
||||||
|
"samples": 10,
|
||||||
|
"validated_invocations": 11
|
||||||
|
},
|
||||||
|
"task_context_diagnostic_page": {
|
||||||
|
"counter_ranges": {
|
||||||
|
"adapter_projection_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"adapter_source_extractions": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_builds": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_checks": {"maximum": 1, "minimum": 1},
|
||||||
|
"index_synchronizations": {"maximum": 0, "minimum": 0},
|
||||||
|
"project_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_built": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_hashed": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_prepare_calls": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_bytes_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_files_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_generation_checks": {"maximum": 2, "minimum": 2},
|
||||||
|
"viewer_manager_requests": {"maximum": 0, "minimum": 0}
|
||||||
|
},
|
||||||
|
"max_ms": 93.115,
|
||||||
|
"maximum_response_bytes": 7190,
|
||||||
|
"median_ms": 73.326,
|
||||||
|
"min_ms": 72.264,
|
||||||
|
"p95_limit_ms": 500,
|
||||||
|
"p95_ms": 93.115,
|
||||||
|
"response_bytes": 7190,
|
||||||
|
"response_limit_bytes": 200000,
|
||||||
|
"samples": 10,
|
||||||
|
"validated_invocations": 11
|
||||||
|
},
|
||||||
|
"task_context_maximum_page": {
|
||||||
|
"counter_ranges": {
|
||||||
|
"adapter_projection_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"adapter_source_extractions": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_builds": {"maximum": 0, "minimum": 0},
|
||||||
|
"index_checks": {"maximum": 1, "minimum": 1},
|
||||||
|
"index_synchronizations": {"maximum": 0, "minimum": 0},
|
||||||
|
"project_loads": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_built": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_output_bytes_hashed": {"maximum": 0, "minimum": 0},
|
||||||
|
"render_prepare_calls": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_bytes_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_files_parsed": {"maximum": 0, "minimum": 0},
|
||||||
|
"source_generation_checks": {"maximum": 2, "minimum": 2},
|
||||||
|
"viewer_manager_requests": {"maximum": 0, "minimum": 0}
|
||||||
|
},
|
||||||
|
"diagnostics_dropped_for_budget": false,
|
||||||
|
"max_ms": 87.372,
|
||||||
|
"maximum_response_bytes": 7192,
|
||||||
|
"median_ms": 84.933,
|
||||||
|
"min_ms": 83.652,
|
||||||
|
"p95_limit_ms": 500,
|
||||||
|
"p95_ms": 87.372,
|
||||||
|
"response_bytes": 7192,
|
||||||
|
"response_limit_bytes": 200000,
|
||||||
|
"samples": 10,
|
||||||
|
"validated_invocations": 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"process_peak_rss_kib": 86072,
|
||||||
|
"schema_version": 1,
|
||||||
|
"source": {
|
||||||
|
"dirty": false,
|
||||||
|
"revision": "fb0df5e4a1c591c2a84788fd4814d98550f11863"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -60,6 +60,17 @@ MCP results retain:
|
||||||
The result schema describes the common envelope. Operation-specific fields are additive and remain
|
The result schema describes the common envelope. Operation-specific fields are additive and remain
|
||||||
bounded by the configured tool-output limit.
|
bounded by the configured tool-output limit.
|
||||||
|
|
||||||
|
The following Milestone 2 CLI additions do not change existing command signatures:
|
||||||
|
|
||||||
|
- `docforge configure codex|claude|openclaw --project ROOT`
|
||||||
|
- `docforge doctor --client codex|claude|openclaw`
|
||||||
|
|
||||||
|
Configuration output is a new version-1 machine-local contract. It preserves the `docforge`
|
||||||
|
package and executable names and emits the existing `docforge.mcp_server` module entrypoint.
|
||||||
|
Existing hand-written client configurations remain valid and are never rewritten automatically.
|
||||||
|
Doctor is inspection-only and does not become a hidden bootstrap, synchronization, or migration
|
||||||
|
path.
|
||||||
|
|
||||||
## Versioned data contracts
|
## Versioned data contracts
|
||||||
|
|
||||||
Milestone 0 preserves:
|
Milestone 0 preserves:
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ commit when Git is available; it cannot change repository state.
|
||||||
- Task context capsule: `schemas/context-capsule.schema.json`, version 1.
|
- Task context capsule: `schemas/context-capsule.schema.json`, version 1.
|
||||||
- Latest generation diff: `schemas/generation-diff.schema.json`, version 1.
|
- Latest generation diff: `schemas/generation-diff.schema.json`, version 1.
|
||||||
- Latest generation-diff page: `schemas/generation-diff-page.schema.json`, version 1.
|
- Latest generation-diff page: `schemas/generation-diff-page.schema.json`, version 1.
|
||||||
|
- Generated client configuration: `schemas/client-configuration.schema.json`, version 1.
|
||||||
|
- Client doctor result: `schemas/doctor-result.schema.json`, version 1.
|
||||||
- Index schema: version 3, disposable and reproducible.
|
- Index schema: version 3, disposable and reproducible.
|
||||||
- Index attestation: schema version 1, disposable and reproducible.
|
- Index attestation: schema version 1, disposable and reproducible.
|
||||||
- Core, CLI, and MCP server: version 1.3.0.dev0.
|
- Core, CLI, and MCP server: version 1.3.0.dev0.
|
||||||
|
|
@ -75,6 +77,25 @@ not history and contains no Logic details or source text. Public pages carry one
|
||||||
`receipt_header`; its `stored_receipt_hash` identifies the complete persisted receipt rather than
|
`receipt_header`; its `stored_receipt_hash` identifies the complete persisted receipt rather than
|
||||||
the header alone. One top-level pagination object carries the only continuation cursor.
|
the header alone. One top-level pagination object carries the only continuation cursor.
|
||||||
|
|
||||||
|
## Machine-local client integration
|
||||||
|
|
||||||
|
Generated Codex, Claude, and OpenClaw fragments are machine-local projections. They are not
|
||||||
|
canonical project content. Version 1 binds the selected project, exact isolated Python
|
||||||
|
interpreter, canonical argument layout, effective policy, no-AST projection, render policy,
|
||||||
|
timeouts, artifact bytes, and configuration hash.
|
||||||
|
|
||||||
|
Preview is side-effect free. Explicit publication creates only one new private standalone
|
||||||
|
fragment in an existing real directory. It never merges or replaces different content. Descriptor,
|
||||||
|
parent, target, content, ownership, permission, and link identities are checked before and after
|
||||||
|
the directory durability boundary. A failure rolls back when that can be proven and otherwise
|
||||||
|
returns bounded unconfirmed publication evidence.
|
||||||
|
|
||||||
|
Doctor is a bounded read-only inspector with one fixed check inventory. It uses stable no-follow
|
||||||
|
descriptor and configuration reads plus stat-only derived-index evidence. It never loads a
|
||||||
|
complete projection, opens SQLite, starts MCP, executes the configured command, synchronizes,
|
||||||
|
builds, renders, starts a viewer, or writes configuration. Unprovable client behavior is a warning,
|
||||||
|
not an invented success.
|
||||||
|
|
||||||
## Isolated proposal model
|
## Isolated proposal model
|
||||||
|
|
||||||
Create, update, move, and delete are ordered node operations inside an isolated changeset. Every
|
Create, update, move, and delete are ordered node operations inside an isolated changeset. Every
|
||||||
|
|
|
||||||
85
docs/MILESTONE_2_BASELINE.md
Normal file
85
docs/MILESTONE_2_BASELINE.md
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
# Milestone 2 baseline
|
||||||
|
|
||||||
|
## Scope and method
|
||||||
|
|
||||||
|
This baseline records the agent-retrieval and client-integration behavior added in Milestone 2.
|
||||||
|
It was captured on 2026-07-29 from clean candidate commit
|
||||||
|
`fb0df5e4a1c591c2a84788fd4814d98550f11863`.
|
||||||
|
|
||||||
|
The maintained command was:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python tools/milestone2_benchmark.py \
|
||||||
|
--nodes 1000 \
|
||||||
|
--samples 10 \
|
||||||
|
--output /tmp/docforge-milestone2-final.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The fixture contains 1,000 Markdown nodes and 999 edges in a direct fan-in around one focus node.
|
||||||
|
The configured MCP response limit is 200,000 characters. Durations use
|
||||||
|
`time.perf_counter_ns()` and nearest-rank p95. Peak memory uses an isolated child process and
|
||||||
|
`RUSAGE_SELF`. Every warmup and measured invocation is validated.
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
|
||||||
|
- Linux 7.1.3-200.nobara.fc44.x86_64.
|
||||||
|
- CPython 3.14.6.
|
||||||
|
- x86_64.
|
||||||
|
- Ten warm samples after one warmup.
|
||||||
|
- Isolated memory ceiling: 262,144 KiB.
|
||||||
|
|
||||||
|
The complete machine-readable result is
|
||||||
|
[`benchmarks/milestone2-2026-07-29.json`](../benchmarks/milestone2-2026-07-29.json).
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
| Operation | Median | p95 | Limit | Maximum response |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| Read bootstrap | 9.406 ms | 9.884 ms | 100 ms | 7,495 B |
|
||||||
|
| No-AST bootstrap | 9.153 ms | 9.379 ms | 100 ms | 8,796 B |
|
||||||
|
| Task diagnostic page | 73.326 ms | 93.115 ms | 500 ms | 7,190 B |
|
||||||
|
| Task complete traversal | 703.561 ms | 721.847 ms | 2,500 ms | 151,172 B/page |
|
||||||
|
| Generation diagnostic page | 40.961 ms | 41.650 ms | 100 ms | 66,516 B |
|
||||||
|
| Generation maximum page | 55.565 ms | 59.184 ms | 100 ms | 199,566 B |
|
||||||
|
| Generation complete traversal | 418.607 ms | 425.315 ms | 500 ms | 66,516 B/page |
|
||||||
|
| Codex configuration preview | 314.365 ms | 364.383 ms | 500 ms | 2,627 B |
|
||||||
|
| Claude configuration preview | 314.326 ms | 364.365 ms | 500 ms | 2,748 B |
|
||||||
|
| OpenClaw configuration preview | 314.401 ms | 364.532 ms | 500 ms | 2,869 B |
|
||||||
|
| Codex doctor | 0.421 ms | 0.556 ms | 100 ms | 3,595 B |
|
||||||
|
| Claude doctor | 0.364 ms | 0.446 ms | 100 ms | 3,669 B |
|
||||||
|
| OpenClaw doctor | 0.384 ms | 0.484 ms | 100 ms | 3,602 B |
|
||||||
|
|
||||||
|
Isolated peak RSS was 86,448 KiB.
|
||||||
|
|
||||||
|
Task traversal returned 108 evidence records and 892 explicit omissions across 11 pages. One
|
||||||
|
individually oversized focus record became a response-limit surrogate bound to the original record
|
||||||
|
hash. The remaining omissions were token-budget evidence. The benchmark verified every unique
|
||||||
|
subject, reconstructed the original collection hash, and matched the exact 1,000-node fixture.
|
||||||
|
|
||||||
|
Generation traversal returned all 1,000 changed-node details across 10 pages. It reconstructed the
|
||||||
|
stored retained-collection hash. The maximum generation page approached the response limit and
|
||||||
|
proved that optional diagnostics were dropped before the primary result.
|
||||||
|
|
||||||
|
## Structured-work gates
|
||||||
|
|
||||||
|
Configuration preview and doctor performed zero project loads, source parses, adapter projection
|
||||||
|
loads, adapter extraction, index checks, synchronization, index builds, render preparation,
|
||||||
|
rendered-byte construction or hashing, and viewer-manager requests.
|
||||||
|
|
||||||
|
Task-context pages performed exactly one index check and two cheap source-generation checks. They
|
||||||
|
performed none of the hidden work above. Generation-diff pages performed exactly two cheap
|
||||||
|
source-generation checks and no index check or hidden work.
|
||||||
|
|
||||||
|
## Measured limits and future notes
|
||||||
|
|
||||||
|
- Continuation is stateless and regenerates the task capsule for each page. The complete
|
||||||
|
11-page traversal remains within its gate, but later work can avoid repeated planning without
|
||||||
|
weakening generation binding.
|
||||||
|
- Configuration preview deliberately spends about 314 ms proving that the exact isolated
|
||||||
|
interpreter can import the MCP module. Discovery-only checks were rejected as unsafe.
|
||||||
|
- Claude configuration syntax is supported, but its timeout representation remains unverified.
|
||||||
|
Doctor therefore reports degraded rather than healthy.
|
||||||
|
- Doctor is a configuration inspector, not an MCP connection or SQLite integrity test.
|
||||||
|
- Legacy adapters without cheap source-generation identity report unknown for generation-diff
|
||||||
|
freshness.
|
||||||
|
- The results do not justify a storage rewrite. SQLite remains fast after one generation is pinned.
|
||||||
68
docs/MILESTONE_2_CLOSEOUT.md
Normal file
68
docs/MILESTONE_2_CLOSEOUT.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Milestone 2 closeout
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Milestone 2 is complete. One project-bound server can expose an explicit effective policy and
|
||||||
|
return compact, task-shaped, explainable context. Users can generate deterministic client
|
||||||
|
fragments and inspect their bindings without hidden runtime work.
|
||||||
|
|
||||||
|
Implemented contracts:
|
||||||
|
|
||||||
|
- Version-1 effective policy and capability-aware bootstrap.
|
||||||
|
- Version-1 retrieval plans and context capsules.
|
||||||
|
- Bounded task-context pagination with evidence gaps and explicit omissions.
|
||||||
|
- One disposable latest-generation transition receipt and paged read surface.
|
||||||
|
- Deterministic Codex, Claude, and OpenClaw standalone configuration fragments.
|
||||||
|
- Fixed-inventory read-only doctor results.
|
||||||
|
- Dedicated configuration and doctor JSON schemas.
|
||||||
|
- Repository-native Milestone 2 contract, smoke, scale, response-size, counter, and memory gates.
|
||||||
|
|
||||||
|
## Candidate evidence
|
||||||
|
|
||||||
|
The frozen implementation candidate is
|
||||||
|
`fb0df5e4a1c591c2a84788fd4814d98550f11863`.
|
||||||
|
|
||||||
|
The complete repository gate passed:
|
||||||
|
|
||||||
|
- Ruff formatting and lint.
|
||||||
|
- HTML, rendered-manual HTML, CSS, and JavaScript checks.
|
||||||
|
- Pyright with zero diagnostics.
|
||||||
|
- Warning-strict compilation and tests.
|
||||||
|
- 205 tests and 120 subtests.
|
||||||
|
- Lock and npm dependency-tree checks.
|
||||||
|
- Wheel and source-distribution builds.
|
||||||
|
- Milestone 0, 1, and 2 smoke benchmarks.
|
||||||
|
|
||||||
|
Three independent read-only adversarial audits covered client publication and policy binding,
|
||||||
|
doctor race and malformed-input behavior, and benchmark/contract evidence. Reproduced descriptor,
|
||||||
|
parent, target, filesystem, policy, secret-redaction, ambiguity, parser, response-size, and hidden
|
||||||
|
work defects were fixed and regression-tested before the candidate was frozen.
|
||||||
|
|
||||||
|
The clean ten-sample 1,000-node benchmark passed every threshold. Exact measurements and counter
|
||||||
|
ranges are recorded in
|
||||||
|
[`MILESTONE_2_BASELINE.md`](MILESTONE_2_BASELINE.md) and
|
||||||
|
[`benchmarks/milestone2-2026-07-29.json`](../benchmarks/milestone2-2026-07-29.json).
|
||||||
|
|
||||||
|
## Preserved boundaries
|
||||||
|
|
||||||
|
- The `docforge` package, imports, CLI executable, MCP executable, and existing tool names remain.
|
||||||
|
- Legacy one-method `load_projection()` adapters remain supported.
|
||||||
|
- The no-AST shorthand and legacy adapter-policy payload remain compatible.
|
||||||
|
- Project descriptor schema version 1 remains unchanged.
|
||||||
|
- No storage replacement was introduced.
|
||||||
|
- No legacy DocForge MCP or DocForge2 self-hosting was used.
|
||||||
|
- WorldForge and ScrapeStation were not touched.
|
||||||
|
- No production MCP integration was repointed.
|
||||||
|
- The legacy Forgejo repository and `legacy` remote were not changed.
|
||||||
|
- No tag, release, release announcement, or visibility change was created.
|
||||||
|
|
||||||
|
## Known follow-up work
|
||||||
|
|
||||||
|
The next active milestone may improve projection independence. It must not silently absorb these
|
||||||
|
separate future ideas:
|
||||||
|
|
||||||
|
- Avoid recomputing a complete task capsule for every continuation page.
|
||||||
|
- Add authenticated cursors only if a stronger threat model requires them.
|
||||||
|
- Verify Claude's native timeout representation.
|
||||||
|
- Add versioned adapter-owned launcher metadata before generating custom-adapter configurations.
|
||||||
|
- Keep doctor read-only; a live connection test must be an explicit separate operation.
|
||||||
|
|
@ -508,6 +508,67 @@ visualization-status
|
||||||
visualization-stop
|
visualization-stop
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Client configuration and doctor
|
||||||
|
|
||||||
|
Preview one deterministic standalone client fragment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docforge configure codex --project /absolute/path/MyProject
|
||||||
|
docforge configure claude --project /absolute/path/MyProject
|
||||||
|
docforge configure openclaw --project /absolute/path/MyProject
|
||||||
|
```
|
||||||
|
|
||||||
|
Preview is the default. Add `--output /absolute/path/fragment` to create a new private fragment in
|
||||||
|
an existing real directory. Publication is create-only. DocForge accepts an identical existing
|
||||||
|
private single-link file as unchanged, but it never merges, replaces, broadens permissions, or
|
||||||
|
follows a symlink. Descriptor, parent, and target identities are revalidated across the
|
||||||
|
publication commit.
|
||||||
|
|
||||||
|
The generated command uses the exact current Python interpreter with isolated module startup.
|
||||||
|
Generation first proves that this interpreter can import `docforge.mcp_server`. The result binds
|
||||||
|
the project root, effective policy, arguments, artifact bytes, and all hashes. It copies no ambient
|
||||||
|
environment values.
|
||||||
|
|
||||||
|
Select authority explicitly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docforge configure codex \
|
||||||
|
--project /absolute/path/MyProject \
|
||||||
|
--capability-mode proposal \
|
||||||
|
--proposal-writer project-editor
|
||||||
|
|
||||||
|
docforge configure codex \
|
||||||
|
--project /absolute/path/MyProject \
|
||||||
|
--capability-mode application \
|
||||||
|
--proposal-writer project-editor \
|
||||||
|
--canonical-applier project-editor
|
||||||
|
```
|
||||||
|
|
||||||
|
Read mode is the default. Proposal and application modes fail closed unless the descriptor
|
||||||
|
declares the named writer, and application requires the same writer/applier identity. Add
|
||||||
|
`--no-ast` to preserve the no-AST binding. Generic CLI generation refuses project-owned adapters
|
||||||
|
because it cannot safely reconstruct their composition.
|
||||||
|
|
||||||
|
Inspect one configured client binding:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docforge doctor --client codex --project /absolute/path/MyProject
|
||||||
|
docforge doctor --client codex \
|
||||||
|
--project /absolute/path/MyProject \
|
||||||
|
--config /absolute/path/config.toml \
|
||||||
|
--server-name my-project-docforge
|
||||||
|
```
|
||||||
|
|
||||||
|
Doctor returns `healthy`, `degraded`, or `unhealthy` with exit codes 0, 1, or 2. Its fixed
|
||||||
|
version-1 inventory checks project and descriptor binding, the client driver and entry, executable
|
||||||
|
and arguments, project root, effective policy, no-AST state, timeouts, environment-key names,
|
||||||
|
tool-filter representation, and stat-only index presence.
|
||||||
|
|
||||||
|
Doctor is intentionally not a connection test. It never loads canonical sources, opens SQLite,
|
||||||
|
starts MCP, executes the configured command, synchronizes, builds, renders, starts a viewer, or
|
||||||
|
writes configuration. Claude timeout representation and client filtering that cannot be proved
|
||||||
|
locally remain explicit warnings.
|
||||||
|
|
||||||
## MCP usage
|
## MCP usage
|
||||||
|
|
||||||
Run one MCP server per project with absolute paths:
|
Run one MCP server per project with absolute paths:
|
||||||
|
|
|
||||||
894
schemas/client-configuration.schema.json
Normal file
894
schemas/client-configuration.schema.json
Normal file
|
|
@ -0,0 +1,894 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://docforge.local/schema/client-configuration-v1.json",
|
||||||
|
"title": "DocForge deterministic client configuration plan",
|
||||||
|
"$defs": {
|
||||||
|
"sha256": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
},
|
||||||
|
"adapter_policy": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"mode",
|
||||||
|
"ast_analysis",
|
||||||
|
"logic_projection",
|
||||||
|
"incremental_extraction",
|
||||||
|
"adapter_rewrite"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"mode": { "const": "standard" },
|
||||||
|
"ast_analysis": { "const": "allowed" },
|
||||||
|
"logic_projection": { "const": "allowed" },
|
||||||
|
"incremental_extraction": { "const": "allowed" },
|
||||||
|
"adapter_rewrite": { "const": "not_requested" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"mode",
|
||||||
|
"ast_analysis",
|
||||||
|
"logic_projection",
|
||||||
|
"incremental_extraction",
|
||||||
|
"adapter_rewrite",
|
||||||
|
"blocked_tools",
|
||||||
|
"instruction"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"mode": { "const": "preserve-no-ast" },
|
||||||
|
"ast_analysis": { "const": "forbidden" },
|
||||||
|
"logic_projection": { "const": "forbidden" },
|
||||||
|
"incremental_extraction": { "const": "allowed" },
|
||||||
|
"adapter_rewrite": { "const": "forbidden" },
|
||||||
|
"blocked_tools": {
|
||||||
|
"const": ["docforge_get_logic"]
|
||||||
|
},
|
||||||
|
"instruction": {
|
||||||
|
"const": "Preserve the existing adapter extraction strategy. Do not add Python AST, Tree-sitter, compiler-AST, or function-Logic extraction. Non-AST incremental fingerprinting and caching remain allowed."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"effective_policy": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"capability_mode",
|
||||||
|
"capability_source",
|
||||||
|
"adapter_evolution",
|
||||||
|
"ast_analysis",
|
||||||
|
"logic_indexing",
|
||||||
|
"synchronization",
|
||||||
|
"integrity",
|
||||||
|
"manual_render",
|
||||||
|
"graph_render",
|
||||||
|
"live_viewer",
|
||||||
|
"profiling",
|
||||||
|
"blocked_tools",
|
||||||
|
"prohibitions",
|
||||||
|
"precedence"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "const": 1 },
|
||||||
|
"capability_mode": {
|
||||||
|
"enum": ["read", "proposal", "application"]
|
||||||
|
},
|
||||||
|
"capability_source": { "const": "explicit" },
|
||||||
|
"adapter_evolution": { "enum": ["allowed", "preserve"] },
|
||||||
|
"ast_analysis": { "enum": ["allowed", "forbidden"] },
|
||||||
|
"logic_indexing": { "enum": ["full", "off"] },
|
||||||
|
"synchronization": { "const": "automatic" },
|
||||||
|
"integrity": { "const": "validated" },
|
||||||
|
"manual_render": { "enum": ["auto", "explicit", "disabled"] },
|
||||||
|
"graph_render": { "const": "disabled" },
|
||||||
|
"live_viewer": { "const": "on-demand" },
|
||||||
|
"profiling": { "const": "disabled" },
|
||||||
|
"blocked_tools": {
|
||||||
|
"type": "array",
|
||||||
|
"maxItems": 1,
|
||||||
|
"items": { "const": "docforge_get_logic" },
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"prohibitions": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 7,
|
||||||
|
"maxItems": 11,
|
||||||
|
"items": {
|
||||||
|
"enum": [
|
||||||
|
"arbitrary_file_access",
|
||||||
|
"arbitrary_renderer_execution",
|
||||||
|
"shell_execution",
|
||||||
|
"git_mutation",
|
||||||
|
"deployment",
|
||||||
|
"publication",
|
||||||
|
"project_switching",
|
||||||
|
"adapter_ast_upgrade",
|
||||||
|
"tree_sitter_upgrade",
|
||||||
|
"compiler_ast_upgrade",
|
||||||
|
"function_logic_extraction"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"precedence": {
|
||||||
|
"const": [
|
||||||
|
"core_safety",
|
||||||
|
"explicit_binding",
|
||||||
|
"no_ast_shorthand",
|
||||||
|
"resource_availability"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"diagnostics": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"operation",
|
||||||
|
"outcome",
|
||||||
|
"elapsed_ns",
|
||||||
|
"stages",
|
||||||
|
"counters"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "const": 1 },
|
||||||
|
"operation": { "const": "cli.configure" },
|
||||||
|
"outcome": { "const": "ok" },
|
||||||
|
"elapsed_ns": { "type": "integer", "minimum": 0 },
|
||||||
|
"stages": {
|
||||||
|
"type": "object",
|
||||||
|
"maxProperties": 14,
|
||||||
|
"propertyNames": {
|
||||||
|
"enum": [
|
||||||
|
"source.generation",
|
||||||
|
"source.parse",
|
||||||
|
"adapter.projection",
|
||||||
|
"adapter.extract",
|
||||||
|
"index.check",
|
||||||
|
"index.synchronize",
|
||||||
|
"index.build",
|
||||||
|
"index.read",
|
||||||
|
"render.status",
|
||||||
|
"render.prepare",
|
||||||
|
"render.output_hash",
|
||||||
|
"visualization.status",
|
||||||
|
"viewer.manager",
|
||||||
|
"mcp.runtime_validation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["calls", "elapsed_ns"],
|
||||||
|
"properties": {
|
||||||
|
"calls": { "type": "integer", "minimum": 1 },
|
||||||
|
"elapsed_ns": { "type": "integer", "minimum": 0 }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"counters": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"project_loads",
|
||||||
|
"source_files_parsed",
|
||||||
|
"source_bytes_parsed",
|
||||||
|
"adapter_projection_loads",
|
||||||
|
"adapter_source_extractions",
|
||||||
|
"source_generation_checks",
|
||||||
|
"index_checks",
|
||||||
|
"index_synchronizations",
|
||||||
|
"index_builds",
|
||||||
|
"render_prepare_calls",
|
||||||
|
"render_output_bytes_built",
|
||||||
|
"render_output_bytes_hashed",
|
||||||
|
"viewer_manager_requests"
|
||||||
|
],
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"maxProperties": 13
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"schema_version",
|
||||||
|
"operation",
|
||||||
|
"action",
|
||||||
|
"client",
|
||||||
|
"server_name",
|
||||||
|
"project",
|
||||||
|
"binding",
|
||||||
|
"effective_policy",
|
||||||
|
"artifact",
|
||||||
|
"configuration_hash",
|
||||||
|
"warnings"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"status": { "const": "ok" },
|
||||||
|
"schema_version": { "const": 1 },
|
||||||
|
"operation": { "const": "client.configure" },
|
||||||
|
"action": { "enum": ["preview", "write"] },
|
||||||
|
"client": { "enum": ["codex", "claude", "openclaw"] },
|
||||||
|
"server_name": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"project_id",
|
||||||
|
"project_root",
|
||||||
|
"project_root_fingerprint",
|
||||||
|
"adapter"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"project_id": { "type": "string", "minLength": 1 },
|
||||||
|
"project_root": { "type": "string", "minLength": 1 },
|
||||||
|
"project_root_fingerprint": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{16}$"
|
||||||
|
},
|
||||||
|
"adapter": { "type": "string", "minLength": 1 }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"binding": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"transport",
|
||||||
|
"capability_mode",
|
||||||
|
"adapter_policy",
|
||||||
|
"render_policy",
|
||||||
|
"command",
|
||||||
|
"args",
|
||||||
|
"environment",
|
||||||
|
"timeouts"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"transport": { "const": "stdio" },
|
||||||
|
"capability_mode": {
|
||||||
|
"enum": ["read", "proposal", "application"]
|
||||||
|
},
|
||||||
|
"adapter_policy": { "$ref": "#/$defs/adapter_policy" },
|
||||||
|
"render_policy": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["manual", "graph", "live_viewer"],
|
||||||
|
"properties": {
|
||||||
|
"manual": { "enum": ["auto", "explicit", "disabled"] },
|
||||||
|
"graph": { "const": "disabled" },
|
||||||
|
"live_viewer": { "const": "on-demand" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"command": { "type": "string", "minLength": 1 },
|
||||||
|
"args": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 7,
|
||||||
|
"maxItems": 64,
|
||||||
|
"prefixItems": [
|
||||||
|
{ "const": "-I" },
|
||||||
|
{ "const": "-m" },
|
||||||
|
{ "const": "docforge.mcp_server" },
|
||||||
|
{ "const": "--project-root" },
|
||||||
|
{ "type": "string", "minLength": 1, "maxLength": 4096 },
|
||||||
|
{ "const": "--capability-mode" },
|
||||||
|
{ "enum": ["read", "proposal", "application"] }
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 4096
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"type": "object",
|
||||||
|
"maxProperties": 0
|
||||||
|
},
|
||||||
|
"timeouts": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["startup_seconds", "tool_seconds"],
|
||||||
|
"properties": {
|
||||||
|
"startup_seconds": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 3600
|
||||||
|
},
|
||||||
|
"tool_seconds": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 86400
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"effective_policy": { "$ref": "#/$defs/effective_policy" },
|
||||||
|
"artifact": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"format",
|
||||||
|
"content",
|
||||||
|
"content_sha256",
|
||||||
|
"output_path",
|
||||||
|
"write_state",
|
||||||
|
"durability"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"format": {
|
||||||
|
"enum": [
|
||||||
|
"codex-toml-fragment-v1",
|
||||||
|
"claude-json-fragment-v1",
|
||||||
|
"openclaw-json-fragment-v1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 65536
|
||||||
|
},
|
||||||
|
"content_sha256": { "$ref": "#/$defs/sha256" },
|
||||||
|
"output_path": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"write_state": {
|
||||||
|
"enum": ["not_requested", "created", "unchanged"]
|
||||||
|
},
|
||||||
|
"durability": {
|
||||||
|
"enum": ["not_applicable", "confirmed", "unconfirmed"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"configuration_hash": { "$ref": "#/$defs/sha256" },
|
||||||
|
"warnings": {
|
||||||
|
"type": "array",
|
||||||
|
"maxItems": 8,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["code"],
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"enum": [
|
||||||
|
"timeout_format_unverified",
|
||||||
|
"publication_durability_unconfirmed",
|
||||||
|
"publication_location_unconfirmed",
|
||||||
|
"publication_binding_unconfirmed"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"diagnostics": { "$ref": "#/$defs/diagnostics" }
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"warnings": {
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"code": { "const": "publication_binding_unconfirmed" }
|
||||||
|
},
|
||||||
|
"required": ["code"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["warnings"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"artifact": {
|
||||||
|
"properties": {
|
||||||
|
"output_path": { "type": "null" },
|
||||||
|
"write_state": { "const": "created" },
|
||||||
|
"durability": { "const": "unconfirmed" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": { "client": { "const": "codex" } },
|
||||||
|
"required": ["client"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"artifact": {
|
||||||
|
"properties": {
|
||||||
|
"format": { "const": "codex-toml-fragment-v1" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"not": {
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"code": { "const": "timeout_format_unverified" }
|
||||||
|
},
|
||||||
|
"required": ["code"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": { "client": { "const": "openclaw" } },
|
||||||
|
"required": ["client"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"artifact": {
|
||||||
|
"properties": {
|
||||||
|
"format": { "const": "openclaw-json-fragment-v1" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"not": {
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"code": { "const": "timeout_format_unverified" }
|
||||||
|
},
|
||||||
|
"required": ["code"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": { "client": { "const": "claude" } },
|
||||||
|
"required": ["client"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"artifact": {
|
||||||
|
"properties": {
|
||||||
|
"format": { "const": "claude-json-fragment-v1" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"code": { "const": "timeout_format_unverified" }
|
||||||
|
},
|
||||||
|
"required": ["code"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": { "action": { "const": "preview" } },
|
||||||
|
"required": ["action"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"artifact": {
|
||||||
|
"properties": {
|
||||||
|
"output_path": { "type": "null" },
|
||||||
|
"write_state": { "const": "not_requested" },
|
||||||
|
"durability": { "const": "not_applicable" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"else": {
|
||||||
|
"properties": {
|
||||||
|
"artifact": {
|
||||||
|
"properties": {
|
||||||
|
"output_path": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"write_state": { "enum": ["created", "unchanged"] }
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": { "write_state": { "const": "created" } },
|
||||||
|
"required": ["write_state"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"durability": { "enum": ["confirmed", "unconfirmed"] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"else": {
|
||||||
|
"properties": {
|
||||||
|
"durability": { "const": "not_applicable" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"binding": {
|
||||||
|
"properties": { "capability_mode": { "const": "read" } },
|
||||||
|
"required": ["capability_mode"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["binding"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"binding": {
|
||||||
|
"properties": {
|
||||||
|
"args": {
|
||||||
|
"prefixItems": [{}, {}, {}, {}, {}, {}, { "const": "read" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"effective_policy": {
|
||||||
|
"properties": { "capability_mode": { "const": "read" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"binding": {
|
||||||
|
"properties": { "capability_mode": { "const": "proposal" } },
|
||||||
|
"required": ["capability_mode"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["binding"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"binding": {
|
||||||
|
"properties": {
|
||||||
|
"args": {
|
||||||
|
"prefixItems": [{}, {}, {}, {}, {}, {}, { "const": "proposal" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"effective_policy": {
|
||||||
|
"properties": { "capability_mode": { "const": "proposal" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"binding": {
|
||||||
|
"properties": { "capability_mode": { "const": "application" } },
|
||||||
|
"required": ["capability_mode"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["binding"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"binding": {
|
||||||
|
"properties": {
|
||||||
|
"args": {
|
||||||
|
"prefixItems": [{}, {}, {}, {}, {}, {}, { "const": "application" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"effective_policy": {
|
||||||
|
"properties": { "capability_mode": { "const": "application" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"binding": {
|
||||||
|
"properties": {
|
||||||
|
"adapter_policy": {
|
||||||
|
"properties": { "mode": { "const": "preserve-no-ast" } },
|
||||||
|
"required": ["mode"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["adapter_policy"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["binding"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"binding": {
|
||||||
|
"properties": {
|
||||||
|
"args": {
|
||||||
|
"contains": { "const": "--no-ast" },
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"effective_policy": {
|
||||||
|
"properties": {
|
||||||
|
"adapter_evolution": { "const": "preserve" },
|
||||||
|
"ast_analysis": { "const": "forbidden" },
|
||||||
|
"logic_indexing": { "const": "off" },
|
||||||
|
"blocked_tools": { "const": ["docforge_get_logic"] },
|
||||||
|
"prohibitions": {
|
||||||
|
"const": [
|
||||||
|
"arbitrary_file_access",
|
||||||
|
"arbitrary_renderer_execution",
|
||||||
|
"shell_execution",
|
||||||
|
"git_mutation",
|
||||||
|
"deployment",
|
||||||
|
"publication",
|
||||||
|
"project_switching",
|
||||||
|
"adapter_ast_upgrade",
|
||||||
|
"tree_sitter_upgrade",
|
||||||
|
"compiler_ast_upgrade",
|
||||||
|
"function_logic_extraction"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"else": {
|
||||||
|
"properties": {
|
||||||
|
"binding": {
|
||||||
|
"properties": {
|
||||||
|
"args": {
|
||||||
|
"not": {
|
||||||
|
"contains": { "const": "--no-ast" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"effective_policy": {
|
||||||
|
"properties": {
|
||||||
|
"adapter_evolution": { "const": "allowed" },
|
||||||
|
"ast_analysis": { "const": "allowed" },
|
||||||
|
"logic_indexing": { "const": "full" },
|
||||||
|
"blocked_tools": { "const": [] },
|
||||||
|
"prohibitions": {
|
||||||
|
"const": [
|
||||||
|
"arbitrary_file_access",
|
||||||
|
"arbitrary_renderer_execution",
|
||||||
|
"shell_execution",
|
||||||
|
"git_mutation",
|
||||||
|
"deployment",
|
||||||
|
"publication",
|
||||||
|
"project_switching"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"binding": {
|
||||||
|
"properties": {
|
||||||
|
"render_policy": {
|
||||||
|
"properties": { "manual": { "const": "auto" } },
|
||||||
|
"required": ["manual"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["render_policy"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["binding"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"effective_policy": {
|
||||||
|
"properties": { "manual_render": { "const": "auto" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"binding": {
|
||||||
|
"properties": {
|
||||||
|
"render_policy": {
|
||||||
|
"properties": { "manual": { "const": "explicit" } },
|
||||||
|
"required": ["manual"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["render_policy"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["binding"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"effective_policy": {
|
||||||
|
"properties": { "manual_render": { "const": "explicit" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"binding": {
|
||||||
|
"properties": {
|
||||||
|
"render_policy": {
|
||||||
|
"properties": { "manual": { "const": "disabled" } },
|
||||||
|
"required": ["manual"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["render_policy"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["binding"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"effective_policy": {
|
||||||
|
"properties": { "manual_render": { "const": "disabled" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"artifact": {
|
||||||
|
"properties": {
|
||||||
|
"durability": { "const": "unconfirmed" }
|
||||||
|
},
|
||||||
|
"required": ["durability"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["artifact"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"artifact": {
|
||||||
|
"properties": {
|
||||||
|
"write_state": { "const": "created" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"code": { "const": "publication_durability_unconfirmed" }
|
||||||
|
},
|
||||||
|
"required": ["code"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"code": { "const": "publication_location_unconfirmed" }
|
||||||
|
},
|
||||||
|
"required": ["code"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"code": { "const": "publication_binding_unconfirmed" }
|
||||||
|
},
|
||||||
|
"required": ["code"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"warnings": {
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"code": { "const": "publication_location_unconfirmed" }
|
||||||
|
},
|
||||||
|
"required": ["code"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["warnings"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"artifact": {
|
||||||
|
"properties": {
|
||||||
|
"output_path": { "type": "null" },
|
||||||
|
"write_state": { "const": "created" },
|
||||||
|
"durability": { "const": "unconfirmed" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"action": { "const": "write" },
|
||||||
|
"artifact": {
|
||||||
|
"properties": { "output_path": { "type": "null" } },
|
||||||
|
"required": ["output_path"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["action", "artifact"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"warnings": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"code": { "const": "publication_location_unconfirmed" }
|
||||||
|
},
|
||||||
|
"required": ["code"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"code": { "const": "publication_binding_unconfirmed" }
|
||||||
|
},
|
||||||
|
"required": ["code"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"warnings": {
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"code": { "const": "publication_durability_unconfirmed" }
|
||||||
|
},
|
||||||
|
"required": ["code"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["warnings"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"artifact": {
|
||||||
|
"properties": {
|
||||||
|
"write_state": { "const": "created" },
|
||||||
|
"durability": { "const": "unconfirmed" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
466
schemas/doctor-result.schema.json
Normal file
466
schemas/doctor-result.schema.json
Normal file
|
|
@ -0,0 +1,466 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://docforge.local/schema/doctor-result-v1.json",
|
||||||
|
"title": "DocForge bounded read-only integration doctor result",
|
||||||
|
"$defs": {
|
||||||
|
"diagnostics": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"operation",
|
||||||
|
"outcome",
|
||||||
|
"elapsed_ns",
|
||||||
|
"stages",
|
||||||
|
"counters"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "const": 1 },
|
||||||
|
"operation": { "const": "cli.doctor" },
|
||||||
|
"outcome": { "const": "ok" },
|
||||||
|
"elapsed_ns": { "type": "integer", "minimum": 0 },
|
||||||
|
"stages": {
|
||||||
|
"type": "object",
|
||||||
|
"maxProperties": 14,
|
||||||
|
"propertyNames": {
|
||||||
|
"enum": [
|
||||||
|
"source.generation",
|
||||||
|
"source.parse",
|
||||||
|
"adapter.projection",
|
||||||
|
"adapter.extract",
|
||||||
|
"index.check",
|
||||||
|
"index.synchronize",
|
||||||
|
"index.build",
|
||||||
|
"index.read",
|
||||||
|
"render.status",
|
||||||
|
"render.prepare",
|
||||||
|
"render.output_hash",
|
||||||
|
"visualization.status",
|
||||||
|
"viewer.manager",
|
||||||
|
"mcp.runtime_validation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["calls", "elapsed_ns"],
|
||||||
|
"properties": {
|
||||||
|
"calls": { "type": "integer", "minimum": 1 },
|
||||||
|
"elapsed_ns": { "type": "integer", "minimum": 0 }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"counters": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"project_loads",
|
||||||
|
"source_files_parsed",
|
||||||
|
"source_bytes_parsed",
|
||||||
|
"adapter_projection_loads",
|
||||||
|
"adapter_source_extractions",
|
||||||
|
"source_generation_checks",
|
||||||
|
"index_checks",
|
||||||
|
"index_synchronizations",
|
||||||
|
"index_builds",
|
||||||
|
"render_prepare_calls",
|
||||||
|
"render_output_bytes_built",
|
||||||
|
"render_output_bytes_hashed",
|
||||||
|
"viewer_manager_requests"
|
||||||
|
],
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"maxProperties": 13
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"schema_version",
|
||||||
|
"doctor_state",
|
||||||
|
"client",
|
||||||
|
"project",
|
||||||
|
"config",
|
||||||
|
"summary",
|
||||||
|
"guarantees",
|
||||||
|
"checks"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"status": { "const": "ok" },
|
||||||
|
"schema_version": { "const": 1 },
|
||||||
|
"doctor_state": { "enum": ["healthy", "degraded", "unhealthy"] },
|
||||||
|
"client": { "enum": ["codex", "claude", "openclaw"] },
|
||||||
|
"project": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"project_id",
|
||||||
|
"project_root",
|
||||||
|
"project_root_fingerprint",
|
||||||
|
"adapter"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"project_id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||||
|
"project_root": { "type": "string", "minLength": 1, "maxLength": 4096 },
|
||||||
|
"project_root_fingerprint": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{16}$"
|
||||||
|
},
|
||||||
|
"adapter": { "type": "string", "minLength": 1, "maxLength": 256 }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["path", "server_name"],
|
||||||
|
"properties": {
|
||||||
|
"path": { "type": "string", "minLength": 1, "maxLength": 4096 },
|
||||||
|
"server_name": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"maxLength": 256
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["passed", "warning", "failed", "skipped"],
|
||||||
|
"properties": {
|
||||||
|
"passed": { "type": "integer", "minimum": 0, "maximum": 14 },
|
||||||
|
"warning": { "type": "integer", "minimum": 0, "maximum": 14 },
|
||||||
|
"failed": { "type": "integer", "minimum": 0, "maximum": 14 },
|
||||||
|
"skipped": { "type": "integer", "minimum": 0, "maximum": 14 }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"guarantees": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"read_only",
|
||||||
|
"project_loads",
|
||||||
|
"adapter_projection_loads",
|
||||||
|
"adapter_source_extractions",
|
||||||
|
"sqlite_opens",
|
||||||
|
"index_checks",
|
||||||
|
"index_synchronizations",
|
||||||
|
"index_builds",
|
||||||
|
"renders",
|
||||||
|
"viewer_operations",
|
||||||
|
"client_config_writes",
|
||||||
|
"configured_command_executions"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"read_only": { "const": true },
|
||||||
|
"project_loads": { "const": 0 },
|
||||||
|
"adapter_projection_loads": { "const": 0 },
|
||||||
|
"adapter_source_extractions": { "const": 0 },
|
||||||
|
"sqlite_opens": { "const": 0 },
|
||||||
|
"index_checks": { "const": 0 },
|
||||||
|
"index_synchronizations": { "const": 0 },
|
||||||
|
"index_builds": { "const": 0 },
|
||||||
|
"renders": { "const": 0 },
|
||||||
|
"viewer_operations": { "const": 0 },
|
||||||
|
"client_config_writes": { "const": 0 },
|
||||||
|
"configured_command_executions": { "const": 0 }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"checks": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 14,
|
||||||
|
"maxItems": 14,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["check_id", "state", "code", "message", "details"],
|
||||||
|
"properties": {
|
||||||
|
"check_id": {
|
||||||
|
"enum": [
|
||||||
|
"project.binding",
|
||||||
|
"project.canonical_validation",
|
||||||
|
"client.driver",
|
||||||
|
"client.config",
|
||||||
|
"client.entry",
|
||||||
|
"server.executable",
|
||||||
|
"server.arguments",
|
||||||
|
"server.project_binding",
|
||||||
|
"policy.effective",
|
||||||
|
"policy.no_ast",
|
||||||
|
"client.timeouts",
|
||||||
|
"client.environment",
|
||||||
|
"client.tool_filter",
|
||||||
|
"derived.index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"enum": ["passed", "warning", "failed", "skipped"]
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512
|
||||||
|
},
|
||||||
|
"details": {
|
||||||
|
"type": "object",
|
||||||
|
"maxProperties": 16,
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "type": "string", "maxLength": 512 },
|
||||||
|
{ "type": "integer" },
|
||||||
|
{ "type": "boolean" },
|
||||||
|
{ "type": "null" },
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"maxItems": 16,
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "type": "string", "maxLength": 256 },
|
||||||
|
{ "type": "integer" },
|
||||||
|
{ "type": "boolean" },
|
||||||
|
{ "type": "null" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"diagnostics": { "$ref": "#/$defs/diagnostics" }
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": { "check_id": { "const": "project.binding" } },
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"check_id": { "const": "project.canonical_validation" }
|
||||||
|
},
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": { "check_id": { "const": "client.driver" } },
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": { "check_id": { "const": "client.config" } },
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": { "check_id": { "const": "client.entry" } },
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": { "check_id": { "const": "server.executable" } },
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": { "check_id": { "const": "server.arguments" } },
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": {
|
||||||
|
"check_id": { "const": "server.project_binding" }
|
||||||
|
},
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": { "check_id": { "const": "policy.effective" } },
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": { "check_id": { "const": "policy.no_ast" } },
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": { "check_id": { "const": "client.timeouts" } },
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": { "check_id": { "const": "client.environment" } },
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": { "check_id": { "const": "client.tool_filter" } },
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"checks": {
|
||||||
|
"contains": {
|
||||||
|
"properties": { "check_id": { "const": "derived.index" } },
|
||||||
|
"required": ["check_id"]
|
||||||
|
},
|
||||||
|
"minContains": 1,
|
||||||
|
"maxContains": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": { "doctor_state": { "const": "healthy" } },
|
||||||
|
"required": ["doctor_state"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"summary": {
|
||||||
|
"properties": {
|
||||||
|
"warning": { "const": 0 },
|
||||||
|
"failed": { "const": 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": { "doctor_state": { "const": "degraded" } },
|
||||||
|
"required": ["doctor_state"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"summary": {
|
||||||
|
"properties": {
|
||||||
|
"warning": { "minimum": 1 },
|
||||||
|
"failed": { "const": 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": { "doctor_state": { "const": "unhealthy" } },
|
||||||
|
"required": ["doctor_state"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"summary": {
|
||||||
|
"properties": {
|
||||||
|
"failed": { "minimum": 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
"enum": [
|
"enum": [
|
||||||
"test",
|
"test",
|
||||||
"benchmark.m1",
|
"benchmark.m1",
|
||||||
|
"benchmark.m2",
|
||||||
"mcp.invoke",
|
"mcp.invoke",
|
||||||
"mcp.bootstrap",
|
"mcp.bootstrap",
|
||||||
"mcp.sync",
|
"mcp.sync",
|
||||||
|
|
@ -57,6 +58,8 @@
|
||||||
"cli.impact",
|
"cli.impact",
|
||||||
"cli.context",
|
"cli.context",
|
||||||
"cli.generation-diff",
|
"cli.generation-diff",
|
||||||
|
"cli.configure",
|
||||||
|
"cli.doctor",
|
||||||
"cli.render",
|
"cli.render",
|
||||||
"cli.render-status",
|
"cli.render-status",
|
||||||
"cli.preview",
|
"cli.preview",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ import webbrowser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .application import CanonicalApplicationService, GenericCanonicalApplier
|
from .application import CanonicalApplicationService, GenericCanonicalApplier
|
||||||
|
from .client_config import CLIENT_NAMES, generate_client_configuration
|
||||||
from .context import compile_context
|
from .context import compile_context
|
||||||
|
from .doctor import run_doctor
|
||||||
from .errors import DocForgeError
|
from .errors import DocForgeError
|
||||||
from .index import ProjectIndex
|
from .index import ProjectIndex
|
||||||
from .onboarding import assess_project, scaffold_project
|
from .onboarding import assess_project, scaffold_project
|
||||||
|
|
@ -21,13 +23,33 @@ from .viewer_manager import ViewerManagerClient
|
||||||
|
|
||||||
def _parser() -> argparse.ArgumentParser:
|
def _parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(prog="docforge")
|
parser = argparse.ArgumentParser(prog="docforge")
|
||||||
parser.add_argument("--project-root", type=Path, required=True)
|
parser.add_argument("--project-root", type=Path)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--diagnostics",
|
"--diagnostics",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Attach bounded request-local stage timings and counters",
|
help="Attach bounded request-local stage timings and counters",
|
||||||
)
|
)
|
||||||
commands = parser.add_subparsers(dest="command", required=True)
|
commands = parser.add_subparsers(dest="command", required=True)
|
||||||
|
configure = commands.add_parser("configure")
|
||||||
|
configure.add_argument("client", choices=CLIENT_NAMES)
|
||||||
|
configure.add_argument("--project", type=Path, required=True)
|
||||||
|
configure.add_argument("--name")
|
||||||
|
configure.add_argument(
|
||||||
|
"--capability-mode",
|
||||||
|
choices=("read", "proposal", "application"),
|
||||||
|
default="read",
|
||||||
|
)
|
||||||
|
configure.add_argument("--proposal-writer")
|
||||||
|
configure.add_argument("--canonical-applier")
|
||||||
|
configure.add_argument("--no-ast", action="store_true")
|
||||||
|
configure.add_argument("--startup-timeout", type=int, default=30)
|
||||||
|
configure.add_argument("--tool-timeout", type=int, default=300)
|
||||||
|
configure.add_argument("--output", type=Path)
|
||||||
|
doctor = commands.add_parser("doctor")
|
||||||
|
doctor.add_argument("--client", choices=CLIENT_NAMES, required=True)
|
||||||
|
doctor.add_argument("--project", type=Path)
|
||||||
|
doctor.add_argument("--config", type=Path)
|
||||||
|
doctor.add_argument("--server-name")
|
||||||
onboard = commands.add_parser("onboard")
|
onboard = commands.add_parser("onboard")
|
||||||
onboard.add_argument("--language", action="append", default=[])
|
onboard.add_argument("--language", action="append", default=[])
|
||||||
onboard.add_argument("--scaffold", action="store_true")
|
onboard.add_argument("--scaffold", action="store_true")
|
||||||
|
|
@ -92,6 +114,33 @@ def _parser() -> argparse.ArgumentParser:
|
||||||
|
|
||||||
|
|
||||||
def _run(arguments: argparse.Namespace) -> dict[str, object]:
|
def _run(arguments: argparse.Namespace) -> dict[str, object]:
|
||||||
|
if arguments.command == "configure":
|
||||||
|
project = Project.open(arguments.project)
|
||||||
|
return generate_client_configuration(
|
||||||
|
project,
|
||||||
|
arguments.client,
|
||||||
|
server_name=arguments.name,
|
||||||
|
capability_mode=arguments.capability_mode,
|
||||||
|
proposal_writer=arguments.proposal_writer,
|
||||||
|
canonical_applier=arguments.canonical_applier,
|
||||||
|
no_ast=arguments.no_ast,
|
||||||
|
startup_timeout=arguments.startup_timeout,
|
||||||
|
tool_timeout=arguments.tool_timeout,
|
||||||
|
output=arguments.output,
|
||||||
|
)
|
||||||
|
if arguments.command == "doctor":
|
||||||
|
root = arguments.project or arguments.project_root or Path.cwd()
|
||||||
|
return run_doctor(
|
||||||
|
Project.open(root),
|
||||||
|
arguments.client,
|
||||||
|
config_path=arguments.config,
|
||||||
|
server_name=arguments.server_name,
|
||||||
|
)
|
||||||
|
if arguments.project_root is None:
|
||||||
|
raise DocForgeError(
|
||||||
|
"missing_project_root",
|
||||||
|
"This command requires --project-root",
|
||||||
|
)
|
||||||
if arguments.command == "onboard":
|
if arguments.command == "onboard":
|
||||||
languages = tuple(arguments.language)
|
languages = tuple(arguments.language)
|
||||||
if arguments.scaffold:
|
if arguments.scaffold:
|
||||||
|
|
@ -250,13 +299,14 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
) as collector:
|
) as collector:
|
||||||
try:
|
try:
|
||||||
result = _run(arguments)
|
result = _run(arguments)
|
||||||
code = 0
|
doctor_state = result.get("doctor_state")
|
||||||
|
code = 2 if doctor_state == "unhealthy" else (1 if doctor_state == "degraded" else 0)
|
||||||
except DocForgeError as error:
|
except DocForgeError as error:
|
||||||
result = {"status": "error", "error": error.as_dict()}
|
result = {"status": "error", "error": error.as_dict()}
|
||||||
code = 2
|
code = 2
|
||||||
if collector is not None:
|
if collector is not None:
|
||||||
result["diagnostics"] = collector.as_dict(
|
result["diagnostics"] = collector.as_dict(
|
||||||
outcome="ok" if code == 0 else "error",
|
outcome="ok" if result.get("status") == "ok" else "error",
|
||||||
)
|
)
|
||||||
print(json.dumps(result, sort_keys=True, indent=2))
|
print(json.dumps(result, sort_keys=True, indent=2))
|
||||||
return code
|
return code
|
||||||
|
|
|
||||||
937
src/docforge/client_config.py
Normal file
937
src/docforge/client_config.py
Normal file
|
|
@ -0,0 +1,937 @@
|
||||||
|
"""Deterministic, explicit client-configuration plans for DocForge MCP."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import secrets
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections.abc import Callable
|
||||||
|
from contextlib import suppress
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Literal, cast
|
||||||
|
|
||||||
|
from .changeset_contract import document_hash
|
||||||
|
from .errors import DocForgeError
|
||||||
|
from .models import ProjectService
|
||||||
|
from .policy import CapabilityMode, compose_effective_policy
|
||||||
|
from .project import project_root_fingerprint, validate_descriptor_binding
|
||||||
|
|
||||||
|
ClientName = Literal["codex", "claude", "openclaw"]
|
||||||
|
CLIENT_NAMES: tuple[ClientName, ...] = ("codex", "claude", "openclaw")
|
||||||
|
MAX_CLIENT_FRAGMENT_BYTES = 1_000_000
|
||||||
|
GENERATED_CAPABILITY_MODES: tuple[CapabilityMode, ...] = (
|
||||||
|
"read",
|
||||||
|
"proposal",
|
||||||
|
"application",
|
||||||
|
)
|
||||||
|
_SERVER_NAME = re.compile(r"[a-z0-9][a-z0-9_-]{0,63}")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class _FileIdentity:
|
||||||
|
device: int
|
||||||
|
inode: int
|
||||||
|
mode: int
|
||||||
|
size: int
|
||||||
|
mtime_ns: int
|
||||||
|
ctime_ns: int
|
||||||
|
uid: int
|
||||||
|
link_count: int
|
||||||
|
|
||||||
|
|
||||||
|
def _file_identity(status: os.stat_result) -> _FileIdentity:
|
||||||
|
return _FileIdentity(
|
||||||
|
device=status.st_dev,
|
||||||
|
inode=status.st_ino,
|
||||||
|
mode=status.st_mode,
|
||||||
|
size=status.st_size,
|
||||||
|
mtime_ns=status.st_mtime_ns,
|
||||||
|
ctime_ns=status.st_ctime_ns,
|
||||||
|
uid=status.st_uid,
|
||||||
|
link_count=status.st_nlink,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _client_name(value: str) -> ClientName:
|
||||||
|
if value not in CLIENT_NAMES:
|
||||||
|
raise DocForgeError(
|
||||||
|
"unsupported_client",
|
||||||
|
"Client configuration target is unsupported",
|
||||||
|
client=value,
|
||||||
|
allowed=list(CLIENT_NAMES),
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _capability_mode(value: str) -> CapabilityMode:
|
||||||
|
if value not in GENERATED_CAPABILITY_MODES:
|
||||||
|
raise DocForgeError(
|
||||||
|
"invalid_capability_mode",
|
||||||
|
"Generated configuration supports read, proposal, or application mode",
|
||||||
|
capability_mode=value,
|
||||||
|
allowed=list(GENERATED_CAPABILITY_MODES),
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _bounded_seconds(value: int, *, field: str, maximum: int) -> int:
|
||||||
|
if type(value) is not int or value < 1 or value > maximum:
|
||||||
|
raise DocForgeError(
|
||||||
|
"invalid_timeout",
|
||||||
|
"Client timeout is outside the supported range",
|
||||||
|
field=field,
|
||||||
|
minimum=1,
|
||||||
|
maximum=maximum,
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _default_server_name(project_id: str, fingerprint: str) -> str:
|
||||||
|
prefix = re.sub(r"[^a-z0-9_-]+", "-", project_id.lower()).strip("-_")
|
||||||
|
prefix = prefix or "project"
|
||||||
|
suffix = f"-{fingerprint}"
|
||||||
|
available = 64 - len("docforge-") - len(suffix)
|
||||||
|
return f"docforge-{prefix[:available]}{suffix}"
|
||||||
|
|
||||||
|
|
||||||
|
def _validated_server_name(value: str | None, *, project_id: str, fingerprint: str) -> str:
|
||||||
|
selected = value or _default_server_name(project_id, fingerprint)
|
||||||
|
if _SERVER_NAME.fullmatch(selected) is None:
|
||||||
|
raise DocForgeError(
|
||||||
|
"invalid_server_name",
|
||||||
|
"Generated server name must be a stable lowercase client identifier",
|
||||||
|
pattern=_SERVER_NAME.pattern,
|
||||||
|
maximum_length=64,
|
||||||
|
)
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
def _toml_string(value: str) -> str:
|
||||||
|
return json.dumps(value, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _toml_array(values: list[str]) -> str:
|
||||||
|
return "[" + ", ".join(_toml_string(value) for value in values) + "]"
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact(
|
||||||
|
client: ClientName,
|
||||||
|
*,
|
||||||
|
server_name: str,
|
||||||
|
command: str,
|
||||||
|
arguments: list[str],
|
||||||
|
startup_timeout: int,
|
||||||
|
tool_timeout: int,
|
||||||
|
) -> tuple[str, str, str | None]:
|
||||||
|
if client == "codex":
|
||||||
|
content = "\n".join(
|
||||||
|
(
|
||||||
|
f'[mcp_servers."{server_name}"]',
|
||||||
|
f"command = {_toml_string(command)}",
|
||||||
|
f"args = {_toml_array(arguments)}",
|
||||||
|
"env = {}",
|
||||||
|
f"startup_timeout_sec = {startup_timeout}",
|
||||||
|
f"tool_timeout_sec = {tool_timeout}",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return "codex-toml-fragment-v1", content, None
|
||||||
|
if client == "openclaw":
|
||||||
|
content = (
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"mcp": {
|
||||||
|
"servers": {
|
||||||
|
server_name: {
|
||||||
|
"args": arguments,
|
||||||
|
"command": command,
|
||||||
|
"connectTimeout": startup_timeout,
|
||||||
|
"env": {},
|
||||||
|
"supportsParallelToolCalls": False,
|
||||||
|
"timeout": tool_timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
return "openclaw-json-fragment-v1", content, None
|
||||||
|
content = (
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
server_name: {
|
||||||
|
"args": arguments,
|
||||||
|
"command": command,
|
||||||
|
"env": {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
"claude-json-fragment-v1",
|
||||||
|
content,
|
||||||
|
"Claude per-server timeout representation is not yet verified.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _signature(
|
||||||
|
directory_fd: int,
|
||||||
|
name: str,
|
||||||
|
) -> _FileIdentity | None:
|
||||||
|
try:
|
||||||
|
status = os.stat(name, dir_fd=directory_fd, follow_symlinks=False)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
except OSError as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"unsafe_output",
|
||||||
|
"Configuration output cannot be inspected safely",
|
||||||
|
) from error
|
||||||
|
if stat.S_ISLNK(status.st_mode) or not stat.S_ISREG(status.st_mode):
|
||||||
|
raise DocForgeError(
|
||||||
|
"unsafe_output",
|
||||||
|
"Configuration output must be a regular file and not a symbolic link",
|
||||||
|
)
|
||||||
|
return _file_identity(status)
|
||||||
|
|
||||||
|
|
||||||
|
def _parent_binding_current(path: Path, directory_fd: int) -> bool:
|
||||||
|
try:
|
||||||
|
before = path.lstat()
|
||||||
|
resolved = path.resolve(strict=True)
|
||||||
|
after = path.lstat()
|
||||||
|
opened = os.fstat(directory_fd)
|
||||||
|
return (
|
||||||
|
not stat.S_ISLNK(before.st_mode)
|
||||||
|
and stat.S_ISDIR(before.st_mode)
|
||||||
|
and resolved == path
|
||||||
|
and (before.st_dev, before.st_ino, before.st_mode)
|
||||||
|
== (after.st_dev, after.st_ino, after.st_mode)
|
||||||
|
== (opened.st_dev, opened.st_ino, opened.st_mode)
|
||||||
|
)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _require_parent_binding(path: Path, directory_fd: int) -> None:
|
||||||
|
if not _parent_binding_current(path, directory_fd):
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_changed",
|
||||||
|
"Configuration output parent changed during publication",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _bound_parent(path: Path) -> tuple[Path, int]:
|
||||||
|
absolute = Path(os.path.abspath(path.expanduser()))
|
||||||
|
parent = absolute.parent
|
||||||
|
try:
|
||||||
|
parent_status = parent.lstat()
|
||||||
|
resolved = parent.resolve(strict=True)
|
||||||
|
except OSError as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"invalid_output",
|
||||||
|
"Configuration output parent does not exist",
|
||||||
|
) from error
|
||||||
|
if (
|
||||||
|
stat.S_ISLNK(parent_status.st_mode)
|
||||||
|
or not stat.S_ISDIR(parent_status.st_mode)
|
||||||
|
or resolved != parent
|
||||||
|
or absolute.name in {"", ".", ".."}
|
||||||
|
):
|
||||||
|
raise DocForgeError(
|
||||||
|
"unsafe_output",
|
||||||
|
"Configuration output parent must be one real non-symlinked directory",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
directory_fd = os.open(
|
||||||
|
parent,
|
||||||
|
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
||||||
|
)
|
||||||
|
except OSError as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"unsafe_output",
|
||||||
|
"Configuration output parent cannot be opened safely",
|
||||||
|
) from error
|
||||||
|
opened = os.fstat(directory_fd)
|
||||||
|
if opened.st_dev != parent_status.st_dev or opened.st_ino != parent_status.st_ino:
|
||||||
|
with suppress(OSError):
|
||||||
|
os.close(directory_fd)
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_changed",
|
||||||
|
"Configuration output parent changed while it was opened",
|
||||||
|
)
|
||||||
|
return absolute, directory_fd
|
||||||
|
|
||||||
|
|
||||||
|
def _read_existing(
|
||||||
|
directory_fd: int,
|
||||||
|
name: str,
|
||||||
|
signature: _FileIdentity,
|
||||||
|
) -> bytes:
|
||||||
|
if signature.size > MAX_CLIENT_FRAGMENT_BYTES:
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_oversized",
|
||||||
|
"Existing configuration output exceeds the bounded fragment limit",
|
||||||
|
maximum_bytes=MAX_CLIENT_FRAGMENT_BYTES,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
descriptor = os.open(
|
||||||
|
name,
|
||||||
|
os.O_RDONLY | os.O_NOFOLLOW,
|
||||||
|
dir_fd=directory_fd,
|
||||||
|
)
|
||||||
|
except OSError as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"unsafe_output",
|
||||||
|
"Configuration output cannot be opened safely",
|
||||||
|
) from error
|
||||||
|
try:
|
||||||
|
opened = os.fstat(descriptor)
|
||||||
|
opened_signature = _file_identity(opened)
|
||||||
|
if opened_signature != signature:
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_changed",
|
||||||
|
"Configuration output changed while it was opened",
|
||||||
|
)
|
||||||
|
remaining = MAX_CLIENT_FRAGMENT_BYTES + 1
|
||||||
|
chunks: list[bytes] = []
|
||||||
|
while remaining:
|
||||||
|
chunk = os.read(descriptor, min(65_536, remaining))
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
chunks.append(chunk)
|
||||||
|
remaining -= len(chunk)
|
||||||
|
raw = b"".join(chunks)
|
||||||
|
finally:
|
||||||
|
with suppress(OSError):
|
||||||
|
os.close(descriptor)
|
||||||
|
if len(raw) > MAX_CLIENT_FRAGMENT_BYTES or _signature(directory_fd, name) != signature:
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_changed",
|
||||||
|
"Configuration output changed while it was read",
|
||||||
|
)
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def _private_existing(identity: _FileIdentity) -> bool:
|
||||||
|
return (
|
||||||
|
identity.uid == os.geteuid()
|
||||||
|
and stat.S_IMODE(identity.mode) & 0o077 == 0
|
||||||
|
and identity.link_count == 1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rollback_link(
|
||||||
|
directory_fd: int,
|
||||||
|
name: str,
|
||||||
|
expected: _FileIdentity,
|
||||||
|
) -> bool:
|
||||||
|
try:
|
||||||
|
current = _signature(directory_fd, name)
|
||||||
|
if current is None:
|
||||||
|
return True
|
||||||
|
if current.device != expected.device or current.inode != expected.inode:
|
||||||
|
return True
|
||||||
|
os.unlink(name, dir_fd=directory_fd)
|
||||||
|
return True
|
||||||
|
except (DocForgeError, OSError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _rollback_and_sync(
|
||||||
|
directory_fd: int,
|
||||||
|
name: str,
|
||||||
|
expected: _FileIdentity,
|
||||||
|
) -> bool:
|
||||||
|
if not _rollback_link(directory_fd, name, expected):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
os.fsync(directory_fd)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _atomic_write(
|
||||||
|
path: Path,
|
||||||
|
content: str,
|
||||||
|
*,
|
||||||
|
validate_binding: Callable[[], None],
|
||||||
|
) -> tuple[str, str, str | None, Path | None]:
|
||||||
|
target, directory_fd = _bound_parent(path)
|
||||||
|
encoded = content.encode("utf-8")
|
||||||
|
if len(encoded) > MAX_CLIENT_FRAGMENT_BYTES:
|
||||||
|
with suppress(OSError):
|
||||||
|
os.close(directory_fd)
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_oversized",
|
||||||
|
"Generated configuration fragment exceeds the bounded limit",
|
||||||
|
maximum_bytes=MAX_CLIENT_FRAGMENT_BYTES,
|
||||||
|
)
|
||||||
|
temporary_name = f".docforge-client-{secrets.token_hex(12)}"
|
||||||
|
temporary_created = False
|
||||||
|
committed = False
|
||||||
|
linked_identity: _FileIdentity | None = None
|
||||||
|
try:
|
||||||
|
_require_parent_binding(target.parent, directory_fd)
|
||||||
|
before = _signature(directory_fd, target.name)
|
||||||
|
if before is not None:
|
||||||
|
if not _private_existing(before):
|
||||||
|
raise DocForgeError(
|
||||||
|
"unsafe_output",
|
||||||
|
(
|
||||||
|
"Existing configuration fragment must be owned by the current user, "
|
||||||
|
"private, and singly linked"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
existing = _read_existing(directory_fd, target.name, before)
|
||||||
|
if existing == encoded:
|
||||||
|
validate_binding()
|
||||||
|
_require_parent_binding(target.parent, directory_fd)
|
||||||
|
current = _signature(directory_fd, target.name)
|
||||||
|
if (
|
||||||
|
current != before
|
||||||
|
or current is None
|
||||||
|
or not _private_existing(current)
|
||||||
|
or _read_existing(directory_fd, target.name, current) != encoded
|
||||||
|
):
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_changed",
|
||||||
|
"Configuration output changed before unchanged publication was confirmed",
|
||||||
|
)
|
||||||
|
validate_binding()
|
||||||
|
_require_parent_binding(target.parent, directory_fd)
|
||||||
|
return "unchanged", "not_applicable", None, target
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_conflict",
|
||||||
|
"Configuration fragment already exists with different content",
|
||||||
|
existing_sha256=hashlib.sha256(existing).hexdigest(),
|
||||||
|
generated_sha256=hashlib.sha256(encoded).hexdigest(),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
temporary_fd = os.open(
|
||||||
|
temporary_name,
|
||||||
|
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
|
||||||
|
0o600,
|
||||||
|
dir_fd=directory_fd,
|
||||||
|
)
|
||||||
|
except OSError as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_publication_failed",
|
||||||
|
"Configuration fragment temporary file could not be created",
|
||||||
|
) from error
|
||||||
|
temporary_created = True
|
||||||
|
try:
|
||||||
|
with os.fdopen(temporary_fd, "wb", closefd=True) as handle:
|
||||||
|
handle.write(encoded)
|
||||||
|
handle.flush()
|
||||||
|
os.fsync(handle.fileno())
|
||||||
|
except OSError as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_publication_failed",
|
||||||
|
"Configuration fragment temporary file could not be written durably",
|
||||||
|
) from error
|
||||||
|
temporary_identity = _signature(directory_fd, temporary_name)
|
||||||
|
if temporary_identity is None:
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_changed",
|
||||||
|
"Configuration fragment temporary file disappeared before publication",
|
||||||
|
)
|
||||||
|
if _signature(directory_fd, target.name) is not None:
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_changed",
|
||||||
|
"Configuration output appeared before atomic publication",
|
||||||
|
)
|
||||||
|
_require_parent_binding(target.parent, directory_fd)
|
||||||
|
validate_binding()
|
||||||
|
try:
|
||||||
|
os.link(
|
||||||
|
temporary_name,
|
||||||
|
target.name,
|
||||||
|
src_dir_fd=directory_fd,
|
||||||
|
dst_dir_fd=directory_fd,
|
||||||
|
follow_symlinks=False,
|
||||||
|
)
|
||||||
|
except FileExistsError as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_changed",
|
||||||
|
"Configuration output appeared during atomic publication",
|
||||||
|
) from error
|
||||||
|
except OSError as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_publication_failed",
|
||||||
|
"Configuration fragment could not be published atomically",
|
||||||
|
) from error
|
||||||
|
linked_identity = temporary_identity
|
||||||
|
try:
|
||||||
|
validate_binding()
|
||||||
|
except DocForgeError:
|
||||||
|
if _rollback_link(directory_fd, target.name, temporary_identity):
|
||||||
|
raise
|
||||||
|
committed = True
|
||||||
|
return (
|
||||||
|
"created",
|
||||||
|
"unconfirmed",
|
||||||
|
"publication_binding_unconfirmed",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
linked = _signature(directory_fd, target.name)
|
||||||
|
if (
|
||||||
|
linked is None
|
||||||
|
or linked.device != temporary_identity.device
|
||||||
|
or linked.inode != temporary_identity.inode
|
||||||
|
or _read_existing(directory_fd, target.name, linked) != encoded
|
||||||
|
or not _parent_binding_current(target.parent, directory_fd)
|
||||||
|
):
|
||||||
|
if _rollback_link(directory_fd, target.name, temporary_identity):
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_changed",
|
||||||
|
"Configuration output changed during atomic publication",
|
||||||
|
)
|
||||||
|
committed = True
|
||||||
|
return (
|
||||||
|
"created",
|
||||||
|
"unconfirmed",
|
||||||
|
"publication_location_unconfirmed",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
durability = "confirmed"
|
||||||
|
warning: str | None = None
|
||||||
|
try:
|
||||||
|
os.unlink(temporary_name, dir_fd=directory_fd)
|
||||||
|
temporary_created = False
|
||||||
|
published = _signature(directory_fd, target.name)
|
||||||
|
if (
|
||||||
|
published is None
|
||||||
|
or not _private_existing(published)
|
||||||
|
or published.device != temporary_identity.device
|
||||||
|
or published.inode != temporary_identity.inode
|
||||||
|
or _read_existing(directory_fd, target.name, published) != encoded
|
||||||
|
or not _parent_binding_current(target.parent, directory_fd)
|
||||||
|
):
|
||||||
|
if _rollback_link(directory_fd, target.name, temporary_identity):
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_changed",
|
||||||
|
"Configuration output changed after atomic publication",
|
||||||
|
)
|
||||||
|
committed = True
|
||||||
|
return (
|
||||||
|
"created",
|
||||||
|
"unconfirmed",
|
||||||
|
"publication_location_unconfirmed",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
validate_binding()
|
||||||
|
except DocForgeError:
|
||||||
|
if _rollback_link(directory_fd, target.name, temporary_identity):
|
||||||
|
raise
|
||||||
|
committed = True
|
||||||
|
return (
|
||||||
|
"created",
|
||||||
|
"unconfirmed",
|
||||||
|
"publication_binding_unconfirmed",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
os.fsync(directory_fd)
|
||||||
|
except OSError:
|
||||||
|
durability = "unconfirmed"
|
||||||
|
warning = "publication_durability_unconfirmed"
|
||||||
|
try:
|
||||||
|
validate_binding()
|
||||||
|
except DocForgeError:
|
||||||
|
if _rollback_and_sync(directory_fd, target.name, temporary_identity):
|
||||||
|
raise
|
||||||
|
committed = True
|
||||||
|
return (
|
||||||
|
"created",
|
||||||
|
"unconfirmed",
|
||||||
|
"publication_binding_unconfirmed",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
published = _signature(directory_fd, target.name)
|
||||||
|
publication_current = (
|
||||||
|
published is not None
|
||||||
|
and _private_existing(published)
|
||||||
|
and published.device == temporary_identity.device
|
||||||
|
and published.inode == temporary_identity.inode
|
||||||
|
and _read_existing(directory_fd, target.name, published) == encoded
|
||||||
|
and _parent_binding_current(target.parent, directory_fd)
|
||||||
|
)
|
||||||
|
except DocForgeError:
|
||||||
|
publication_current = False
|
||||||
|
if not publication_current:
|
||||||
|
if _rollback_and_sync(directory_fd, target.name, temporary_identity):
|
||||||
|
raise DocForgeError(
|
||||||
|
"output_changed",
|
||||||
|
"Configuration output changed before publication was finalized",
|
||||||
|
)
|
||||||
|
committed = True
|
||||||
|
return (
|
||||||
|
"created",
|
||||||
|
"unconfirmed",
|
||||||
|
"publication_location_unconfirmed",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
committed = True
|
||||||
|
return "created", durability, warning, target
|
||||||
|
except Exception:
|
||||||
|
if not committed and linked_identity is not None:
|
||||||
|
_rollback_link(directory_fd, target.name, linked_identity)
|
||||||
|
if not committed and temporary_created:
|
||||||
|
with suppress(OSError):
|
||||||
|
os.unlink(temporary_name, dir_fd=directory_fd)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
with suppress(OSError):
|
||||||
|
os.close(directory_fd)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_configuration_result(result: dict[str, object]) -> None:
|
||||||
|
artifact = cast(dict[str, object], result["artifact"])
|
||||||
|
binding = cast(dict[str, object], result["binding"])
|
||||||
|
policy = cast(dict[str, object], result["effective_policy"])
|
||||||
|
project = cast(dict[str, object], result["project"])
|
||||||
|
content = cast(str, artifact["content"])
|
||||||
|
if artifact["content_sha256"] != hashlib.sha256(content.encode("utf-8")).hexdigest():
|
||||||
|
raise AssertionError("Generated client content hash drifted")
|
||||||
|
timeouts = cast(dict[str, object], binding["timeouts"])
|
||||||
|
expected_format, expected_content, _ = _artifact(
|
||||||
|
cast(ClientName, result["client"]),
|
||||||
|
server_name=cast(str, result["server_name"]),
|
||||||
|
command=cast(str, binding["command"]),
|
||||||
|
arguments=cast(list[str], binding["args"]),
|
||||||
|
startup_timeout=cast(int, timeouts["startup_seconds"]),
|
||||||
|
tool_timeout=cast(int, timeouts["tool_seconds"]),
|
||||||
|
)
|
||||||
|
if artifact["format"] != expected_format or content != expected_content:
|
||||||
|
raise AssertionError("Generated client artifact drifted from its binding")
|
||||||
|
adapter_policy = cast(dict[str, object], binding["adapter_policy"])
|
||||||
|
render_policy = cast(dict[str, object], binding["render_policy"])
|
||||||
|
arguments = cast(list[str], binding["args"])
|
||||||
|
prefix = [
|
||||||
|
"-I",
|
||||||
|
"-m",
|
||||||
|
"docforge.mcp_server",
|
||||||
|
"--project-root",
|
||||||
|
cast(str, project["project_root"]),
|
||||||
|
"--capability-mode",
|
||||||
|
cast(str, binding["capability_mode"]),
|
||||||
|
]
|
||||||
|
if arguments[:7] != prefix:
|
||||||
|
raise AssertionError("Generated client arguments drifted from their binding")
|
||||||
|
remaining = arguments[7:]
|
||||||
|
no_ast_argument = "--no-ast" in arguments
|
||||||
|
if no_ast_argument:
|
||||||
|
if remaining[-1:] != ["--no-ast"] or arguments.count("--no-ast") != 1:
|
||||||
|
raise AssertionError("Generated no-AST argument layout drifted")
|
||||||
|
remaining = remaining[:-1]
|
||||||
|
mode = binding["capability_mode"]
|
||||||
|
if (
|
||||||
|
(mode == "read" and remaining)
|
||||||
|
or (
|
||||||
|
mode == "proposal"
|
||||||
|
and (len(remaining) != 2 or remaining[0] != "--proposal-writer" or not remaining[1])
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
mode == "application"
|
||||||
|
and (
|
||||||
|
len(remaining) != 4
|
||||||
|
or remaining[0] != "--proposal-writer"
|
||||||
|
or remaining[2] != "--canonical-applier"
|
||||||
|
or not remaining[1]
|
||||||
|
or remaining[1] != remaining[3]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise AssertionError("Generated authority argument layout drifted")
|
||||||
|
composed_policy = compose_effective_policy(
|
||||||
|
selected_mode=cast(CapabilityMode, mode),
|
||||||
|
capability_source="explicit",
|
||||||
|
no_ast=adapter_policy["mode"] == "preserve-no-ast",
|
||||||
|
diagnostics=False,
|
||||||
|
render_configured=render_policy["manual"] != "disabled",
|
||||||
|
application_enabled=mode == "application",
|
||||||
|
)
|
||||||
|
expected_policy = composed_policy.as_dict()
|
||||||
|
if (
|
||||||
|
policy != expected_policy
|
||||||
|
or adapter_policy != composed_policy.adapter_policy()
|
||||||
|
or binding["capability_mode"] != policy["capability_mode"]
|
||||||
|
or no_ast_argument != (adapter_policy["mode"] == "preserve-no-ast")
|
||||||
|
or render_policy["manual"] != policy["manual_render"]
|
||||||
|
or render_policy["graph"] != policy["graph_render"]
|
||||||
|
or render_policy["live_viewer"] != policy["live_viewer"]
|
||||||
|
or (
|
||||||
|
adapter_policy["mode"] == "preserve-no-ast"
|
||||||
|
and (
|
||||||
|
policy["adapter_evolution"] != "preserve"
|
||||||
|
or policy["ast_analysis"] != "forbidden"
|
||||||
|
or policy["logic_indexing"] != "off"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
adapter_policy["mode"] == "standard"
|
||||||
|
and (
|
||||||
|
policy["adapter_evolution"] != "allowed"
|
||||||
|
or policy["ast_analysis"] != "allowed"
|
||||||
|
or policy["logic_indexing"] != "full"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise AssertionError("Generated client policy drifted from its binding")
|
||||||
|
expected_hash = document_hash(
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"client": result["client"],
|
||||||
|
"server_name": result["server_name"],
|
||||||
|
"project": project,
|
||||||
|
"binding": binding,
|
||||||
|
"effective_policy": policy,
|
||||||
|
"artifact_format": artifact["format"],
|
||||||
|
"artifact_content_sha256": artifact["content_sha256"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if result["configuration_hash"] != expected_hash:
|
||||||
|
raise AssertionError("Generated client configuration hash drifted")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_client_configuration(
|
||||||
|
project: ProjectService,
|
||||||
|
client: str,
|
||||||
|
*,
|
||||||
|
server_name: str | None = None,
|
||||||
|
capability_mode: str = "read",
|
||||||
|
proposal_writer: str | None = None,
|
||||||
|
canonical_applier: str | None = None,
|
||||||
|
no_ast: bool = False,
|
||||||
|
startup_timeout: int = 30,
|
||||||
|
tool_timeout: int = 300,
|
||||||
|
output: Path | None = None,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
"""Build one deterministic client fragment and optionally publish it explicitly."""
|
||||||
|
|
||||||
|
validate_descriptor_binding(project.descriptor)
|
||||||
|
selected_client = _client_name(client)
|
||||||
|
selected_mode = _capability_mode(capability_mode)
|
||||||
|
startup_seconds = _bounded_seconds(
|
||||||
|
startup_timeout,
|
||||||
|
field="startup_timeout",
|
||||||
|
maximum=3_600,
|
||||||
|
)
|
||||||
|
tool_seconds = _bounded_seconds(
|
||||||
|
tool_timeout,
|
||||||
|
field="tool_timeout",
|
||||||
|
maximum=86_400,
|
||||||
|
)
|
||||||
|
descriptor = project.descriptor
|
||||||
|
if descriptor.adapter != "generic":
|
||||||
|
raise DocForgeError(
|
||||||
|
"client_configuration_unavailable",
|
||||||
|
"Generic CLI configuration cannot reconstruct a project-owned adapter",
|
||||||
|
adapter=descriptor.adapter,
|
||||||
|
)
|
||||||
|
writer_ids = {writer.writer_id for writer in descriptor.proposal_writers}
|
||||||
|
if selected_mode == "read":
|
||||||
|
if proposal_writer is not None or canonical_applier is not None:
|
||||||
|
raise DocForgeError(
|
||||||
|
"invalid_capability_binding",
|
||||||
|
"Read configuration cannot bind proposal or application authority",
|
||||||
|
)
|
||||||
|
elif selected_mode == "proposal":
|
||||||
|
if proposal_writer is None or proposal_writer not in writer_ids:
|
||||||
|
raise DocForgeError(
|
||||||
|
"capability_unavailable",
|
||||||
|
"Proposal configuration requires a descriptor-declared writer",
|
||||||
|
required="proposal_writer",
|
||||||
|
)
|
||||||
|
if canonical_applier is not None:
|
||||||
|
raise DocForgeError(
|
||||||
|
"invalid_capability_binding",
|
||||||
|
"Proposal configuration cannot bind a canonical applier",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if (
|
||||||
|
proposal_writer is None
|
||||||
|
or canonical_applier is None
|
||||||
|
or proposal_writer != canonical_applier
|
||||||
|
or proposal_writer not in writer_ids
|
||||||
|
):
|
||||||
|
raise DocForgeError(
|
||||||
|
"capability_unavailable",
|
||||||
|
"Application configuration requires one declared writer/applier identity",
|
||||||
|
required="matching_declared_writer_and_applier",
|
||||||
|
)
|
||||||
|
|
||||||
|
fingerprint = project_root_fingerprint(descriptor.root)
|
||||||
|
selected_name = _validated_server_name(
|
||||||
|
server_name,
|
||||||
|
project_id=descriptor.project_id,
|
||||||
|
fingerprint=fingerprint,
|
||||||
|
)
|
||||||
|
executable = Path(os.path.abspath(sys.executable))
|
||||||
|
try:
|
||||||
|
executable_status = executable.stat()
|
||||||
|
except OSError as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"client_configuration_unavailable",
|
||||||
|
"Current Python executable cannot be inspected",
|
||||||
|
) from error
|
||||||
|
if not stat.S_ISREG(executable_status.st_mode) or not os.access(executable, os.X_OK):
|
||||||
|
raise DocForgeError(
|
||||||
|
"client_configuration_unavailable",
|
||||||
|
"Current Python executable is not a runnable regular file",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
probe = subprocess.run(
|
||||||
|
[
|
||||||
|
str(executable),
|
||||||
|
"-I",
|
||||||
|
"-B",
|
||||||
|
"-c",
|
||||||
|
"import docforge.mcp_server",
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
timeout=10,
|
||||||
|
env=os.environ.copy(),
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.SubprocessError) as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"client_configuration_unavailable",
|
||||||
|
"Current isolated Python executable could not be probed safely",
|
||||||
|
) from error
|
||||||
|
if probe.returncode != 0:
|
||||||
|
raise DocForgeError(
|
||||||
|
"client_configuration_unavailable",
|
||||||
|
"Current isolated Python executable cannot import docforge.mcp_server",
|
||||||
|
)
|
||||||
|
arguments = [
|
||||||
|
"-I",
|
||||||
|
"-m",
|
||||||
|
"docforge.mcp_server",
|
||||||
|
"--project-root",
|
||||||
|
str(descriptor.root),
|
||||||
|
"--capability-mode",
|
||||||
|
selected_mode,
|
||||||
|
]
|
||||||
|
if proposal_writer is not None:
|
||||||
|
arguments.extend(("--proposal-writer", proposal_writer))
|
||||||
|
if canonical_applier is not None:
|
||||||
|
arguments.extend(("--canonical-applier", canonical_applier))
|
||||||
|
if no_ast:
|
||||||
|
arguments.append("--no-ast")
|
||||||
|
|
||||||
|
policy = compose_effective_policy(
|
||||||
|
selected_mode=selected_mode,
|
||||||
|
capability_source="explicit",
|
||||||
|
no_ast=no_ast,
|
||||||
|
diagnostics=False,
|
||||||
|
render_configured=descriptor.render is not None,
|
||||||
|
application_enabled=canonical_applier is not None,
|
||||||
|
)
|
||||||
|
artifact_format, content, warning = _artifact(
|
||||||
|
selected_client,
|
||||||
|
server_name=selected_name,
|
||||||
|
command=str(executable),
|
||||||
|
arguments=arguments,
|
||||||
|
startup_timeout=startup_seconds,
|
||||||
|
tool_timeout=tool_seconds,
|
||||||
|
)
|
||||||
|
if output is None:
|
||||||
|
validate_descriptor_binding(descriptor)
|
||||||
|
write_state = "not_requested"
|
||||||
|
durability = "not_applicable"
|
||||||
|
publication_warning = None
|
||||||
|
output_path = None
|
||||||
|
else:
|
||||||
|
validate_descriptor_binding(descriptor)
|
||||||
|
write_state, durability, publication_warning, published_path = _atomic_write(
|
||||||
|
output,
|
||||||
|
content,
|
||||||
|
validate_binding=lambda: validate_descriptor_binding(descriptor),
|
||||||
|
)
|
||||||
|
output_path = str(published_path) if published_path is not None else None
|
||||||
|
artifact = {
|
||||||
|
"format": artifact_format,
|
||||||
|
"content": content,
|
||||||
|
"content_sha256": hashlib.sha256(content.encode("utf-8")).hexdigest(),
|
||||||
|
"output_path": output_path,
|
||||||
|
"write_state": write_state,
|
||||||
|
"durability": durability,
|
||||||
|
}
|
||||||
|
binding = {
|
||||||
|
"transport": "stdio",
|
||||||
|
"capability_mode": selected_mode,
|
||||||
|
"adapter_policy": policy.adapter_policy(),
|
||||||
|
"render_policy": {
|
||||||
|
"manual": policy.manual_render,
|
||||||
|
"graph": policy.graph_render,
|
||||||
|
"live_viewer": policy.live_viewer,
|
||||||
|
},
|
||||||
|
"command": str(executable),
|
||||||
|
"args": arguments,
|
||||||
|
"environment": {},
|
||||||
|
"timeouts": {
|
||||||
|
"startup_seconds": startup_seconds,
|
||||||
|
"tool_seconds": tool_seconds,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
project_binding = {
|
||||||
|
"project_id": descriptor.project_id,
|
||||||
|
"project_root": str(descriptor.root),
|
||||||
|
"project_root_fingerprint": fingerprint,
|
||||||
|
"adapter": descriptor.adapter,
|
||||||
|
}
|
||||||
|
policy_payload = policy.as_dict()
|
||||||
|
plan_hash = document_hash(
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"client": selected_client,
|
||||||
|
"server_name": selected_name,
|
||||||
|
"project": project_binding,
|
||||||
|
"binding": binding,
|
||||||
|
"effective_policy": policy_payload,
|
||||||
|
"artifact_format": artifact_format,
|
||||||
|
"artifact_content_sha256": artifact["content_sha256"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result: dict[str, object] = {
|
||||||
|
"status": "ok",
|
||||||
|
"schema_version": 1,
|
||||||
|
"operation": "client.configure",
|
||||||
|
"action": "write" if output is not None else "preview",
|
||||||
|
"client": selected_client,
|
||||||
|
"server_name": selected_name,
|
||||||
|
"project": project_binding,
|
||||||
|
"binding": binding,
|
||||||
|
"effective_policy": policy_payload,
|
||||||
|
"artifact": artifact,
|
||||||
|
"configuration_hash": plan_hash,
|
||||||
|
"warnings": [
|
||||||
|
*([] if warning is None else [{"code": "timeout_format_unverified"}]),
|
||||||
|
*([] if publication_warning is None else [{"code": publication_warning}]),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
_validate_configuration_result(result)
|
||||||
|
return result
|
||||||
1287
src/docforge/doctor.py
Normal file
1287
src/docforge/doctor.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -11,6 +11,7 @@ import tempfile
|
||||||
import tomllib
|
import tomllib
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
from contextlib import suppress
|
||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass, replace
|
||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path, PurePosixPath
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
@ -39,6 +40,7 @@ from .telemetry import increment, stage
|
||||||
|
|
||||||
SOURCE_GENERATION_SCHEMA_VERSION = 1
|
SOURCE_GENERATION_SCHEMA_VERSION = 1
|
||||||
GENERIC_SOURCE_CONTRACT = "docforge-core:0.7.1:index:1"
|
GENERIC_SOURCE_CONTRACT = "docforge-core:0.7.1:index:1"
|
||||||
|
MAX_PROJECT_DESCRIPTOR_BYTES = 1_000_000
|
||||||
|
|
||||||
_CORE_METADATA = frozenset(
|
_CORE_METADATA = frozenset(
|
||||||
{
|
{
|
||||||
|
|
@ -210,12 +212,167 @@ def _receipt_signature(path: Path) -> tuple[int, int, int, int, int] | None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_descriptor(descriptor_path: Path) -> bytes:
|
||||||
|
try:
|
||||||
|
parent = descriptor_path.parent
|
||||||
|
parent_status = parent.lstat()
|
||||||
|
if (
|
||||||
|
stat.S_ISLNK(parent_status.st_mode)
|
||||||
|
or not stat.S_ISDIR(parent_status.st_mode)
|
||||||
|
or parent.resolve(strict=True) != parent
|
||||||
|
):
|
||||||
|
raise DocForgeError(
|
||||||
|
"project_descriptor_unsafe",
|
||||||
|
"Project descriptor parent must be one real confined directory",
|
||||||
|
)
|
||||||
|
directory_fd = os.open(
|
||||||
|
parent,
|
||||||
|
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as error:
|
||||||
|
raise DocForgeError("missing_config", "Missing .docforge/project.toml") from error
|
||||||
|
except DocForgeError:
|
||||||
|
raise
|
||||||
|
except OSError as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"project_descriptor_unsafe",
|
||||||
|
"Project descriptor cannot be inspected safely",
|
||||||
|
) from error
|
||||||
|
opened_parent = os.fstat(directory_fd)
|
||||||
|
if opened_parent.st_dev != parent_status.st_dev or opened_parent.st_ino != parent_status.st_ino:
|
||||||
|
with suppress(OSError):
|
||||||
|
os.close(directory_fd)
|
||||||
|
raise DocForgeError(
|
||||||
|
"project_descriptor_changed",
|
||||||
|
"Project descriptor parent changed while it was opened",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
|
||||||
|
def parent_current() -> bool:
|
||||||
|
try:
|
||||||
|
before = parent.lstat()
|
||||||
|
resolved = parent.resolve(strict=True)
|
||||||
|
after = parent.lstat()
|
||||||
|
opened = os.fstat(directory_fd)
|
||||||
|
return (
|
||||||
|
not stat.S_ISLNK(before.st_mode)
|
||||||
|
and stat.S_ISDIR(before.st_mode)
|
||||||
|
and resolved == parent
|
||||||
|
and (before.st_dev, before.st_ino, before.st_mode)
|
||||||
|
== (after.st_dev, after.st_ino, after.st_mode)
|
||||||
|
== (opened.st_dev, opened.st_ino, opened.st_mode)
|
||||||
|
)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not parent_current():
|
||||||
|
raise DocForgeError(
|
||||||
|
"project_descriptor_changed",
|
||||||
|
"Project descriptor parent changed before it was read",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
before = os.stat(
|
||||||
|
descriptor_path.name,
|
||||||
|
dir_fd=directory_fd,
|
||||||
|
follow_symlinks=False,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as error:
|
||||||
|
raise DocForgeError("missing_config", "Missing .docforge/project.toml") from error
|
||||||
|
except OSError as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"project_descriptor_unsafe",
|
||||||
|
"Project descriptor cannot be inspected safely",
|
||||||
|
) from error
|
||||||
|
if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):
|
||||||
|
raise DocForgeError(
|
||||||
|
"project_descriptor_unsafe",
|
||||||
|
"Project descriptor must be a regular file and not a symbolic link",
|
||||||
|
)
|
||||||
|
if before.st_size > MAX_PROJECT_DESCRIPTOR_BYTES:
|
||||||
|
raise DocForgeError(
|
||||||
|
"project_descriptor_oversized",
|
||||||
|
"Project descriptor exceeds the bounded configuration limit",
|
||||||
|
maximum_bytes=MAX_PROJECT_DESCRIPTOR_BYTES,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
descriptor = os.open(
|
||||||
|
descriptor_path.name,
|
||||||
|
os.O_RDONLY | os.O_NOFOLLOW,
|
||||||
|
dir_fd=directory_fd,
|
||||||
|
)
|
||||||
|
except OSError as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"project_descriptor_unsafe",
|
||||||
|
"Project descriptor cannot be opened safely",
|
||||||
|
) from error
|
||||||
|
try:
|
||||||
|
opened = os.fstat(descriptor)
|
||||||
|
if opened.st_dev != before.st_dev or opened.st_ino != before.st_ino:
|
||||||
|
raise DocForgeError(
|
||||||
|
"project_descriptor_changed",
|
||||||
|
"Project descriptor changed while it was opened",
|
||||||
|
)
|
||||||
|
chunks: list[bytes] = []
|
||||||
|
remaining = MAX_PROJECT_DESCRIPTOR_BYTES + 1
|
||||||
|
while remaining:
|
||||||
|
chunk = os.read(descriptor, min(65_536, remaining))
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
chunks.append(chunk)
|
||||||
|
remaining -= len(chunk)
|
||||||
|
raw = b"".join(chunks)
|
||||||
|
finally:
|
||||||
|
with suppress(OSError):
|
||||||
|
os.close(descriptor)
|
||||||
|
if len(raw) > MAX_PROJECT_DESCRIPTOR_BYTES:
|
||||||
|
raise DocForgeError(
|
||||||
|
"project_descriptor_oversized",
|
||||||
|
"Project descriptor exceeds the bounded configuration limit",
|
||||||
|
maximum_bytes=MAX_PROJECT_DESCRIPTOR_BYTES,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
after = os.stat(
|
||||||
|
descriptor_path.name,
|
||||||
|
dir_fd=directory_fd,
|
||||||
|
follow_symlinks=False,
|
||||||
|
)
|
||||||
|
except OSError as error:
|
||||||
|
raise DocForgeError(
|
||||||
|
"project_descriptor_changed",
|
||||||
|
"Project descriptor changed while it was read",
|
||||||
|
) from error
|
||||||
|
if (
|
||||||
|
before.st_dev,
|
||||||
|
before.st_ino,
|
||||||
|
before.st_size,
|
||||||
|
before.st_mtime_ns,
|
||||||
|
before.st_ctime_ns,
|
||||||
|
) != (
|
||||||
|
after.st_dev,
|
||||||
|
after.st_ino,
|
||||||
|
after.st_size,
|
||||||
|
after.st_mtime_ns,
|
||||||
|
after.st_ctime_ns,
|
||||||
|
):
|
||||||
|
raise DocForgeError(
|
||||||
|
"project_descriptor_changed",
|
||||||
|
"Project descriptor changed while it was read",
|
||||||
|
)
|
||||||
|
if not parent_current():
|
||||||
|
raise DocForgeError(
|
||||||
|
"project_descriptor_changed",
|
||||||
|
"Project descriptor parent changed while it was read",
|
||||||
|
)
|
||||||
|
return raw
|
||||||
|
finally:
|
||||||
|
with suppress(OSError):
|
||||||
|
os.close(directory_fd)
|
||||||
|
|
||||||
|
|
||||||
def _load_descriptor(root: Path) -> ProjectDescriptor:
|
def _load_descriptor(root: Path) -> ProjectDescriptor:
|
||||||
descriptor_path = root / ".docforge" / "project.toml"
|
descriptor_path = root / ".docforge" / "project.toml"
|
||||||
if not descriptor_path.is_file():
|
descriptor_bytes = _read_descriptor(descriptor_path)
|
||||||
raise DocForgeError("missing_config", "Missing .docforge/project.toml")
|
|
||||||
try:
|
try:
|
||||||
descriptor_bytes = descriptor_path.read_bytes()
|
|
||||||
document = cast(dict[str, object], tomllib.loads(descriptor_bytes.decode("utf-8")))
|
document = cast(dict[str, object], tomllib.loads(descriptor_bytes.decode("utf-8")))
|
||||||
except UnicodeDecodeError as error:
|
except UnicodeDecodeError as error:
|
||||||
raise DocForgeError("invalid_config", "Project descriptor is not UTF-8") from error
|
raise DocForgeError("invalid_config", "Project descriptor is not UTF-8") from error
|
||||||
|
|
@ -472,6 +629,17 @@ def _load_descriptor(root: Path) -> ProjectDescriptor:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_descriptor_binding(descriptor: ProjectDescriptor) -> None:
|
||||||
|
"""Require the bounded descriptor bytes to match one opened project binding."""
|
||||||
|
|
||||||
|
descriptor_bytes = _read_descriptor(descriptor.descriptor_path)
|
||||||
|
if hashlib.sha256(descriptor_bytes).hexdigest() != descriptor.descriptor_hash:
|
||||||
|
raise DocForgeError(
|
||||||
|
"source_changed",
|
||||||
|
"Project descriptor changed after the project was opened",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _markdown_record(path: Path, text: str) -> tuple[dict[str, Any], str]:
|
def _markdown_record(path: Path, text: str) -> tuple[dict[str, Any], str]:
|
||||||
lines = text.splitlines()
|
lines = text.splitlines()
|
||||||
if not lines or lines[0] != "+++":
|
if not lines or lines[0] != "+++":
|
||||||
|
|
@ -739,11 +907,7 @@ class Project:
|
||||||
|
|
||||||
def load(self) -> ProjectSnapshot:
|
def load(self) -> ProjectSnapshot:
|
||||||
increment("project_loads")
|
increment("project_loads")
|
||||||
descriptor_bytes = self.descriptor.descriptor_path.read_bytes()
|
validate_descriptor_binding(self.descriptor)
|
||||||
if hashlib.sha256(descriptor_bytes).hexdigest() != self.descriptor.descriptor_hash:
|
|
||||||
raise DocForgeError(
|
|
||||||
"source_changed", "Project descriptor changed after the project was opened"
|
|
||||||
)
|
|
||||||
ordered_sources, ordered_directories = self._canonical_inventory()
|
ordered_sources, ordered_directories = self._canonical_inventory()
|
||||||
generation_paths = (
|
generation_paths = (
|
||||||
self.descriptor.descriptor_path,
|
self.descriptor.descriptor_path,
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ OPERATION_NAMES = frozenset(
|
||||||
{
|
{
|
||||||
"test",
|
"test",
|
||||||
"benchmark.m1",
|
"benchmark.m1",
|
||||||
|
"benchmark.m2",
|
||||||
"mcp.invoke",
|
"mcp.invoke",
|
||||||
"mcp.bootstrap",
|
"mcp.bootstrap",
|
||||||
"mcp.sync",
|
"mcp.sync",
|
||||||
|
|
@ -116,6 +117,8 @@ OPERATION_NAMES = frozenset(
|
||||||
"cli.impact",
|
"cli.impact",
|
||||||
"cli.context",
|
"cli.context",
|
||||||
"cli.generation-diff",
|
"cli.generation-diff",
|
||||||
|
"cli.configure",
|
||||||
|
"cli.doctor",
|
||||||
"cli.render",
|
"cli.render",
|
||||||
"cli.render-status",
|
"cli.render-status",
|
||||||
"cli.preview",
|
"cli.preview",
|
||||||
|
|
|
||||||
1264
tests/test_client_integration.py
Normal file
1264
tests/test_client_integration.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -34,6 +34,10 @@ from docforge.viewer_manager import ViewerManagerClient
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
FIXTURES = ROOT / "tests" / "fixtures"
|
FIXTURES = ROOT / "tests" / "fixtures"
|
||||||
RESULT_SCHEMA = json.loads((ROOT / "schemas" / "result.schema.json").read_text())
|
RESULT_SCHEMA = json.loads((ROOT / "schemas" / "result.schema.json").read_text())
|
||||||
|
CLIENT_CONFIGURATION_SCHEMA = json.loads(
|
||||||
|
(ROOT / "schemas" / "client-configuration.schema.json").read_text()
|
||||||
|
)
|
||||||
|
DOCTOR_RESULT_SCHEMA = json.loads((ROOT / "schemas" / "doctor-result.schema.json").read_text())
|
||||||
ZERO_WORK_COUNTERS = (
|
ZERO_WORK_COUNTERS = (
|
||||||
"project_loads",
|
"project_loads",
|
||||||
"source_files_parsed",
|
"source_files_parsed",
|
||||||
|
|
@ -134,6 +138,16 @@ class TelemetryContractTests(unittest.TestCase):
|
||||||
set(COUNTER_NAMES),
|
set(COUNTER_NAMES),
|
||||||
set(properties["counters"]["properties"]),
|
set(properties["counters"]["properties"]),
|
||||||
)
|
)
|
||||||
|
for schema in (CLIENT_CONFIGURATION_SCHEMA, DOCTOR_RESULT_SCHEMA):
|
||||||
|
dedicated = schema["$defs"]["diagnostics"]["properties"]
|
||||||
|
self.assertEqual(
|
||||||
|
set(STAGE_NAMES),
|
||||||
|
set(dedicated["stages"]["propertyNames"]["enum"]),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
set(COUNTER_NAMES),
|
||||||
|
set(dedicated["counters"]["required"]),
|
||||||
|
)
|
||||||
|
|
||||||
def test_thread_and_async_request_contexts_are_isolated(self) -> None:
|
def test_thread_and_async_request_contexts_are_isolated(self) -> None:
|
||||||
barrier = threading.Barrier(2)
|
barrier = threading.Barrier(2)
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,8 @@ PUBLIC_IMPORTS = {
|
||||||
"CanonicalApplicationService",
|
"CanonicalApplicationService",
|
||||||
"GenericCanonicalApplier",
|
"GenericCanonicalApplier",
|
||||||
),
|
),
|
||||||
|
"docforge.client_config": ("generate_client_configuration",),
|
||||||
|
"docforge.doctor": ("run_doctor",),
|
||||||
"docforge.index": ("ProjectIndex",),
|
"docforge.index": ("ProjectIndex",),
|
||||||
"docforge.mcp_server": (
|
"docforge.mcp_server": (
|
||||||
"create_project_server",
|
"create_project_server",
|
||||||
|
|
@ -108,8 +110,10 @@ EXPECTED_CLI_COMMANDS = {
|
||||||
"backlinks",
|
"backlinks",
|
||||||
"build",
|
"build",
|
||||||
"check",
|
"check",
|
||||||
|
"configure",
|
||||||
"context",
|
"context",
|
||||||
"dependencies",
|
"dependencies",
|
||||||
|
"doctor",
|
||||||
"filter",
|
"filter",
|
||||||
"generation-diff",
|
"generation-diff",
|
||||||
"impact",
|
"impact",
|
||||||
|
|
|
||||||
1014
tools/milestone2_benchmark.py
Normal file
1014
tools/milestone2_benchmark.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue