generated from coulomb/repo-seed
one time bootstrap path
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -7,6 +7,7 @@ from repo_registry.core.models import ObservedFact
|
||||
|
||||
|
||||
INDEXED_FACT_KINDS = {
|
||||
"intent",
|
||||
"scope",
|
||||
"documentation",
|
||||
"example",
|
||||
|
||||
1
src/repo_registry/intent/__init__.py
Normal file
1
src/repo_registry/intent/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Intent-file helpers for repository scoping."""
|
||||
130
src/repo_registry/intent/bootstrap.py
Normal file
130
src/repo_registry/intent/bootstrap.py
Normal 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())
|
||||
@@ -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/")):
|
||||
|
||||
Reference in New Issue
Block a user