one time bootstrap path

This commit is contained in:
2026-05-02 00:36:00 +02:00
parent 911ca45618
commit 76f5ecb1b4
12 changed files with 328 additions and 27 deletions

View File

@@ -63,8 +63,7 @@ class CandidateGraphGenerator:
return []
chunks = chunks or []
scope_docs = self._facts(facts, "scope")
docs = scope_docs + self._facts(facts, "documentation")
docs = self._facts(facts, "intent") + self._facts(facts, "documentation")
tests = self._facts(facts, "test")
examples = self._facts(facts, "example")
interfaces = self._facts(facts, "interface")
@@ -662,7 +661,7 @@ class CandidateGraphGenerator:
def _document_purpose_sentence(self, chunks: list[ContentChunk]) -> str:
for chunk in self._documentation_chunks(chunks):
if chunk.kind not in {"scope", "documentation"}:
if chunk.kind not in {"intent", "documentation"}:
continue
lines = [line.strip() for line in chunk.text.splitlines() if line.strip()]
paragraph = next((line for line in lines if not line.startswith("#")), "")
@@ -745,8 +744,8 @@ class CandidateGraphGenerator:
def _documentation_chunks(self, chunks: list[ContentChunk]) -> list[ContentChunk]:
return sorted(
[chunk for chunk in chunks if chunk.kind in {"scope", "documentation"}],
key=lambda chunk: (0 if chunk.kind == "scope" else 1, chunk.path, chunk.start_line),
[chunk for chunk in chunks if chunk.kind in {"intent", "documentation"}],
key=lambda chunk: (0 if chunk.kind == "intent" else 1, chunk.path, chunk.start_line),
)
def _interface_summary(self, chunks: list[ContentChunk]) -> str:

View File

@@ -7,6 +7,7 @@ from repo_registry.core.models import ObservedFact
INDEXED_FACT_KINDS = {
"intent",
"scope",
"documentation",
"example",

View File

@@ -0,0 +1 @@
"""Intent-file helpers for repository scoping."""

View File

@@ -0,0 +1,130 @@
from __future__ import annotations
import argparse
from dataclasses import dataclass
from datetime import date
from pathlib import Path
from typing import Iterable
BOOTSTRAP_NOTE = (
"> Bootstrapped from `SCOPE.md` by repo-scoping.\n"
"> Review and edit this file as design intent. `SCOPE.md` remains the\n"
"> derived current-scope artifact."
)
@dataclass(frozen=True)
class IntentBootstrapResult:
repo_path: str
scope_path: str
intent_path: str
status: str
message: str
def bootstrap_intent_from_scope(
repo_path: str | Path,
*,
dry_run: bool = False,
overwrite: bool = False,
today: date | None = None,
) -> IntentBootstrapResult:
root = Path(repo_path).expanduser().resolve()
scope_path = root / "SCOPE.md"
intent_path = root / "INTENT.md"
if not root.is_dir():
return _result(root, scope_path, intent_path, "missing_repo", "repository path does not exist")
if not scope_path.is_file():
return _result(root, scope_path, intent_path, "missing_scope", "SCOPE.md is not present")
if intent_path.exists() and not overwrite:
return _result(root, scope_path, intent_path, "exists", "INTENT.md already exists")
status = "would_overwrite" if intent_path.exists() else "would_create"
if dry_run:
return _result(root, scope_path, intent_path, status, f"{status} INTENT.md from SCOPE.md")
intent_text = scope_to_intent_text(
scope_path.read_text(encoding="utf-8"),
today=today,
)
intent_path.write_text(intent_text, encoding="utf-8")
created_status = "overwritten" if status == "would_overwrite" else "created"
return _result(root, scope_path, intent_path, created_status, f"{created_status} INTENT.md from SCOPE.md")
def bootstrap_many(
repo_paths: Iterable[str | Path],
*,
dry_run: bool = False,
overwrite: bool = False,
today: date | None = None,
) -> list[IntentBootstrapResult]:
return [
bootstrap_intent_from_scope(
repo_path,
dry_run=dry_run,
overwrite=overwrite,
today=today,
)
for repo_path in repo_paths
]
def scope_to_intent_text(scope_text: str, *, today: date | None = None) -> str:
current_date = today or date.today()
lines = scope_text.splitlines()
while lines and not lines[0].strip():
lines.pop(0)
if lines and lines[0].lstrip().lower().startswith("# scope"):
lines[0] = "# INTENT"
elif not lines or not lines[0].startswith("#"):
lines.insert(0, "# INTENT")
note = f"{BOOTSTRAP_NOTE}\n> Bootstrap date: {current_date.isoformat()}"
insert_at = 1 if lines else 0
while insert_at < len(lines) and not lines[insert_at].strip():
insert_at += 1
lines[insert_at:insert_at] = ["", note, ""]
return "\n".join(lines).rstrip() + "\n"
def _result(
root: Path,
scope_path: Path,
intent_path: Path,
status: str,
message: str,
) -> IntentBootstrapResult:
return IntentBootstrapResult(
repo_path=str(root),
scope_path=str(scope_path),
intent_path=str(intent_path),
status=status,
message=message,
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Bootstrap INTENT.md from SCOPE.md for repositories that do not have intent files yet."
)
parser.add_argument("repo_paths", nargs="+", help="Repository checkout path(s) to inspect")
parser.add_argument("--dry-run", action="store_true", help="Report planned writes without writing files")
parser.add_argument("--overwrite", action="store_true", help="Overwrite existing INTENT.md files")
args = parser.parse_args(argv)
results = bootstrap_many(
args.repo_paths,
dry_run=args.dry_run,
overwrite=args.overwrite,
)
for result in results:
print(f"{result.status}\t{result.repo_path}\t{result.message}")
return 1 if any(result.status in {"missing_repo", "missing_scope"} for result in results) else 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -180,13 +180,22 @@ class DeterministicScanner:
name = path.name.lower()
source_role = self._source_role(relative)
if name == "scope.md":
if name == "intent.md":
facts.append(
FactCandidate(
"intent",
"INTENT",
relative,
metadata={"source_role": "intent_summary"},
)
)
elif name == "scope.md":
facts.append(
FactCandidate(
"scope",
"SCOPE",
relative,
metadata={"source_role": "scope_summary"},
metadata={"source_role": "derived_scope"},
)
)
elif name.startswith("readme"):
@@ -429,8 +438,10 @@ class DeterministicScanner:
lower = relative_path.lower()
parts = lower.split("/")
name = parts[-1]
if name == "intent.md":
return "intent_summary"
if name == "scope.md":
return "scope_summary"
return "derived_scope"
if name in AGENT_GUIDANCE_FILES or any(part in AGENT_GUIDANCE_DIRS for part in parts):
return "agent_guidance"
if lower.startswith((".github/workflows/", ".gitea/workflows/")):