Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
83f27de
Export architecture concepts in explorer bundles
riyandhiman14 Sep 5, 2026
d34222b
Export architecture concepts in Explorer bundles
riyandhiman14 Sep 5, 2026
0dc8588
Export bounded core spine for code exploration
riyandhiman14 Sep 5, 2026
e972a43
Keep core spine source-backed
riyandhiman14 Sep 5, 2026
1b2c2f6
Export recorded symbol documentation
riyandhiman14 Sep 5, 2026
394cd09
Consume curated tour manifests in exports
riyandhiman14 Sep 5, 2026
b18ae77
Harden curated tour CLI input
riyandhiman14 Sep 5, 2026
110efbd
preserve curated visibility selection
riyandhiman14 Sep 5, 2026
2672a69
preserve curated project overview
riyandhiman14 Sep 5, 2026
8211eaf
preserve curated concept map
riyandhiman14 Sep 5, 2026
92cbd1e
config: add lachesis.yml and exclude non-product code from builds
riyandhiman14 Sep 6, 2026
d7ba854
nav(bundle): root request paths in app code and drop dataflow artifacts
riyandhiman14 Sep 6, 2026
6ae6a91
nav(bundle): gate test/example scaffolding out of the export projection
riyandhiman14 Sep 6, 2026
dae6256
Merge origin/dev into feat/lachesis-config-nonproduct-exclusion
riyandhiman14 Sep 6, 2026
da98724
nav(bundle): feature the primary language and gate vendored/generated…
riyandhiman14 Sep 6, 2026
25a4720
nav(bundle): normalize node locations and enforce the code-understand…
riyandhiman14 Sep 6, 2026
98162ab
nav(bundle): root request lifecycles at the dispatcher, not the tramp…
riyandhiman14 Sep 6, 2026
b1a7717
nav(export): rank lifecycle roots by control cone so the real dispatc…
riyandhiman14 Sep 6, 2026
6881889
nav(export): route the lifecycle spine to its result terminus, name h…
riyandhiman14 Sep 6, 2026
af9024c
Merge pull request #64 from UnboundCompute/feat/lachesis-config-nonpr…
riyandhiman14 Sep 6, 2026
d96e11a
release: cut 0.5.2 (version, changelog, server.json, crate)
riyandhiman14 Sep 6, 2026
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
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,55 @@ Lachesis is pre-1.0. Until 1.0 the graph schema, the query surface and the MCP t
may change between minor versions; those changes are called out here explicitly rather
than left for you to discover.

## [0.5.2]

Code-understanding release. 0.5.1 gave the 2.0 Explorer bundle a comprehension
layer; this release makes that layer read like something a newcomer could follow and
scopes it to the code that actually ships. A project can now declare what is and is
not its own source, and the guided request walks, hop captions and concept areas are
reworked so the projection reads as prose over the real call graph rather than a wall
of symbols. Version, projection and build-scoping only: no graph-schema, query-engine
or candidate-surface changes, and the schema 1.0 security bundle is unchanged.

### Added

- **`lachesis.yml` project configuration.** A repository can declare its own build and
export knobs in a single `lachesis.yml` at the tree root, resolved from the analysis
root upward. Vendored, generated, build-config and test/example scaffolding are
excluded from the build and from the export projection by default, so the graph and
the comprehension surface describe the product code rather than its dependencies and
fixtures; the defaults are overridable per project.
- **Human-readable hop captions.** Each hop in a guided request walk keeps its exact
symbol (still greppable) and gains a `reads_as` phrase derived from the symbol's own
morphology — a leading action verb rendered over its object tokens — so a reader
follows the lifecycle in plain language without losing the identifier.
- **Module-centric concepts.** `graph.concepts` are now the module areas a newcomer
would name — one per product file, ranked by how much it defines and labelled by its
own module stem — replacing the single coarse call-community that a flat package
collapsed into.
- **Curated tour manifests and recorded symbol documentation** are consumed by the
export when present, and a bounded, source-backed core spine is exported for
exploration.

### Changed

- **Request lifecycles root at the real dispatcher and reach their result.** A guided
request now begins at the function that actually dispatches the work rather than the
thin entry trampoline, and the spine is routed forward — over edges recovered from
the call graph, not just the story tree — to the hop that constructs the returned
result. Lifecycle roots are ranked by their control cone so the genuine dispatcher
leads. Entrypoints, request roots and concepts are gated to the repository's primary
language so the projection stays in the repo's own idiom. This changes 2.0
comprehension output by design; the strict source-backing on every emitted hop is
unchanged.

### Fixed

- Node locations are normalized (a null file, line or end-line reads as `""`/`0`
rather than a missing key), and the code-understanding contract is enforced — a
code-understanding projection with no openable entrypoints is rejected rather than
emitted empty.

## [0.5.1]

Explorer-bundle comprehension release. The graph-first 2.0 Explorer bundle already
Expand Down
48 changes: 45 additions & 3 deletions lachesis/cli/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,20 @@ def _run(argv: list[str] | None = None) -> None:
"exists to reach is never scoped out. An explicitly named file is always "
"kept; a directory is walked with the same ignore rules as source_dir.",
)
parser.add_argument(
"--config", metavar="FILE", default=None,
help="path to a lachesis.yml config file. When omitted, the tree is searched "
"upward from source_dir for lachesis.yml (or .yaml/.lachesis.* variants). "
"The config sets what a build ingests, size caps, and runtime knobs; its "
"built-in default excludes tests, examples, docs, fixtures, benchmarks and "
"vendored trees from the graph.",
)
parser.add_argument(
"--all-sources", action="store_true",
help="disable the default non-product exclusion and compile the whole tree -- "
"tests, examples, docs and vendored code included. Equivalent to a config "
"with `build.exclude: []`, and wins over any config file for this run.",
)
args = parser.parse_args(argv)
# Validate the source tree up front. Without this the streaming build path
# happily runs against a nonexistent path or a single file, finds no frontend
Expand Down Expand Up @@ -300,6 +314,27 @@ def _run(argv: list[str] | None = None) -> None:
for included in include_paths:
if not os.path.exists(included):
parser.error(f"--include path does not exist: {included}")
# Resolve the project config (lachesis.yml). Its build.paths filter carries the
# non-product exclusion default -- tests, examples, docs, fixtures, benchmarks and
# vendored trees are dropped from the graph unless the tree opts back in with
# `build.exclude: []`/an allow-list, or this run passes --all-sources. The filter is
# threaded into every build variant *and* into source_content_hash, so a filtered
# build and its cache-validity key describe the very same file set. Any config knob
# that mirrors an env var (the `runtime:` block, atropos root) is applied to the
# environment here, before the first pipeline call reads it; setdefault keeps an
# inherited env var winning over the file, matching the documented precedence.
from lachesis import config as _config
try:
cfg = _config.load(start=args.source_dir, explicit=args.config)
except _config.ConfigError as error:
parser.error(str(error))
for warning in cfg.warnings:
print(f"lachesis config: {warning}", file=sys.stderr)
if cfg.source:
print(f"lachesis: using config {cfg.source}", file=sys.stderr)
_config.apply_runtime_env(cfg)
# --all-sources wins over the file: complete coverage, no exclusion.
path_filter = None if args.all_sources else cfg.build.paths
# --prune deletes pure-lexical/proof records at the store boundary, so apply the
# same output defaults before the streaming branch as the ordinary path below.
# Previously the early return skipped this block and made --stream-shards run
Expand All @@ -314,11 +349,13 @@ def _run(argv: list[str] | None = None) -> None:
args.source_dir, args.stream_shards, frontend_out,
timeout_seconds=args.timeout,
max_files_per_package=args.shard_large_packages,
path_filter=path_filter,
)
else:
readers, snapshots = run_project_streaming(
args.source_dir, args.stream_shards, frontend_out,
timeout_seconds=args.timeout, include_paths=include_paths,
path_filter=path_filter,
)
if not snapshots:
parser.error(
Expand Down Expand Up @@ -374,6 +411,7 @@ def _run(argv: list[str] | None = None) -> None:
readers, snapshots = run_project_streaming(
args.source_dir, stream_root, frontend_out,
timeout_seconds=args.timeout, include_paths=include_paths,
path_filter=path_filter,
)
if not snapshots:
parser.error(
Expand Down Expand Up @@ -402,17 +440,20 @@ def _run(argv: list[str] | None = None) -> None:
args.source_dir, frontend_out, enrich=compile_enrich,
max_workers=args.max_workers, timeout_seconds=args.timeout,
max_files_per_package=args.shard_large_packages,
path_filter=path_filter,
)
elif args.incremental:
graph, snapshots = run_project_incremental(args.source_dir, frontend_out,
enrich=compile_enrich,
timeout_seconds=args.timeout,
include_paths=include_paths)
include_paths=include_paths,
path_filter=path_filter)
else:
graph, snapshots = run_project(args.source_dir, frontend_out,
enrich=compile_enrich,
timeout_seconds=args.timeout,
include_paths=include_paths)
include_paths=include_paths,
path_filter=path_filter)
build_fingerprint = None
if args.incremental and frontend_out:
manifest_path = default_manifest_path(frontend_out)
Expand Down Expand Up @@ -444,7 +485,8 @@ def _run(argv: list[str] | None = None) -> None:
source_dir=args.source_dir if args.reduced else None,
# Hashed rather than assumed: the store records what the tree was at build time,
# so a load can tell whether an already-joined cache still describes it.
source_content_hash=(source_content_hash(args.source_dir, include_paths=include_paths)
source_content_hash=(source_content_hash(args.source_dir, include_paths=include_paths,
path_filter=path_filter)
if args.reduced else None),
build_fingerprint=build_fingerprint,
)
Expand Down
32 changes: 32 additions & 0 deletions lachesis/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,17 @@ def git(*a: str) -> str | None:
return repo, commit


def _load_curated_tour(path: str) -> dict:
"""Load an OSS tour fragment without accepting a verified owner claim."""
config = json.loads(Path(path).expanduser().read_text(encoding="utf-8"))
curated_tour = config.get("meta", {}).get("curated_tour", config.get("curated_tour"))
if not isinstance(curated_tour, dict):
raise ValueError("curated tour file must contain meta.curated_tour")
curated_tour = dict(curated_tour)
curated_tour.pop("maintainer", None)
return curated_tour


def command_trace(args: argparse.Namespace) -> int:
"""Build (or reuse) a graph and export a lachesis-explorer bundle.json."""
from lachesis.cli.indexer import (EnvironmentProblem, NoSourceFound,
Expand Down Expand Up @@ -389,6 +400,13 @@ def command_trace(args: argparse.Namespace) -> int:

repo, commit = _repo_meta(source)
progress.phase("exporting bundle")
curated_tour = None
if args.curated_tour:
try:
curated_tour = _load_curated_tour(args.curated_tour)
except (OSError, UnicodeError, json.JSONDecodeError, AttributeError, ValueError) as error:
_stderr(f"lachesis trace: curated tour: {error}")
return EXIT_USAGE
try:
bundle = bundle_mod.build_bundle(
str(graph_path),
Expand All @@ -401,6 +419,7 @@ def command_trace(args: argparse.Namespace) -> int:
schema_version=args.schema_version,
source_url_template=args.source_url_template,
description=args.description,
curated_tour=curated_tour,
)
except Exception as error: # noqa: BLE001 - CLI turns export errors into one line
_stderr(f"lachesis trace: {error}")
Expand Down Expand Up @@ -657,6 +676,10 @@ def command_build(args: argparse.Namespace) -> int:
forwarded.extend(["--stream-shards", args.stream_shards])
for included in getattr(args, "include_paths", None) or []:
forwarded.extend(["--include", included])
if getattr(args, "config", None):
forwarded.extend(["--config", args.config])
if getattr(args, "all_sources", False):
forwarded.append("--all-sources")
return analyze.main(forwarded)


Expand Down Expand Up @@ -947,6 +970,13 @@ def build_parser() -> argparse.ArgumentParser:
help="also analyse this file or directory even if it is outside "
"source_dir (repeatable); point it at an advisory's file so a "
"narrowed scope never excludes the file the run must reach")
build.add_argument("--config", metavar="FILE", default=None,
help="lachesis.yml to control this build (default: search upward "
"from source_dir). Its built-in default excludes tests, "
"examples, docs, fixtures, benchmarks and vendored trees.")
build.add_argument("--all-sources", action="store_true",
help="compile the whole tree, including tests/examples/docs/vendor "
"(disables the non-product exclusion; wins over any config)")
build.set_defaults(handler=command_build, no_prune=False)

trace = subcommands.add_parser(
Expand All @@ -971,6 +1001,8 @@ def build_parser() -> argparse.ArgumentParser:
help="explicit HTTP(S) source template using {file}, {line}, {end_line}, {revision}")
trace.add_argument("--description", metavar="TEXT",
help="one-line projection description recorded in bundle meta (2.0)")
trace.add_argument("--curated-tour", metavar="JSON",
help="read a meta.curated_tour fragment and validate it against the exported paths")
trace.add_argument("--per-family", type=_positive_int, default=6, metavar="N",
help="max leads to draw from each sink family (default: 6)")
trace.add_argument("--max-flows", type=_positive_int, default=40, metavar="N",
Expand Down
21 changes: 19 additions & 2 deletions lachesis/cli/test_cli_args.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import json
import tempfile
import unittest
from pathlib import Path


from lachesis.cli.main import build_parser
from lachesis.cli.main import _load_curated_tour, build_parser


class CliArgumentTests(unittest.TestCase):
Expand All @@ -28,7 +31,21 @@ def test_scan_rejects_invalid_limits_and_ranks(self):
with self.subTest(option=option, value=value):
with self.assertRaises(SystemExit) as raised:
parser.parse_args(["scan", option, value])
self.assertEqual(raised.exception.code, 2)
self.assertEqual(raised.exception.code, 2)

def test_curated_tour_argument_and_loader_strip_unverified_identity(self):
args = build_parser().parse_args(["trace", "--curated-tour", "lachesis-tour.json"])
self.assertEqual(args.curated_tour, "lachesis-tour.json")
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "tour.json"
path.write_text(json.dumps({"meta": {"curated_tour": {
"id": "tour.start", "title": "Start here",
"maintainer": {"name": "Untrusted", "verified": True},
"steps": [{"flow_id": "request.main"}],
}}}), encoding="utf-8")
loaded = _load_curated_tour(str(path))
self.assertNotIn("maintainer", loaded)
self.assertEqual("tour.start", loaded["id"])


if __name__ == "__main__":
Expand Down
Loading
Loading