Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
d820529
fix(parsers/python): match imports by module components in both direc…
gadievron Jun 12, 2026
18d2640
feat(context): gate local-only suppression on trust_boundaries, not j…
gadievron Jun 12, 2026
73f0685
fix(cli): add zig to the --language choices for parse and scan
gadievron Jun 12, 2026
2d2edee
fix(go-parser): key generic-type methods on base type, not "unknown"
gadievron Jun 17, 2026
5d566bb
build(parsers/go): stop tracking the compiled go_parser binary
gadievron Jun 17, 2026
95a734e
fix(reachability): anchor user-input entry-point patterns with word b…
gadievron Jun 17, 2026
83599ff
Merge PR #118: feat(context): gate local-only suppression on trust_bo…
ar7casper Jun 28, 2026
36e9ee6
Merge PR #119: fix(cli): add zig to the --language choices for parse …
ar7casper Jun 28, 2026
7e77376
Merge PR #116: fix(parsers/python): match imports by module component…
ar7casper Jun 28, 2026
b3df808
Merge PR #126: fix(go-parser): key generic-type methods on base type
ar7casper Jun 28, 2026
4d25bf8
Merge PR #128: fix(reachability): anchor user-input entry-point patte…
ar7casper Jun 28, 2026
47291fc
fix(parsers): CM-A is_test_file anchored to path components, not subs…
gadievron Jun 8, 2026
3f14acc
fix(parsers/zig): u13 call_graph_builder — local-type/builtin/const-a…
gadievron Jun 8, 2026
f780a73
fix(parsers/zig): u14 function_extractor — fn-name/test-classificatio…
gadievron Jun 8, 2026
1ffefe2
fix(parsers/c): u1 call_graph_builder — builtin-leak same-file pre-ch…
gadievron Jun 8, 2026
2d521c1
fix(parsers/c): u2 function_extractor — ctor/dtor/struct/union/lambda…
gadievron Jun 8, 2026
eee2bcf
fix(parsers/c): u3 unit_generator — carry is_inline into the unit (BU…
gadievron Jun 8, 2026
1e48d89
fix(parsers/php): u5 call_graph_builder — variable-fn + parent::/self…
gadievron Jun 8, 2026
5f97c39
fix(parsers/c): land deferred [51] member-dispatch + [30] virtual/inh…
gadievron Jun 9, 2026
7eb6320
fix(parsers/ruby): u10 call_graph_builder — parenless/crossfile-class…
gadievron Jun 8, 2026
b5f930d
fix(parsers/php): u6 function_extractor — nested-fn/attribute-start_l…
gadievron Jun 8, 2026
f2a5041
fix(parsers/php): u7 unit_generator — namespace key-drift + is_static…
gadievron Jun 8, 2026
4a64396
fix(parsers/ruby): u11 function_extractor — nested/singleton/define_m…
gadievron Jun 8, 2026
3b1a00b
fix(parsers/ruby): guard alias resolution to single unconditional bin…
gadievron Jun 9, 2026
114f5b4
fix(reachability): u12 entry_point_detector — seed silent binary main…
gadievron Jun 8, 2026
5aaacb6
fix(parsers/go): u4 BUG-5 — normalize call_graph.json func records to…
gadievron Jun 8, 2026
cfb0dcc
fix(parsers/go): [5] re-verify — complete the snake-case normalizatio…
gadievron Jun 9, 2026
72f0294
fix(parsers/go): u4 callgraph — func-value alias edge + top-level cal…
gadievron Jun 8, 2026
27160de
feat(reachability): opt-in library-mode seeds the public API surface
gadievron Jun 12, 2026
713e73b
fix(parsers/python): u9 function_extractor — 6 finder bugs (BUG-NEW 9…
gadievron Jun 8, 2026
d138016
fix(parsers/python): u8 call_graph_builder — builtin-leak/alias/impor…
gadievron Jun 8, 2026
d404c9e
fix(parsers/python): [34] re-verify — token-aware property classifica…
gadievron Jun 9, 2026
b6a9586
fix(parsers/python): guard alias resolution to single unconditional b…
gadievron Jun 9, 2026
2f005fa
fix(parsers/python): resolve self/cls-aliased method calls in the cal…
gadievron Jun 11, 2026
aeeb641
fix(parsers/go): classify entry points by signature, not return type …
gadievron Jun 17, 2026
2a69057
fix(parsers/js): extract module-level block-scoped functions
gadievron Jun 14, 2026
169bd70
fix(parsers/c): resolve static-inline functions in included headers
gadievron Jun 13, 2026
858401e
fix(parsers/php): keep both same-name defs from conditional/guarded b…
gadievron Jun 14, 2026
0f4a9c8
feat(reachability): warn on silent library blackout across all parsers
gadievron Jun 12, 2026
9b0b6c6
fix(parsers/c): stop same-(file,name) functions collapsing to one fun…
gadievron Jun 13, 2026
fed05e2
fix(parsers/python): extract block-scoped def/class units
gadievron Jun 14, 2026
e759fa1
fix(parsers/ruby): keep both same-name defs from conditional branches
gadievron Jun 14, 2026
d5a10a6
feat(reachability): extend opt-in library-mode to all parsers + CLI
gadievron Jun 12, 2026
e66d588
fix(zig,php parsers): attribute generic-container & anonymous-class m…
gadievron Jun 17, 2026
4058a1c
docs(zig,php parsers): surface grammar-alignment prerequisite in regr…
gadievron Jun 17, 2026
f7d32ac
fix(parsers/php): resolve $this->/self:: calls into used traits
gadievron Jun 17, 2026
080d67d
fix(parsers/c): guard macro-alias resolution against cyclic #defines
gadievron Jun 17, 2026
18fcd4f
Merge PR #108: All parsers: anchor is_test_file to path components
ar7casper Jul 5, 2026
21efd08
Merge PR #109: Go parser + reachability: fix 3 finder bugs
ar7casper Jul 5, 2026
f00c649
Merge PR #110: Zig parser: fix 6 finder bugs
ar7casper Jul 5, 2026
620e7d6
Merge PR #111: PHP parser: fix 8 finder bugs
ar7casper Jul 5, 2026
a4557ac
Merge PR #112: Python parser: fix 10 finder bugs
ar7casper Jul 5, 2026
2a95ca3
Merge PR #113: Ruby parser: fix 11 finder bugs
ar7casper Jul 5, 2026
8b85212
Merge PR #114: C/C++ parser: fix 11 finder bugs
ar7casper Jul 5, 2026
f3ca103
Merge PR #117: feat(reachability): opt-in library-mode seeds the publ…
ar7casper Jul 5, 2026
956c480
Merge PR #115: fix(parsers/python): resolve self/cls-aliased method c…
ar7casper Jul 5, 2026
7787607
Merge PR #125: fix(parsers/python): extract block-scoped def/class units
ar7casper Jul 5, 2026
0ebd013
Merge PR #129: fix(parsers/go): classify entry points by signature, n…
ar7casper Jul 5, 2026
3fb7bff
Merge PR #121: fix(parsers/c): header static-inline call edges + same…
ar7casper Jul 5, 2026
e09e6db
Merge PR #122: fix(parsers/php,ruby): keep both same-name defs from c…
ar7casper Jul 5, 2026
26ccf42
Merge PR #127: fix(zig,php parsers): attribute generic-container & an…
ar7casper Jul 5, 2026
a6d0398
Merge PR #120: feat(reachability): library-mode for all parsers + sil…
ar7casper Jul 5, 2026
35a8f13
Merge PR #124: fix(parsers/js): extract module-level block-scoped fun…
ar7casper Jul 5, 2026
4d948e0
Merge PR #130: fix(parsers/c): guard macro-alias resolution against c…
ar7casper Jul 5, 2026
2dabeaf
Merge PR #131: fix(parsers/php): resolve $this->/self:: calls into us…
ar7casper Jul 5, 2026
85dbcfb
fix(parser_adapter): restore --fresh dataset deletion silently droppe…
ar7casper Jul 5, 2026
4905f90
fix(parsers): normalize repo-relative paths to POSIX so func_ids matc…
ar7casper Jul 5, 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
24 changes: 22 additions & 2 deletions libs/openant-core/context/application_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,26 @@ def get_type_info(self) -> dict:
"""Get detailed information about this application type."""
return APPLICATION_TYPE_INFO.get(self.application_type, {})

def suppress_local_only(self) -> bool:
"""Whether to tell the analyzer to flag only REMOTE-attacker vulnerabilities.

The "local users have access, only flag remote" framing is correct for a
CLI/library whose inputs are all operator-controlled. But a data-processing
library (parser, deserializer, codec) takes UNTRUSTED INPUT DATA — and that
data crossing into the code IS the attack surface, even with no network
listener. ``requires_remote_trigger`` alone (False for every library) would
suppress exactly those bugs. Gate on the already-captured ``trust_boundaries``:
if any input source is ``untrusted``, do NOT suppress, regardless of type.
"""
if self.requires_remote_trigger:
return False
# Case-insensitive: trust_boundaries values are LLM-generated and may
# deviate from the schema's lowercase 'untrusted' (e.g. 'Untrusted').
return not any(
str(level).lower() == "untrusted"
for level in (self.trust_boundaries or {}).values()
)


# Files to check for manual override (in order of priority)
MANUAL_OVERRIDE_FILES = [
Expand Down Expand Up @@ -488,7 +508,7 @@ def _build_type_descriptions() -> str:

**Guidelines:**
- `application_type`: MUST be one of: web_app, cli_tool, library, agent_framework, unsupported
- `requires_remote_trigger`: Set to `false` for cli_tool, library, agent_framework. Set to `true` for web_app.
- `requires_remote_trigger`: Set to `true` for web_app, AND for any cli_tool/library/agent_framework that PROCESSES UNTRUSTED INPUT DATA (a parser, deserializer, codec, file/format reader, or anything where `trust_boundaries` marks an input source `untrusted` — the untrusted data crossing into the code is the attack surface even with no network listener). Set to `false` only when every input source is operator-controlled/trusted.
- `confidence`: 0.0-1.0 based on how much information was available.
- Be specific in `not_a_vulnerability` - these will directly prevent false positives.
"""
Expand Down Expand Up @@ -644,7 +664,7 @@ def format_context_for_prompt(context: ApplicationContext) -> str:
lines.append(f"- {item}")
lines.append("")

if not context.requires_remote_trigger:
if context.suppress_local_only():
lines.append("**IMPORTANT:** This is a CLI tool/library. Users running this code have local access.")
lines.append("Only flag vulnerabilities that could be exploited by a REMOTE attacker, not by local users.")
lines.append("")
Expand Down
66 changes: 51 additions & 15 deletions libs/openant-core/core/parser_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def parse_repository(
name: str = None,
diff_manifest: str | None = None,
fresh: bool = False,
library_mode: bool = False,
) -> ParseResult:
"""Parse a repository into an OpenAnt dataset.

Expand All @@ -96,6 +97,8 @@ def parse_repository(
fresh: If True, delete existing dataset.json before parsing so all
units are regenerated from scratch. Only dataset.json is deleted;
other artifacts in output_dir (e.g. analyzer outputs) are preserved.
library_mode: If True, seed the public API surface as reachability
entry points (opt-in, union-only).

Returns:
ParseResult with paths to generated files and stats.
Expand Down Expand Up @@ -127,19 +130,19 @@ def parse_repository(

# Dispatch to the right parser
if language == "python":
result = _parse_python(repo_path, output_dir, processing_level, skip_tests, name)
result = _parse_python(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
elif language == "javascript":
result = _parse_javascript(repo_path, output_dir, processing_level, skip_tests, name)
result = _parse_javascript(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
elif language == "go":
result = _parse_go(repo_path, output_dir, processing_level, skip_tests, name)
result = _parse_go(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
elif language == "c":
result = _parse_c(repo_path, output_dir, processing_level, skip_tests, name)
result = _parse_c(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
elif language == "ruby":
result = _parse_ruby(repo_path, output_dir, processing_level, skip_tests, name)
result = _parse_ruby(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
elif language == "php":
result = _parse_php(repo_path, output_dir, processing_level, skip_tests, name)
result = _parse_php(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
elif language == "zig":
result = _parse_zig(repo_path, output_dir, processing_level, skip_tests, name)
result = _parse_zig(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
else:
raise ValueError(f"Unsupported language: {language}")

Expand Down Expand Up @@ -207,11 +210,18 @@ def _maybe_apply_diff_filter(
# Reachability filter (shared by Python path; JS/Go handle it internally)
# ---------------------------------------------------------------------------

# library_seed_ids is now shared in utilities/agentic_enhancer/entry_point_detector.py
# so every parser pipeline (not just Python) can seed the public API. It is loaded
# below via the same importlib path as EntryPointDetector to dodge the heavy
# utilities/__init__ imports.


def apply_reachability_filter(
dataset: dict,
output_dir: str,
processing_level: str,
extra_entry_points: "set[str] | None" = None,
library_mode: bool = False,
) -> dict:
"""Filter dataset units to only those reachable from entry points.

Expand Down Expand Up @@ -254,6 +264,8 @@ def _load_module(name, filename):
_epd = _load_module("entry_point_detector", "entry_point_detector.py")
_ra = _load_module("reachability_analyzer", "reachability_analyzer.py")
EntryPointDetector = _epd.EntryPointDetector
blackout_warning = _epd.blackout_warning
library_seed_ids = _epd.library_seed_ids
ReachabilityAnalyzer = _ra.ReachabilityAnalyzer

call_graph_path = os.path.join(output_dir, "call_graph.json")
Expand All @@ -277,6 +289,11 @@ def _load_module(name, filename):
entry_points = detector.detect_entry_points()
if extra_entry_points:
entry_points = entry_points | extra_entry_points
# Library-mode (opt-in): the public API is the entry surface. Union-only —
# never demotes a structurally-detected app entry point, so an app scan with
# the flag on can only gain reachable units, never lose one.
if library_mode:
entry_points = entry_points | library_seed_ids(functions)

units = dataset.get("units", [])
original_count = len(units)
Expand Down Expand Up @@ -349,6 +366,12 @@ def _load_module(name, filename):
file=sys.stderr,
)

_blackout = blackout_warning(detector.entry_point_details, original_count,
len(filtered_units), library_mode=library_mode)
if _blackout:
dataset["metadata"]["reachability_filter"]["warning"] = _blackout
print(f" [Warning] {_blackout}", file=sys.stderr)

# Warn about unimplemented higher-level filters
if processing_level == "codeql":
print(
Expand All @@ -374,7 +397,7 @@ def _load_module(name, filename):
# Python parser
# ---------------------------------------------------------------------------

def _parse_python(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
def _parse_python(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
"""Invoke the Python parser.

The Python parser has a clean `parse_repository()` function that we can
Expand Down Expand Up @@ -402,7 +425,8 @@ def _parse_python(repo_path: str, output_dir: str, processing_level: str, skip_t

# Apply reachability filter if processing_level requires it
if processing_level != "all":
dataset = _apply_reachability_filter(dataset, output_dir, processing_level)
dataset = _apply_reachability_filter(dataset, output_dir, processing_level,
library_mode=library_mode)

# Write outputs
write_json(dataset_path, dataset)
Expand Down Expand Up @@ -523,7 +547,7 @@ def _file_lock(lock_path: Path):
f.close()


def _parse_javascript(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
def _parse_javascript(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
"""Invoke the JavaScript/TypeScript parser.

The JS parser is a PipelineTest class that runs Node.js subprocesses.
Expand All @@ -547,6 +571,8 @@ def _parse_javascript(repo_path: str, output_dir: str, processing_level: str, sk
cmd.extend(["--name", name])
if skip_tests:
cmd.append("--skip-tests")
if library_mode:
cmd.append("--library-mode")

result = subprocess.run(
cmd,
Expand Down Expand Up @@ -582,7 +608,7 @@ def _parse_javascript(repo_path: str, output_dir: str, processing_level: str, sk
# Go parser
# ---------------------------------------------------------------------------

def _parse_go(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
def _parse_go(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
"""Invoke the Go parser.

The Go parser is a PipelineTest class that calls a compiled Go binary.
Expand All @@ -603,6 +629,8 @@ def _parse_go(repo_path: str, output_dir: str, processing_level: str, skip_tests
cmd.extend(["--name", name])
if skip_tests:
cmd.append("--skip-tests")
if library_mode:
cmd.append("--library-mode")

result = subprocess.run(
cmd,
Expand Down Expand Up @@ -638,7 +666,7 @@ def _parse_go(repo_path: str, output_dir: str, processing_level: str, skip_tests
# C/C++ parser
# ---------------------------------------------------------------------------

def _parse_c(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
def _parse_c(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
"""Invoke the C/C++ parser.

The C parser uses tree-sitter for function extraction and call graph
Expand All @@ -661,6 +689,8 @@ def _parse_c(repo_path: str, output_dir: str, processing_level: str, skip_tests:
cmd.extend(["--name", name])
if skip_tests:
cmd.append("--skip-tests")
if library_mode:
cmd.append("--library-mode")

result = subprocess.run(
cmd,
Expand Down Expand Up @@ -697,7 +727,7 @@ def _parse_c(repo_path: str, output_dir: str, processing_level: str, skip_tests:
# Ruby parser
# ---------------------------------------------------------------------------

def _parse_ruby(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
def _parse_ruby(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
"""Invoke the Ruby parser.

The Ruby parser uses tree-sitter for function extraction and call graph
Expand All @@ -720,6 +750,8 @@ def _parse_ruby(repo_path: str, output_dir: str, processing_level: str, skip_tes
cmd.extend(["--name", name])
if skip_tests:
cmd.append("--skip-tests")
if library_mode:
cmd.append("--library-mode")

result = subprocess.run(
cmd,
Expand Down Expand Up @@ -756,7 +788,7 @@ def _parse_ruby(repo_path: str, output_dir: str, processing_level: str, skip_tes
# PHP parser
# ---------------------------------------------------------------------------

def _parse_php(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
def _parse_php(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
"""Invoke the PHP parser.

The PHP parser uses tree-sitter for function extraction and call graph
Expand All @@ -779,6 +811,8 @@ def _parse_php(repo_path: str, output_dir: str, processing_level: str, skip_test
cmd.extend(["--name", name])
if skip_tests:
cmd.append("--skip-tests")
if library_mode:
cmd.append("--library-mode")

result = subprocess.run(
cmd,
Expand Down Expand Up @@ -815,7 +849,7 @@ def _parse_php(repo_path: str, output_dir: str, processing_level: str, skip_test
# Zig parser
# ---------------------------------------------------------------------------

def _parse_zig(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
def _parse_zig(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
"""Invoke the Zig parser.

The Zig parser uses tree-sitter for function extraction and call graph
Expand All @@ -838,6 +872,8 @@ def _parse_zig(repo_path: str, output_dir: str, processing_level: str, skip_test
cmd.extend(["--name", name])
if skip_tests:
cmd.append("--skip-tests")
if library_mode:
cmd.append("--library-mode")

result = subprocess.run(
cmd,
Expand Down
2 changes: 2 additions & 0 deletions libs/openant-core/core/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def scan_repository(
diff_manifest: str | None = None,
llm_reachability: bool = False,
llm_reachability_max_code_bytes: int = 1500,
library_mode: bool = False,
) -> ScanResult:
"""Scan a repository for vulnerabilities.

Expand Down Expand Up @@ -171,6 +172,7 @@ def _step_label(name: str) -> str:
processing_level=effective_parse_level,
skip_tests=skip_tests,
diff_manifest=diff_manifest,
library_mode=library_mode,
)

ctx.summary = {
Expand Down
10 changes: 8 additions & 2 deletions libs/openant-core/openant/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def cmd_scan(args):
repo_url=getattr(args, "repo_url", None),
commit_sha=getattr(args, "commit_sha", None),
diff_manifest=getattr(args, "diff_manifest", None),
library_mode=getattr(args, "library_mode", False),
llm_reachability=getattr(args, "llm_reachability", False),
llm_reachability_max_code_bytes=getattr(
args, "llm_reachability_max_code_bytes", 1500
Expand Down Expand Up @@ -129,6 +130,7 @@ def cmd_parse(args):
name=getattr(args, "name", None),
diff_manifest=getattr(args, "diff_manifest", None),
fresh=getattr(args, "fresh", False),
library_mode=getattr(args, "library_mode", False),
)

ctx.summary = {
Expand Down Expand Up @@ -972,7 +974,7 @@ def main():
scan_p.add_argument("--output", "-o", help="Output directory (default: temp dir)")
scan_p.add_argument(
"--language", "-l",
choices=["auto", "python", "javascript", "go", "c", "ruby", "php"],
choices=["auto", "python", "javascript", "go", "c", "ruby", "php", "zig"],
default="auto",
help="Language (default: auto-detect)",
)
Expand All @@ -995,6 +997,8 @@ def main():
scan_p.add_argument("--dynamic-test", action="store_true",
help="Enable Docker-isolated dynamic testing (off by default)")
scan_p.add_argument("--no-skip-tests", action="store_true", help="Include test files in parsing (default: tests are skipped)")
scan_p.add_argument("--library-mode", action="store_true",
help="Seed the exported public API as entry points (for libraries with no main/route/CLI entry point)")
scan_p.add_argument("--limit", type=int, help="Max units to analyze")
scan_p.add_argument(
"--llm-config",
Expand Down Expand Up @@ -1047,7 +1051,7 @@ def main():
parse_p.add_argument("--output", "-o", help="Output directory (default: temp dir)")
parse_p.add_argument(
"--language", "-l",
choices=["auto", "python", "javascript", "go", "c", "ruby", "php"],
choices=["auto", "python", "javascript", "go", "c", "ruby", "php", "zig"],
default="auto",
help="Language (default: auto-detect)",
)
Expand All @@ -1058,6 +1062,8 @@ def main():
help="Processing level (default: reachable)",
)
parse_p.add_argument("--no-skip-tests", action="store_true", help="Include test files in parsing (default: tests are skipped)")
parse_p.add_argument("--library-mode", action="store_true",
help="Seed the exported public API as entry points (for libraries with no main/route/CLI entry point)")
parse_p.add_argument("--name", help="Dataset name (default: derived from repo path)")
parse_p.add_argument("--diff-manifest", help="Path to diff_manifest.json; tags units with diff_selected")
parse_p.add_argument("--fresh", action="store_true",
Expand Down
Loading
Loading