Skip to content
Open
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,26 @@ To run this project in a Docker container, you'll need to pass your API keys as
```
</details>

## 📡 Publish to understand-quickly (opt-in)

Add `--publish` to land the generated tutorial in [`looptech-ai/understand-quickly`](https://github.com/looptech-ai/understand-quickly), a public registry of code-knowledge graphs that ships an MCP server. The flag emits a small `generic@1` JSON projection of the tutorial (abstractions/chapters as nodes, relationships as edges) at `<output>/<project>/tutorial.json` with `metadata.{tool, tool_version, generated_at}` plus `commit` when a local git repo is available (i.e. for `--dir`; not always populated for remote `--repo` crawls). If `UNDERSTAND_QUICKLY_TOKEN` is set, it also fires a `repository_dispatch` so the registry resyncs the entry.

```bash
python main.py --repo https://github.com/example/demo --publish
```

Without the token, only the local file is written. The drop-in CI step is the [`looptech-ai/uq-publish-action`](https://github.com/looptech-ai/uq-publish-action):

```yaml
- uses: looptech-ai/uq-publish-action@v0.1.0
with:
graph-path: 'output/<project>/tutorial.json'
format: 'generic@1'
token: ${{ secrets.UNDERSTAND_QUICKLY_TOKEN }}
```

Submitting via `--publish` is governed by the [Understand-Quickly Data License 1.0](https://github.com/looptech-ai/understand-quickly/blob/main/DATA-LICENSE.md). It is opt-in.

## 💡 Development Tutorial

- I built using [**Agentic Coding**](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to), the fastest development paradigm, where humans simply [design](docs/design.md) and agents [code](flow.py).
Expand Down
12 changes: 12 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ def main():
parser.add_argument("--no-cache", action="store_true", help="Disable LLM response caching (default: caching enabled)")
# Add max_abstraction_num parameter to control the number of abstractions
parser.add_argument("--max-abstractions", type=int, default=10, help="Maximum number of abstractions to identify (default: 10)")
# Opt-in publish to the understand-quickly registry of code-knowledge graphs.
# https://github.com/looptech-ai/understand-quickly
parser.add_argument(
"--publish",
action="store_true",
help="Emit a generic@1 knowledge-graph projection of the tutorial and (if "
"UNDERSTAND_QUICKLY_TOKEN is set) dispatch it to the understand-quickly "
"registry. Opt-in; default behavior is unchanged.",
)

args = parser.parse_args()

Expand Down Expand Up @@ -88,6 +97,9 @@ def main():
# Add max_abstraction_num parameter
"max_abstraction_num": args.max_abstractions,

# Opt-in publish to understand-quickly (looptech-ai/understand-quickly).
"publish_to_uq": args.publish,

# Outputs will be populated by the nodes
"files": [],
"abstractions": [],
Expand Down
28 changes: 28 additions & 0 deletions nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,3 +878,31 @@ def exec(self, prep_res):
def post(self, shared, prep_res, exec_res):
shared["final_output_dir"] = exec_res # Store the output path
print(f"\nTutorial generation complete! Files are in: {exec_res}")

# Opt-in: emit a generic@1 knowledge-graph projection and (if a token is
# set) publish to the understand-quickly registry. Failures here never
# affect tutorial generation — the markdown output is already written.
if shared.get("publish_to_uq"):
try:
from pathlib import Path
from utils.uq_publish import build_generic_graph, publish

source_dir = Path(shared["local_dir"]).resolve() if shared.get("local_dir") else None
graph = build_generic_graph(
project_name=shared["project_name"],
abstractions=shared.get("abstractions", []),
chapter_order=shared.get("chapter_order", []),
relationships=shared.get("relationships", {}),
repo_url=shared.get("repo_url"),
source_dir=source_dir,
files_data=shared.get("files"),
)
graph_path = Path(exec_res) / "tutorial.json"
publish(
graph,
graph_path,
repo_url=shared.get("repo_url"),
source_dir=source_dir,
)
except Exception as exc:
print(f"[uq-publish] warning: {exc}")
101 changes: 101 additions & 0 deletions tests/test_uq_publish.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Tests for utils/uq_publish.py — opt-in understand-quickly publish."""
from __future__ import annotations

import json
import os
import sys
import unittest
from pathlib import Path
from unittest import mock

# Add project root to path so `from utils.uq_publish import ...` works
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from utils.uq_publish import build_generic_graph, publish, TOKEN_ENV # noqa: E402


# Mirrors the actual upstream shape: IdentifyAbstractions emits `files` as a
# list of integer indices into shared["files"] (the (path, content) tuples).
SAMPLE_ABSTRACTIONS = [
{"name": "Flow", "description": "Pipeline orchestrator", "files": [0]},
{"name": "Node", "description": "Unit of work", "files": [1]},
]
SAMPLE_FILES_DATA = [
("flow.py", "class Flow:\n pass\n"),
("nodes.py", "class Node:\n pass\n"),
]
SAMPLE_RELATIONSHIPS = {
"summary": "PocketFlow runs nodes in a flow.",
"details": [{"from": 0, "to": 1, "label": "contains"}],
}


class BuildGenericGraphTests(unittest.TestCase):
def test_emits_generic_at_1_with_metadata(self) -> None:
graph = build_generic_graph(
project_name="demo",
abstractions=SAMPLE_ABSTRACTIONS,
chapter_order=[0, 1],
relationships=SAMPLE_RELATIONSHIPS,
repo_url="https://github.com/example/demo",
source_dir=None,
files_data=SAMPLE_FILES_DATA,
)
self.assertEqual(graph["schema"], "generic@1")
md = graph["metadata"]
self.assertEqual(md["tool"], "pocketflow-tutorial-codebase-knowledge")
self.assertEqual(md["project_name"], "demo")
self.assertTrue(md["generated_at"].endswith("Z"))
# Two abstractions -> two nodes; one relationship + one chapter-order edge.
self.assertEqual(len(graph["nodes"]), 2)
self.assertEqual(len(graph["edges"]), 2)
kinds = sorted(e["kind"] for e in graph["edges"])
self.assertEqual(kinds, ["next_chapter", "relationship"])
# File indices resolved to repo-relative paths via files_data.
self.assertEqual(graph["nodes"][0]["files"], ["flow.py"])
self.assertEqual(graph["nodes"][1]["files"], ["nodes.py"])
# chapter_index precomputed from chapter_order.
self.assertEqual(graph["nodes"][0]["chapter_index"], 0)
self.assertEqual(graph["nodes"][1]["chapter_index"], 1)

def test_no_files_data_exposes_indices_under_renamed_field(self) -> None:
graph = build_generic_graph(
project_name="demo",
abstractions=SAMPLE_ABSTRACTIONS,
chapter_order=[0, 1],
relationships=SAMPLE_RELATIONSHIPS,
repo_url=None,
source_dir=None,
)
# Without files_data, expose integer indices as `file_indices` (not
# `files`) so downstream consumers know they're not paths.
self.assertNotIn("files", graph["nodes"][0])
self.assertEqual(graph["nodes"][0]["file_indices"], [0])


class PublishTests(unittest.TestCase):
def test_no_token_writes_file_and_skips_dispatch(self) -> None:
graph = build_generic_graph(
project_name="demo",
abstractions=SAMPLE_ABSTRACTIONS,
chapter_order=[0, 1],
relationships=SAMPLE_RELATIONSHIPS,
repo_url=None,
source_dir=None,
)
env = {k: v for k, v in os.environ.items() if k != TOKEN_ENV}
with mock.patch.dict(os.environ, env, clear=True):
import tempfile
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp) / "tutorial.json"
result = publish(graph, out, source_dir=Path(tmp))
self.assertFalse(result["dispatched"])
self.assertTrue(out.exists())
data = json.loads(out.read_text())
self.assertEqual(data["schema"], "generic@1")
self.assertEqual(data["metadata"]["tool"],
"pocketflow-tutorial-codebase-knowledge")


if __name__ == "__main__":
unittest.main()
Loading