Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,6 @@ jobs:

- name: Check proposal tracking gate
run: make proposal-tracking-gate

- name: Check DocC documentation sync
run: make docc-sync
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ credentials, private keys, or machine-local tokens to `.0al`.
- For any change affecting SpecGraph tooling or SpecGraph specifications, do not let proposals accumulate separately from runtime realization; close the loop through `observe -> propose -> improve tools -> observe again`.
- When adding or changing proposal markdown, include the source draft plus the required proposal tracking material: runtime registry, promotion registry/trace, or an explicit no-runtime classification. Verify with `make proposal-tracking-gate`.
- When project work reveals reusable lessons, update [CONTRIBUTING.md](CONTRIBUTING.md); if the lesson changes mandatory agent behavior, update this file too.
- Keep DocC documentation synchronized with ordinary repository documentation. When changing `docs/`, `README.md`, `CONTRIBUTING.md`, `AGENTS.md`, or `tools/README.md` in a way that affects published technical guidance, update the corresponding `Sources/SpecGraph/Documentation.docc/` page or the DocC sync contract, then run `make docc-sync`.
- When addressing actionable PR review threads, treat review feedback as process evidence: classify the root cause, add or name a prevention action such as a regression test, validator, policy rule, documentation rule, or agent instruction, record verification, and only use accepted risk when prevention is intentionally deferred. Use [tools/review_feedback_policy.json](tools/review_feedback_policy.json) as the vocabulary source.
- When operating the supervisor, use the repo-local skills under [`.codex/skills`](.codex/skills) as the default operational wrapper before ad hoc CLI usage; especially `specgraph-supervisor`, `specgraph-supervisor-gate-review`, and `specgraph-supervisor-child-materialize`.
- Prefer the repository Makefile shortcuts for routine supervisor/viewer/test operations instead of direct verbose commands: `make viewer-surfaces`, `make dashboard`, `make backlog`, `make next-move`, `make spec-activity`, `make proposal-spec-trace`, `make proposal-tracking`, `make proposal-tracking-gate`, `make external-consumers`, `make external-handoffs`, `make metrics-delivery`, `make metrics-feedback`, `make metrics-source-promotion`, `make metric-signals`, `make metric-thresholds`, `make metric-packs`, `make metric-pack-drift`, `make metric-pack-adapters`, `make metric-pack-runs`, `make metric-pricing`, `make model-usage`, `make conversation-memory`, `make conversation-memory-map`, `make conversation-memory-pressure`, `make pre-spec-semantics`, `make implementation-work`, `make supervisor-evidence-packet`, `make supervisor-stalled-run-salvage`, `make factory-architecture`, `make swift-typed-tooling`, `make project-environment`, `make init-product-workspace`, `make review-feedback`, `make publish-bundle`, `make test`, and `make test-supervisor`. Use direct `python3 tools/supervisor.py ... --output-mode full` only when a task explicitly needs the complete artifact on stdout.
- Prefer the repository Makefile shortcuts for routine supervisor/viewer/test operations instead of direct verbose commands: `make viewer-surfaces`, `make dashboard`, `make backlog`, `make next-move`, `make spec-activity`, `make proposal-spec-trace`, `make proposal-tracking`, `make proposal-tracking-gate`, `make external-consumers`, `make external-handoffs`, `make metrics-delivery`, `make metrics-feedback`, `make metrics-source-promotion`, `make metric-signals`, `make metric-thresholds`, `make metric-packs`, `make metric-pack-drift`, `make metric-pack-adapters`, `make metric-pack-runs`, `make metric-pricing`, `make model-usage`, `make conversation-memory`, `make conversation-memory-map`, `make conversation-memory-pressure`, `make pre-spec-semantics`, `make implementation-work`, `make supervisor-evidence-packet`, `make supervisor-stalled-run-salvage`, `make factory-architecture`, `make swift-typed-tooling`, `make project-environment`, `make init-product-workspace`, `make review-feedback`, `make docc-sync`, `make publish-bundle`, `make test`, and `make test-supervisor`. Use direct `python3 tools/supervisor.py ... --output-mode full` only when a task explicitly needs the complete artifact on stdout.
- Treat generated supervisor runtime artifacts as local by default, including `.worktrees/` and `runs/*`; only intentionally curated artifacts should be promoted into tracked documentation or specification surfaces.
- Tool-related work belongs under `/tools` (including code when needed).
- Test-related work belongs under `/tests` (including test code).
Expand Down
29 changes: 28 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ Proposals are not a parking lot. If a proposal introduces a new surface or proce
observe -> propose -> improve tools -> observe again
```

Use proposals for new semantics, governance rules, viewer contracts, or runtime processes. Use runtime realization PRs when the proposal already exists and the task is to register or implement its derived surface.
Use proposals under `docs/proposals/` for new semantics, governance rules,
viewer contracts, or runtime processes. Use runtime realization PRs when the
proposal already exists and the task is to register or implement its derived
surface or runtime evidence.

Useful patterns:

Expand Down Expand Up @@ -163,6 +166,30 @@ Common viewer-facing surfaces include:
- `runs/spec_activity_feed.json`
- `runs/conversation_memory_*.json`

## DocC Documentation Sync

DocC is the hosted technical documentation surface, but repository Markdown
remains the canonical working documentation for contributors and operators.
When changing ordinary docs in a way that affects public technical guidance,
update the matching DocC mirror in `Sources/SpecGraph/Documentation.docc/`.

The synchronization contract lives in
`tools/docc_sync_contract.json`. It names documentation groups, the ordinary
source files, the DocC mirror pages, and required anchors that must appear in
all grouped files. Use this when adding a new documentation surface or changing
the terms that prove the surfaces still describe the same operational contract.

Run the gate before opening or merging documentation PRs that touch synchronized
surfaces:

```bash
make docc-sync
```

This gate is intentionally anchor-based rather than byte-for-byte comparison:
DocC pages can have different navigation and formatting, but they must preserve
the same operational facts, paths, commands, and boundary terms.

## Metrics

Metrics should be treated as diagnostic plugins or metric packs, not as hard-coded truth. SpecGraph should preserve the distinction between:
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ help:
' make project-environment Refresh project environment governance profile JSON' \
' make init-product-workspace PRODUCT_WORKSPACE_PROJECT_ID=<id> PRODUCT_WORKSPACE_ROOT=<path>' \
' make review-feedback Refresh review feedback index' \
' make docc-sync Validate DocC mirrors against repository docs' \
' make publish-bundle Build static specs/ + runs/ publish bundle' \
' make test Run full Python test suite quietly' \
' make test-supervisor Run supervisor tests quietly'
Expand Down Expand Up @@ -197,6 +198,10 @@ init-product-workspace:
review-feedback:
@$(PYTHON) $(SUPERVISOR) --build-review-feedback-index

.PHONY: docc-sync
docc-sync:
@$(PYTHON) tools/validate_docc_sync.py
Comment thread
SoundBlaster marked this conversation as resolved.

.PHONY: publish-bundle
publish-bundle:
@$(PYTHON) tools/build_static_artifact_bundle.py --refresh-publish-surfaces
Expand Down
51 changes: 51 additions & 0 deletions tests/test_docc_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

import importlib.util
import json
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]


def _load_docc_sync_module():
module_path = ROOT / "tools" / "validate_docc_sync.py"
spec = importlib.util.spec_from_file_location("_validate_docc_sync_under_test", module_path)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


def test_docc_sync_contract_passes() -> None:
module = _load_docc_sync_module()

assert module.validate(ROOT / "tools" / "docc_sync_contract.json") == []


def test_docc_sync_rejects_empty_synchronized_documents(tmp_path) -> None:
module = _load_docc_sync_module()
source = tmp_path / "empty.md"
source.write_text("", encoding="utf-8")
mirror = tmp_path / "mirror.md"
mirror.write_text("anchor\n", encoding="utf-8")
contract = tmp_path / "contract.json"
contract.write_text(
json.dumps(
{
"groups": [
{
"id": "empty-source",
"docs": [str(source)],
"docc": [str(mirror)],
"required_terms": ["anchor"],
}
]
}
),
encoding="utf-8",
)

assert module.validate(contract) == [
f"empty-source: {source} is empty",
f"empty-source: {source} is missing required term 'anchor'",
]
72 changes: 72 additions & 0 deletions tools/docc_sync_contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"groups": [
{
"id": "static-artifact-publishing",
"description": "Static artifact and GitHub Pages publication rules must stay aligned between repository docs and DocC.",
"docs": [
"docs/static_artifact_publish.md"
],
"docc": [
"Sources/SpecGraph/Documentation.docc/ArtifactPublishing.md"
],
"required_terms": [
"GitHub Pages",
"documentation/SpecGraph/",
"documentation/specgraph/",
"landing/"
],
"docs_required_terms": [
"artifact_manifest.json",
"checksums.sha256",
"make publish-bundle"
],
"docc_required_terms": [
"Static Host",
"product-facing surfaces"
]
},
{
"id": "proposal-runtime-evidence",
"description": "Proposal tracking and runtime evidence rules must stay visible in both contributor docs and DocC.",
"docs": [
"CONTRIBUTING.md"
],
"docc": [
"Sources/SpecGraph/Documentation.docc/ProposalsAndRuntime.md"
],
"required_terms": [
"proposal",
"runtime evidence",
"docs/proposals/",
"make proposal-tracking-gate"
],
"docs_required_terms": [
"Proposal tracking"
],
"docc_required_terms": [
"proposal tracking"
]
},
{
"id": "operator-source-map",
"description": "The DocC root must keep pointing operators back to canonical repository documentation sources.",
"docs": [
"README.md",
"AGENTS.md",
"tools/README.md"
],
Comment thread
SoundBlaster marked this conversation as resolved.
"docc": [
"Sources/SpecGraph/Documentation.docc/SpecGraph.md"
],
"docs_required_terms": [
"SpecGraph"
],
"docc_required_terms": [
"README.md",
"CONTRIBUTING.md",
"AGENTS.md",
"tools/README.md"
]
}
]
}
123 changes: 123 additions & 0 deletions tools/validate_docc_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""Validate that repository docs and DocC mirror pages stay synchronized."""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Any

ROOT = Path(__file__).resolve().parents[1]
DEFAULT_CONTRACT = ROOT / "tools" / "docc_sync_contract.json"


def _load_contract(path: Path) -> dict[str, Any]:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(f"DocC sync contract not found: {path}") from None
except json.JSONDecodeError as exc:
raise SystemExit(f"Invalid DocC sync contract JSON at {path}: {exc}") from exc

if not isinstance(data, dict) or not isinstance(data.get("groups"), list):
raise SystemExit("DocC sync contract must contain a top-level 'groups' list")
return data


def _read_text(relative_path: str, errors: list[str]) -> str:
path = Path(relative_path)
if not path.is_absolute():
path = ROOT / path
if not path.is_file():
errors.append(f"missing file: {relative_path}")
return ""
return path.read_text(encoding="utf-8")


def _validate_group(group: dict[str, Any], errors: list[str]) -> None:
group_id = str(group.get("id", "<missing-id>"))
docs = group.get("docs")
docc = group.get("docc")
required_terms = group.get("required_terms", [])
docs_required_terms = group.get("docs_required_terms", [])
docc_required_terms = group.get("docc_required_terms", [])

if not isinstance(docs, list) or not docs:
errors.append(f"{group_id}: 'docs' must be a non-empty list")
return
if not isinstance(docc, list) or not docc:
errors.append(f"{group_id}: 'docc' must be a non-empty list")
return
for field_name, value in (
("required_terms", required_terms),
("docs_required_terms", docs_required_terms),
("docc_required_terms", docc_required_terms),
):
if not isinstance(value, list):
errors.append(f"{group_id}: '{field_name}' must be a list")
return

if not required_terms and not docs_required_terms and not docc_required_terms:
errors.append(
f"{group_id}: at least one of 'required_terms', "
"'docs_required_terms', or 'docc_required_terms' must be non-empty"
)
return

texts = {str(path): _read_text(str(path), errors) for path in [*docs, *docc]}

term_sets: dict[str, list[Any]] = {}
for path in docs:
term_sets[str(path)] = [*required_terms, *docs_required_terms]
for path in docc:
term_sets[str(path)] = [*required_terms, *docc_required_terms]

for path, text in texts.items():
if not text.strip():
errors.append(f"{group_id}: {path} is empty")
for term in term_sets[path]:
Comment thread
SoundBlaster marked this conversation as resolved.
if not isinstance(term, str) or not term:
errors.append(f"{group_id}: required term must be a non-empty string")
continue
if term not in text:
errors.append(f"{group_id}: {path} is missing required term {term!r}")


def validate(contract_path: Path) -> list[str]:
contract = _load_contract(contract_path)
errors: list[str] = []
for raw_group in contract["groups"]:
if not isinstance(raw_group, dict):
errors.append("DocC sync contract group must be an object")
continue
_validate_group(raw_group, errors)
return errors


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Validate synchronized coverage between repository docs and DocC pages."
)
parser.add_argument(
"--contract",
type=Path,
default=DEFAULT_CONTRACT,
help="Path to the DocC sync contract JSON.",
)
args = parser.parse_args(argv)

errors = validate(args.contract)
if errors:
print("DocC sync validation failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
return 1

print("DocC sync validation passed.")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading