parseforge 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
parseforge/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.2.1"
File without changes
parseforge/cli/main.py ADDED
@@ -0,0 +1,55 @@
1
+ """parseforge CLI entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from parseforge import naming
8
+
9
+
10
+ @click.group()
11
+ @click.version_option(package_name="parseforge")
12
+ def main() -> None:
13
+ """ParseForge — forge, validate, and promote TextFSM templates from CLI output."""
14
+
15
+
16
+ @main.command("name")
17
+ @click.option("--vendor", required=True)
18
+ @click.option("--family", required=True)
19
+ @click.option("--os", "os_", required=True)
20
+ @click.option("--version", required=True)
21
+ @click.argument("command", nargs=-1, required=True)
22
+ def name_cmd(
23
+ vendor: str, family: str, os_: str, version: str, command: tuple[str, ...]
24
+ ) -> None:
25
+ """Print the canonical cli-name for a raw CLI COMMAND.
26
+
27
+ Looked up in the local cache first; only sent to an LLM (via a
28
+ RegexBuilder) on a cache miss. No RegexBuilder is wired into the
29
+ scaffold yet, so this only succeeds for commands already in
30
+ ~/.parseforge/.cli-name.json until one is implemented.
31
+ """
32
+ context = naming.CliContext(vendor=vendor, family=family, os=os_, version=version)
33
+ click.echo(naming.cli_name(" ".join(command), context))
34
+
35
+
36
+ @main.command("run")
37
+ @click.option("--vendor", required=True)
38
+ @click.option("--family", required=True)
39
+ @click.option("--os", "os_", required=True)
40
+ @click.option("--version", required=True)
41
+ @click.argument("command", nargs=-1, required=True)
42
+ def run_cmd(
43
+ vendor: str, family: str, os_: str, version: str, command: tuple[str, ...]
44
+ ) -> None:
45
+ """Run the pipeline for a single CLI COMMAND against a device (§5)."""
46
+ from parseforge.pipeline import run_command_pipeline
47
+
48
+ run_command_pipeline(
49
+ " ".join(command),
50
+ key_fields={"vendor": vendor, "family": family, "os": os_, "version": version},
51
+ )
52
+
53
+
54
+ if __name__ == "__main__":
55
+ main()
@@ -0,0 +1,40 @@
1
+ """Generation stage — LLM call and template extraction (SPEC.md §5 steps 5-6).
2
+
3
+ Writes raw-llm-response.txt / usage.txt (step 5), then extracts and
4
+ cleans the template into raw-template / template.textfsm (step 6).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Protocol
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class GenerationResult:
15
+ raw_response: str
16
+ usage: str
17
+ raw_template: str
18
+ template: str
19
+
20
+
21
+ class Generator(Protocol):
22
+ """An LLM backend capable of producing a TextFSM template from sample input.
23
+
24
+ ``prior_context`` carries additional samples of the same command when
25
+ running in Mode 1 (batch) per §4.
26
+ """
27
+
28
+ def generate(
29
+ self, input_text: str, prior_context: list[str] | None = None
30
+ ) -> GenerationResult: ...
31
+
32
+
33
+ def extract_template(raw_response: str) -> str:
34
+ """Pull the template body out of a raw LLM response (step 6, pre-cleanup)."""
35
+ raise NotImplementedError("extraction rule TBD — see SPEC.md §5 step 6")
36
+
37
+
38
+ def clean_template(raw_template: str) -> str:
39
+ """Normalize an extracted template into a valid template.textfsm body."""
40
+ raise NotImplementedError("cleanup rule TBD — see SPEC.md §5 step 6")
@@ -0,0 +1,15 @@
1
+ from .assemble import pattern_to_cli_name
2
+ from .cache import DEFAULT_INDEX_PATH, NameIndex
3
+ from .llm import CliContext, RegexBuilder, UnimplementedRegexBuilder, build_prompt
4
+ from .resolver import cli_name
5
+
6
+ __all__ = [
7
+ "cli_name",
8
+ "CliContext",
9
+ "RegexBuilder",
10
+ "UnimplementedRegexBuilder",
11
+ "build_prompt",
12
+ "NameIndex",
13
+ "DEFAULT_INDEX_PATH",
14
+ "pattern_to_cli_name",
15
+ ]
@@ -0,0 +1,35 @@
1
+ """Turn a validated regex pattern into a canonical, kebab-case cli-name.
2
+
3
+ Fixed-text segments pass through as-is; named capture groups
4
+ (``(?P<var1>...)``) become their group name. Tokens are joined with
5
+ ``-`` and lowercased, matching SPEC.md §2's naming convention.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+
12
+ _LEADING_FLAGS = re.compile(r"^\(\?[a-zA-Z]+\)")
13
+ _NAMED_GROUP = re.compile(r"^\(\?P<(?P<name>\w+)>.*\)$")
14
+ _ESCAPED_CHAR = re.compile(r"\\(.)")
15
+
16
+
17
+ def pattern_to_cli_name(pattern: str) -> str:
18
+ """
19
+ >>> pattern_to_cli_name(r"(?i)show\\s+version")
20
+ 'show-version'
21
+ >>> pattern_to_cli_name(r"(?i)show\\s+interface\\s+(?P<var1>\\S+)\\s+status")
22
+ 'show-interface-var1-status'
23
+ """
24
+ body = _LEADING_FLAGS.sub("", pattern)
25
+ tokens = [t for t in re.split(r"\\s\+", body) if t.strip()]
26
+
27
+ parts: list[str] = []
28
+ for token in tokens:
29
+ token = token.strip()
30
+ group = _NAMED_GROUP.match(token)
31
+ if group:
32
+ parts.append(group.group("name"))
33
+ else:
34
+ parts.append(_ESCAPED_CHAR.sub(r"\1", token))
35
+ return "-".join(parts).lower()
@@ -0,0 +1,48 @@
1
+ """On-disk cli-name -> regex index, so a CLI is only ever sent to the LLM once.
2
+
3
+ Persisted as a flat JSON dict (SPEC.md naming discussion):
4
+ {"show-version": "(?i)show\\s+version", ...}
5
+
6
+ The cli-name segment is vendor/OS-agnostic by design — vendor, family, os,
7
+ and version are already distinguishing directory levels in the storage
8
+ layout (SPEC.md §3), so a single global index is intentional rather than
9
+ one index per device context.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import re
16
+ from pathlib import Path
17
+
18
+ DEFAULT_INDEX_PATH = Path.home() / ".parseforge" / ".cli-name.json"
19
+
20
+
21
+ class NameIndex:
22
+ """Loads, queries, and persists the cli-name -> regex mapping."""
23
+
24
+ def __init__(self, path: Path = DEFAULT_INDEX_PATH) -> None:
25
+ self.path = path
26
+ self._entries: dict[str, str] = self._load()
27
+
28
+ def _load(self) -> dict[str, str]:
29
+ if not self.path.exists():
30
+ return {}
31
+ return json.loads(self.path.read_text(encoding="utf-8"))
32
+
33
+ def match(self, command: str) -> str | None:
34
+ """Return the cli-name of the first stored pattern that fully matches
35
+ ``command``, or None if this command hasn't been named before."""
36
+ for cli_name, pattern in self._entries.items():
37
+ if re.fullmatch(pattern, command):
38
+ return cli_name
39
+ return None
40
+
41
+ def add(self, cli_name: str, pattern: str) -> None:
42
+ self._entries[cli_name] = pattern
43
+
44
+ def save(self) -> None:
45
+ self.path.parent.mkdir(parents=True, exist_ok=True)
46
+ self.path.write_text(
47
+ json.dumps(self._entries, indent=2, sort_keys=True), encoding="utf-8"
48
+ )
@@ -0,0 +1,62 @@
1
+ """LLM-driven regex construction for a CLI command.
2
+
3
+ Only called on a cache miss (see resolver.py) — once a command's pattern
4
+ is in the index, matching is pure regex and costs no tokens.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Protocol
11
+
12
+ PROMPT_TEMPLATE = """\
13
+ This is CLI "{command}" of {vendor} {family} {os} version {version}.
14
+ Create a regex pattern to match it:
15
+ - If a token is fixed text, use it as-is.
16
+ - If a token is variable text, use a regex to match it, and name the \
17
+ capture group var1, var2, ... in left-to-right order of appearance.
18
+ - Use \\s+ to match whitespace between tokens.
19
+
20
+ Example 1: "show version" -> "show" and "version" are fixed text ->
21
+ r"(?i)show\\s+version"
22
+
23
+ Example 2: "show interface Ge1.1 status" -> "show", "interface", and \
24
+ "status" are fixed text ->
25
+ r"(?i)show\\s+interface\\s+(?P<var1>\\S+)\\s+status"
26
+
27
+ Respond with only the regex pattern, nothing else.
28
+ """
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class CliContext:
33
+ vendor: str
34
+ family: str
35
+ os: str
36
+ version: str
37
+
38
+
39
+ def build_prompt(command: str, context: CliContext) -> str:
40
+ return PROMPT_TEMPLATE.format(
41
+ command=command,
42
+ vendor=context.vendor,
43
+ family=context.family,
44
+ os=context.os,
45
+ version=context.version,
46
+ )
47
+
48
+
49
+ class RegexBuilder(Protocol):
50
+ """An LLM backend that turns a raw CLI command into a regex pattern."""
51
+
52
+ def build_pattern(self, command: str, context: CliContext) -> str: ...
53
+
54
+
55
+ class UnimplementedRegexBuilder:
56
+ """Default RegexBuilder — no LLM client is wired into the scaffold yet."""
57
+
58
+ def build_pattern(self, command: str, context: CliContext) -> str:
59
+ raise NotImplementedError(
60
+ "no RegexBuilder configured — wire an LLM client that sends "
61
+ "build_prompt(command, context) and returns the regex pattern"
62
+ )
@@ -0,0 +1,45 @@
1
+ """Resolve a raw CLI command to its canonical cli-name, LLM-backed with caching.
2
+
3
+ Flow:
4
+ 1. Check the on-disk index (:mod:`parseforge.naming.cache`) for a stored
5
+ pattern that already matches this command — no LLM call on a hit.
6
+ 2. On a miss, ask the LLM to build a regex for it
7
+ (:mod:`parseforge.naming.llm`).
8
+ 3. Validate the LLM's pattern actually matches its own source command via
9
+ ``re.fullmatch`` before trusting it.
10
+ 4. Assemble the cli-name from the pattern (:mod:`parseforge.naming.assemble`)
11
+ and persist the new cli-name -> pattern entry to the index.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from pathlib import Path
18
+
19
+ from .assemble import pattern_to_cli_name
20
+ from .cache import DEFAULT_INDEX_PATH, NameIndex
21
+ from .llm import CliContext, RegexBuilder, UnimplementedRegexBuilder
22
+
23
+
24
+ def cli_name(
25
+ command: str,
26
+ context: CliContext,
27
+ builder: RegexBuilder = UnimplementedRegexBuilder(),
28
+ index_path: Path = DEFAULT_INDEX_PATH,
29
+ ) -> str:
30
+ index = NameIndex(index_path)
31
+
32
+ cached = index.match(command)
33
+ if cached is not None:
34
+ return cached
35
+
36
+ pattern = builder.build_pattern(command, context)
37
+ if not re.fullmatch(pattern, command):
38
+ raise ValueError(
39
+ f"LLM-built pattern {pattern!r} does not match its own command {command!r}"
40
+ )
41
+
42
+ name = pattern_to_cli_name(pattern)
43
+ index.add(name, pattern)
44
+ index.save()
45
+ return name
parseforge/paths.py ADDED
@@ -0,0 +1,68 @@
1
+ """Storage layout — three-tier promotion path resolution (SPEC.md §3).
2
+
3
+ Shared path prefix under all three tiers:
4
+ <vendor>/<device-family>/<os>/<version>/<cli-name>/
5
+
6
+ Use hyphenated OS family names (``ios-xe``, ``nx-os``) once multiple
7
+ Cisco OS families share the tree, per §3's note.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import random
13
+ import string
14
+ from dataclasses import dataclass
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+
18
+ TRIALS = "trials"
19
+ INTEGRATION = "integration"
20
+ AUTHORITATIVE = "authoritative"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class DeviceKey:
25
+ """Identifies a template family: vendor/device-family/os/version/cli-name."""
26
+
27
+ vendor: str
28
+ family: str
29
+ os: str
30
+ version: str
31
+ cli_name: str
32
+
33
+ def relative_path(self) -> Path:
34
+ return Path(self.vendor, self.family, self.os, self.version, self.cli_name)
35
+
36
+
37
+ def tier_root(store_root: Path, tier: str) -> Path:
38
+ if tier not in (TRIALS, INTEGRATION, AUTHORITATIVE):
39
+ raise ValueError(f"unknown tier: {tier!r}")
40
+ return store_root / tier
41
+
42
+
43
+ def tier_path(store_root: Path, tier: str, key: DeviceKey) -> Path:
44
+ """Resolve the directory for ``key`` under the given tier."""
45
+ return tier_root(store_root, tier) / key.relative_path()
46
+
47
+
48
+ def new_run_id() -> str:
49
+ """Timestamp+shortid run identifier (§3.1) — chronological, collision-safe."""
50
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
51
+ shortid = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
52
+ return f"{timestamp}-{shortid}"
53
+
54
+
55
+ def trial_run_dir(store_root: Path, key: DeviceKey, run_id: str | None = None) -> Path:
56
+ """Directory for a single trial run: trials/.../<cli-name>/<run-id>/."""
57
+ run_id = run_id or new_run_id()
58
+ return tier_path(store_root, TRIALS, key) / run_id
59
+
60
+
61
+ def integration_result_dir(store_root: Path, key: DeviceKey) -> Path:
62
+ """The single common-result candidate directory for a cli-name (§3.2)."""
63
+ return tier_path(store_root, INTEGRATION, key) / "common-result"
64
+
65
+
66
+ def authoritative_dir(store_root: Path, key: DeviceKey) -> Path:
67
+ """The approved, in-production directory for a cli-name (§3.3)."""
68
+ return tier_path(store_root, AUTHORITATIVE, key)
parseforge/pipeline.py ADDED
@@ -0,0 +1,49 @@
1
+ """Pipeline orchestration (SPEC.md §4, §5).
2
+
3
+ Mode 2 (loop: sample -> generate, per command) is the MVP per §4's
4
+ recommendation. Mode 1 (batch: gather-then-generate) is a config flag
5
+ that only changes the sampling stage — generation, storage, and
6
+ validation stay identical between modes.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from enum import Enum
12
+
13
+ from parseforge import naming, paths
14
+
15
+
16
+ class Mode(str, Enum):
17
+ LOOP = "loop" # MVP: sample -> generate, per command (§4)
18
+ BATCH = "batch" # collect N samples per command before generation (§4)
19
+
20
+
21
+ def run_command_pipeline(
22
+ command: str,
23
+ key_fields: dict,
24
+ regex_builder: naming.RegexBuilder = naming.UnimplementedRegexBuilder(),
25
+ mode: Mode = Mode.LOOP,
26
+ ) -> None:
27
+ """Execute steps 1-7 of the pipeline for a single CLI command.
28
+
29
+ 1. Input intake is the caller's responsibility (device info, auth,
30
+ command list, mode selection already resolved into ``key_fields``,
31
+ which must supply vendor/family/os/version).
32
+ 2. Name generation — :func:`parseforge.naming.cli_name` (LLM-backed,
33
+ cached — see :mod:`parseforge.naming`).
34
+ 3. Path resolution — :mod:`parseforge.paths`.
35
+ 4. Sampling — :mod:`parseforge.sampling`.
36
+ 5-6. Generation — :mod:`parseforge.generation`.
37
+ 7. Self-validation — :mod:`parseforge.validation`.
38
+
39
+ Steps 8-11 (integration selection, authoritative promotion, drift
40
+ monitoring, repeat/loop) run out-of-band across all trials for a
41
+ cli-name, not per single pipeline invocation — see
42
+ :mod:`parseforge.validation` and :mod:`parseforge.promotion`.
43
+ """
44
+ context = naming.CliContext(**key_fields)
45
+ name = naming.cli_name(command, context, builder=regex_builder)
46
+ key = paths.DeviceKey(cli_name=name, **key_fields)
47
+ raise NotImplementedError(
48
+ f"wire sampling -> generation -> validation for {key!r} (mode={mode.value})"
49
+ )
@@ -0,0 +1,43 @@
1
+ """Authoritative promotion and drift monitoring (SPEC.md §3.3, §5 steps 9-10)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+
8
+
9
+ class PromotionDecision(str, Enum):
10
+ AUTO_PROMOTED = "auto_promoted"
11
+ QUEUED_FOR_REVIEW = "queued_for_review"
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class PromotionGate:
16
+ """Confidence threshold gating auto-promotion vs. human review (§3.3, §7).
17
+
18
+ Left configurable per-project rather than hardcoded — see the open
19
+ question in SPEC.md §7.
20
+ """
21
+
22
+ match_rate_threshold: float = 1.0
23
+ min_sample_count: int = 1
24
+
25
+
26
+ def decide_promotion(
27
+ match_rate: float, sample_count: int, gate: PromotionGate
28
+ ) -> PromotionDecision:
29
+ if (
30
+ sample_count >= gate.min_sample_count
31
+ and match_rate >= gate.match_rate_threshold
32
+ ):
33
+ return PromotionDecision.AUTO_PROMOTED
34
+ return PromotionDecision.QUEUED_FOR_REVIEW
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class DriftStatus:
39
+ match_rate: float
40
+ status: str # "ok" | "drifting" | "superseded" — see SPEC.md §6
41
+
42
+ def breached(self, threshold: float) -> bool:
43
+ return self.match_rate < threshold
parseforge/sampling.py ADDED
@@ -0,0 +1,36 @@
1
+ """Sampling stage — connect to a device and capture raw CLI output (SPEC.md §5 step 4).
2
+
3
+ Kept as a separable stage so that Mode 1 (batch: collect N samples per
4
+ command before generation) and Mode 2 (loop: sample-then-generate per
5
+ command) share the same generation/storage/validation stages and differ
6
+ only in how many samples this stage collects before handing off — see
7
+ §4.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Protocol
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class DeviceConnection:
18
+ host: str
19
+ username: str
20
+ password: str
21
+ device_type: str # e.g. "cisco_ios", "cisco_nxos"
22
+
23
+
24
+ class Sampler(Protocol):
25
+ """A backend capable of running a command on a device and returning raw text.
26
+
27
+ A Netmiko-backed implementation is the expected default; see the
28
+ optional ``sampling`` extra in pyproject.toml.
29
+ """
30
+
31
+ def run_command(self, connection: DeviceConnection, command: str) -> str: ...
32
+
33
+
34
+ def sample(sampler: Sampler, connection: DeviceConnection, command: str) -> str:
35
+ """Run ``command`` against ``connection`` and return raw output for input.txt."""
36
+ return sampler.run_command(connection, command)
@@ -0,0 +1,50 @@
1
+ """Validation — self-validation (SPEC.md §5 step 7) and cross-validation
2
+ for integration selection (§3.2, §5 step 8).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ParseResult:
12
+ records: list[dict]
13
+ errors: list[str]
14
+
15
+ @property
16
+ def passed(self) -> bool:
17
+ return not self.errors
18
+
19
+
20
+ def parse(template_text: str, input_text: str) -> ParseResult:
21
+ """Run a template.textfsm against an input.txt sample.
22
+
23
+ Used both for self-validation (a template against its own sample,
24
+ step 7) and cross-validation (every candidate against every known
25
+ sample for a cli-name, step 8).
26
+ """
27
+ raise NotImplementedError("wrap textfsm.TextFSM — see SPEC.md §5 steps 7-8")
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class CandidateScore:
32
+ run_id: str
33
+ match_rate: float
34
+ failed_samples: list[str]
35
+
36
+
37
+ def select_common_result(
38
+ candidates: dict[str, str], samples: dict[str, str]
39
+ ) -> CandidateScore:
40
+ """Cross-validate every candidate template against every known sample.
41
+
42
+ ``candidates`` maps trial run_id -> template.textfsm text.
43
+ ``samples`` maps sample id -> input.txt text.
44
+
45
+ Promotes whichever template parses the largest share of *all* known
46
+ samples correctly (not just the sample it was generated from) — an
47
+ overfit template that only matches its own source sample must lose
48
+ to a template that generalizes, per §3.2.
49
+ """
50
+ raise NotImplementedError("scoring/selection rule — see SPEC.md §3.2")
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: parseforge
3
+ Version: 0.2.1
4
+ Summary: LLM-driven pipeline that forges, validates, and promotes TextFSM templates from network CLI output
5
+ Author-email: Tuyen Mathew Duong <tuyen@geekstrident.com>
6
+ Maintainer-email: Tuyen Mathew Duong <tuyen@geekstrident.com>
7
+ License: MIT
8
+ Keywords: textfsm,network automation,cli parsing,template generator,ai,llm,netmiko,device output parsing
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Information Technology
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Topic :: Software Development :: Code Generators
20
+ Classifier: Topic :: Text Processing
21
+ Classifier: Topic :: Utilities
22
+ Classifier: Operating System :: OS Independent
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: click>=8.1
27
+ Requires-Dist: PyYAML>=6.0
28
+ Requires-Dist: textfsm>=1.1.0
29
+ Provides-Extra: sampling
30
+ Requires-Dist: netmiko>=4.0; extra == "sampling"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
33
+ Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
34
+ Requires-Dist: ruff>=0.4.0; extra == "dev"
35
+ Requires-Dist: black>=24.0.0; extra == "dev"
36
+ Requires-Dist: mypy>=1.10.0; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # ParseForge
40
+
41
+ LLM-driven pipeline that forges, cross-validates, and promotes [TextFSM](https://github.com/google/textfsm)
42
+ templates from network device CLI output.
43
+
44
+ Full design plan: [SPEC.md](SPEC.md).
45
+
46
+ ## Layout
47
+
48
+ ```
49
+ parseforge/
50
+ naming.py CLI command -> canonical cli-name (SPEC §2)
51
+ paths.py trials/integration/authoritative path resolution (SPEC §3)
52
+ sampling.py device connection + command capture (SPEC §5 step 4)
53
+ generation.py LLM call + template extraction (SPEC §5 steps 5-6)
54
+ validation.py self-validation + cross-validation scoring (SPEC §5 steps 7-8)
55
+ promotion.py authoritative promotion gate + drift status (SPEC §3.3, §5 steps 9-10)
56
+ pipeline.py orchestrates the above; Mode LOOP (MVP) vs Mode BATCH (SPEC §4)
57
+ cli/ `parseforge` command-line entry point
58
+
59
+ store/ runtime template tree: trials/ -> integration/ -> authoritative/
60
+ (generated at runtime, not checked in — see .gitignore)
61
+ ```
62
+
63
+ `naming.py` and `paths.py` are implemented and tested. `sampling.py`, `generation.py`,
64
+ `validation.py`, `promotion.py`, and `pipeline.py` are stub interfaces — the SPEC.md
65
+ section cited in each docstring is the next thing to implement.
66
+
67
+ ## Development
68
+
69
+ ```
70
+ pip install -e ".[dev,sampling]"
71
+ pytest
72
+ ```
73
+
74
+ ## CLI
75
+
76
+ ```
77
+ parseforge name show interface GE1.1 status
78
+ # -> show-interface-var1-status
79
+ ```
@@ -0,0 +1,20 @@
1
+ parseforge/__init__.py,sha256=HfjVOrpTnmZ-xVFCYSVmX50EXaBQeJteUHG-PD6iQs8,22
2
+ parseforge/generation.py,sha256=bGUZqUramOJP5M4buqQY3iOcLG1v32tQ_QScUP_XkOg,1215
3
+ parseforge/paths.py,sha256=CWqAKL7oTMGTS7pxgcj_JCHKGd2EOLdOwVb-ATBObCA,2205
4
+ parseforge/pipeline.py,sha256=NDRt4Ennexz4mzhXkdiJ-btvI8JNU4iTXytb1k2Mypo,1905
5
+ parseforge/promotion.py,sha256=QZGatglsUzP3b-l00o0m97mAa_8DYcqA4er8ANqXMXc,1154
6
+ parseforge/sampling.py,sha256=iXQ5iVPoyVxIvms-A9m9Xh7qz4QP66ZrKqVceD81K-8,1182
7
+ parseforge/validation.py,sha256=GJYO2BibehU6zN5kBNsqC2zomgzOCDnLcqur-Gwgzy8,1518
8
+ parseforge/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ parseforge/cli/main.py,sha256=bhOmXezGt4eqntTDmVpnffNSQupP3FCHt_n-tyWQsUE,1779
10
+ parseforge/naming/__init__.py,sha256=UTN8x94Xc9vi2ESiLfOjbmBTG27z-KMRAKl7GBoRhq0,397
11
+ parseforge/naming/assemble.py,sha256=jjta415g5oQRRs6HB8mAAdH--Iw5DsSVvdKFmHSf67I,1114
12
+ parseforge/naming/cache.py,sha256=6UYRN_bBAnnfrh6XwuCBxGdRauBwbtEyFuF2t8UI2Fo,1661
13
+ parseforge/naming/llm.py,sha256=2uRaVp4x0JEedGMEGsMCMoooTR6_adhRO5GeDoVOlFQ,1868
14
+ parseforge/naming/resolver.py,sha256=PzWLAsvxIcdyfXdonOMWvHTLy836noSss-VwXtM_VPE,1421
15
+ parseforge-0.2.1.dist-info/licenses/LICENSE,sha256=LOxmz9EcTI5ykp5TQZioE-5QY7tsUinZjlXwrEEGkgc,1074
16
+ parseforge-0.2.1.dist-info/METADATA,sha256=SUXrIV2dRuogsmUL08k65YAkf3hTvzY18S_iU9Rls34,2983
17
+ parseforge-0.2.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
18
+ parseforge-0.2.1.dist-info/entry_points.txt,sha256=NhSC-qotioae8Lpw2TR2WY_ibzLJog-Bhu2IrYM7l0I,56
19
+ parseforge-0.2.1.dist-info/top_level.txt,sha256=IVTjnydw5oh5ylVuUAzW-BONPa82JP0hHJrhypCw8G0,11
20
+ parseforge-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ parseforge = parseforge.cli.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Geeks Trident LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ parseforge